Skip to content

Commit 55c6d61

Browse files
author
jiahaixin
committed
blog
1 parent 0604cef commit 55c6d61

13 files changed

Lines changed: 277 additions & 40 deletions

File tree

docs/.DS_Store

0 Bytes
Binary file not shown.

docs/.vuepress/.DS_Store

0 Bytes
Binary file not shown.

docs/.vuepress/dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 01cb7f86d902379df686ef6fd7c07ef534dcf388
1+
Subproject commit 6e12227e6cbc377c379237b62b79700c0d4fdbf4

docs/_images/.DS_Store

0 Bytes
Binary file not shown.

docs/_images/Spring/.DS_Store

6 KB
Binary file not shown.

docs/_images/Spring/aop-demo.svg

Lines changed: 3 additions & 0 deletions
Loading

docs/data-management/.DS_Store

0 Bytes
Binary file not shown.
Lines changed: 237 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,250 @@
1-
# Redis 分布式锁
1+
# 分布式锁
22

3-
## 一、什么是分布式锁?
3+
![](https://cdn.jsdelivr.net/gh/Jstarfish/picBed/redis/locks-505878_1280.jpg)
44

5-
要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。
5+
> 分布式锁的文章其实早就烂大街了,但有些“菜鸟”写的太浅,或者自己估计都没搞明白,没用过,看完后我更懵逼了,有些“大牛”写的吧,又太高级,只能看懂前半部分,后边就开始讲论文了,也比较懵逼,所以还得我这个中不溜的来总结下
6+
>
7+
> 文章拢共分为几个部分:
8+
>
9+
> - 什么是分布式锁
10+
> - 分布式锁的实现要求
11+
> -
612
7-
**线程锁**:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
13+
## 一、什么是分布式锁
814

9-
**进程锁**:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源, 可以使用本地系统的信号量控制 。
15+
**分布式~~**,要这么念,首先得是『分布式』,然后才是『锁』
1016

11-
**分布式锁**:分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
17+
- 分布式:这里的分布式指的是分布式系统,涉及到好多技术和理论,包括CAP 理论、分布式存储、分布式事务、分布式锁...
1218

19+
> 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。
20+
>
21+
> 分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是**利用更多的机器,处理更多的数据**
1322
23+
- 锁:对对,就是你想的那个,Javer 学的第一个锁应该就是 `synchronized`
24+
25+
> Java 初级面试问题,来拼写下 **赛克瑞纳挨日的**
26+
27+
28+
29+
**线程锁**`synchronized` 是用在方法或代码块中的,我们把它叫『线程锁』,线程锁的实现其实是靠线程之间共享内存实现的,说白了就是内存中的一个整型数,有空闲、上锁这类状态,比如 synchronized 是在对象头中的 Mark Word 有个锁状态标志,Lock 的实现类大部分都有个叫 `volatile int state` 的共享变量来做状态标志。
30+
31+
**进程锁**:比如说,我们的同一个 linux 服务器,部署了好几个 Java 项目,有可能同时访问或操作服务器上的相同数据,这就需要进程锁,一般可以用『文件锁』来达到进程互斥。
32+
33+
**分布式锁**:随着用户越来越多,我们上了好多服务器,原本有个定时给客户发邮件的任务,不加以控制的话,到点后每台机器跑一次任务,客户就会收到 N 条邮件,这就需要通过分布式锁来互斥了。
34+
35+
> 书面解释:分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
36+
37+
38+
39+
知道了什么是分布式锁,接下来就到了技术选型环节
1440

15-
分布式锁一般有三种实现方式:**1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。**
1641

1742

43+
## 二、分布式锁要怎么搞
1844

19-
TODO:乐观锁、悲观锁
45+
要实现一个分布式锁,我们一般选择集群机器都可以操作的外部系统,然后各个机器都去这个外部系统申请锁。
2046

47+
这个外部系统一般需要满足如下要求才能胜任:
2148

49+
1. 互斥:在任意时刻,只能有一个客户端能持有锁。
50+
2. 防止死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。所以锁一般要有一个过期时间。
51+
4. 解铃还须系铃人:加锁和解锁必须是同一个客户端,一把锁只能有一把钥匙,客户端自己的锁不能被别人给解开,当然也不能去开别人的锁。
52+
4. 容错:外部系统不能太“脆弱”,要保证外部系统的正常运行,客户端才可以加锁和解锁。
2253

23-
### 可靠性
2454

25-
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
2655

27-
1. 互斥性。在任意时刻,只有一个客户端能持有锁。
28-
2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
29-
3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
30-
4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
56+
这么类比恰当不:
3157

58+
好多商贩要租用某个仓库,同一时刻,只能给一个商贩租用,且只能有一把钥匙,还得有固定的“租期”,到期后要回收的,当然最重要的是仓库门不能坏了,要不锁都锁不住。
3259

3360

34-
## 基于 Redis 做分布式锁
3561

36-
setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,存在返回0。
62+
直接上结论:
3763

38-
get(key):获得key对应的value值,若不存在则返回nil。
64+
分布式锁一般有三种实现方式:**1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。**
3965

40-
getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value
66+
但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做
4167

42-
expire(key, seconds):设置key-value的有效期为seconds秒。
4368

4469

70+
第三趴,编码
4571

46-
Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。
72+
## 三、基于 Redis 的分布式锁
4773

48-
\> set lock:codehole true ex 5 nx OK ... **do** something critical ... > del lock:codehole
74+
> 其实 Redis 官网已经给出了实现:https://redis.io/topics/distlock,说各种书籍和博客用了各种手段去用 Redis 实现分布式锁,建议用 Redlock 实现,这样更规范、更安全。我们循序渐进来看
4975
50-
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的奥义所在。
76+
我们默认指定大家用的是 Redis 2.6.12 及更高的版本,就不再去讲 `setnx``expire` 这种了,直接 `set` 命令加锁
5177

5278
```
5379
set key value[expiration EX seconds|PX milliseconds] [NX|XX]
5480
```
5581

82+
eg:
83+
84+
```bash
85+
SET resource_name my_random_value NX PX 30000
86+
```
87+
88+
这条指令的意思:当 key——resource_name 不存在时创建,并设置过期时间 30000 毫秒,值为 my_random_value
89+
90+
Redis 实现分布式锁的主要步骤:
91+
92+
1. 指定一个 key 作为锁标记,存入 Redis 中,指定一个 **唯一的标识** 作为 value。
93+
2. 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 **互斥性** 特性。
94+
3. 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 **防死锁** 特性。
95+
4. 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 **解铃还须系铃人**
96+
97+
设置一个随机值的意思是在解锁时候判断 key 的值和我们存储的随机数是不是一样,一样的话,才是自己的锁,直接 `del` 解锁就行。
98+
99+
当然这个两个操作要保证原子性,所以 Redis 给出了一段 lua 脚本(Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。):
100+
101+
```lua
102+
if redis.call("get",KEYS[1]) == ARGV[1] then
103+
return redis.call("del",KEYS[1])
104+
else
105+
return 0
106+
end
107+
```
108+
109+
110+
111+
### 问题:
112+
113+
我们先抛出两个问题思考:
114+
115+
1. 获取锁时,过期时间要设置多少合适呢?
116+
117+
预估一个合适的时间,其实没那么容易,这个问题先记下,我们先看下 Javaer 要怎么在代码中用 Redis 锁。
118+
119+
2. 容错性如何保证呢?
120+
121+
Redis 挂了怎么办,你可能会说上主从、上集群,但也会出现这样的极端情况,当我们上锁后,主节点就挂了,这个时候还没来的急同步到从节点,主从切换后锁还是丢了
122+
123+
这两个问题,我们接着看
124+
125+
126+
127+
### Redisson 实现代码
128+
129+
redisson 是 Redis 官方的分布式锁组件。GitHub 地址:[https://github.com/redisson/redisson](https://zhuanlan.zhihu.com/write)
130+
131+
redisson 现在已经很强大了,github 的 wiki 也很详细,分布式锁的介绍直接戳 [Distributed locks and synchronizers](https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers)
132+
133+
Redisson 支持单点模式、主从模式、哨兵模式、集群模式,只是配置的不同,我们以单点模式来看下怎么使用,代码很简单,都已经为我们封装好了,直接拿来用就好,详细的demo,我放在了 github: starfish-learn-redisson 上,这里就不一步步来了
134+
135+
```java
136+
RLock lock = redisson.getLock("myLock");
137+
```
138+
139+
RLock 提供了各种锁方法,我们来解读下这个接口方法,
140+
141+
> 注:代码为 3.16.2 版本,可以看到继承自 JDK 的 Lock 接口,和 Reddsion 的异步锁接口 RLockAsync(这个我们先不研究)
142+
143+
#### RLock
144+
145+
```java
146+
public interface RLock extends Lock, RLockAsync {
147+
148+
/**
149+
* 获取锁的名字
150+
*/
151+
String getName();
152+
153+
/**
154+
* 这个叫终端锁操作,表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B线程可以通过
155+
* Thread.currentThread().interrupt(); 方法真正中断该线程
156+
*/
157+
void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;
158+
159+
/**
160+
* 这个应该是最常用的,尝试获取锁
161+
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
162+
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
163+
*/
164+
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
165+
166+
/**
167+
* 锁的有效期设置为 leaseTime,过期后自动失效
168+
* 如果 leaseTime 设置为 -1, 表示不主动过期
169+
*/
170+
void lock(long leaseTime, TimeUnit unit);
171+
172+
/**
173+
* Unlocks the lock independently of its state
174+
*/
175+
boolean forceUnlock();
176+
177+
/**
178+
* 检查是否被另一个线程锁住
179+
*/
180+
boolean isLocked();
181+
182+
/**
183+
* 检查当前线线程是否持有该锁
184+
*/
185+
boolean isHeldByCurrentThread();
186+
187+
/**
188+
* 这个就明了了,检查指定线程是否持有锁
189+
*/
190+
boolean isHeldByThread(long threadId);
191+
192+
/**
193+
* 返回当前线程持有锁的次数
194+
*/
195+
int getHoldCount();
196+
197+
/**
198+
* 返回锁的剩余时间
199+
* @return time in milliseconds
200+
* -2 if the lock does not exist.
201+
* -1 if the lock exists but has no associated expire.
202+
*/
203+
long remainTimeToLive();
204+
205+
}
206+
```
207+
208+
#### Demo
209+
210+
```java
211+
Config config = new Config();
212+
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("").setDatabase(1);
213+
RedissonClient redissonClient = Redisson.create(config);
214+
RLock disLock = redissonClient.getLock("mylock");
215+
boolean isLock;
216+
try {
217+
/**
218+
* 尝试获取锁的最大等待时间是 100 秒,超过这个值还没获取到,就认为获取失败
219+
* 锁的持有时间是 10 秒
220+
*/
221+
isLock = disLock.tryLock(100, 10, TimeUnit.MILLISECONDS);
222+
if (isLock) {
223+
//做自己的业务
224+
Thread.sleep(10000);
225+
}
226+
} catch (Exception e) {
227+
e.printStackTrace();
228+
} finally {
229+
disLock.unlock();
230+
}
231+
```
232+
233+
就是这么简单,Redisson 已经做好了封装,使用起来 so easy,如果使用主从、哨兵、集群这种也只是配置不同。
234+
235+
#### 原理
236+
237+
238+
239+
240+
241+
242+
243+
244+
56245

57246

58-
### 基于 redisson 做分布式锁
59247

60-
redisson 是 redis 官方的分布式锁组件。GitHub 地址:[https://github.com/redisson/redisson](https://zhuanlan.zhihu.com/write)
61248

62249
上面的这个问题 ——> 失效时间设置多长时间为好?这个问题在 redisson 的做法是:每获得一个锁时,只设置一个很短的超时时间,同时起一个线程在每次快要到超时时间时去刷新锁的超时时间。在释放锁的同时结束这个线程。
63250

@@ -74,6 +261,30 @@ RFuture<Boolean> rFuture = rLock.tryLockAsync(5,10,TimeUnit.SECONDS);rLock.unloc
74261

75262

76263

77-
## RedLock
264+
![img](https://i01piccdn.sogoucdn.com/5c535b46a06ec4d8)
265+
266+
## 四、RedLock
267+
268+
我们想象一个这样的场景当机器A申请到一把锁之后,如果Redis主宕机了,这个时候从机并没有同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis作者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。
269+
270+
271+
272+
273+
274+
## 五、Zookeeper 的分布式锁
275+
276+
277+
278+
279+
280+
281+
282+
283+
284+
285+
286+
287+
288+
##### 参考与感谢
78289

79-
我们想象一个这样的场景当机器A申请到一把锁之后,如果Redis主宕机了,这个时候从机并没有同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis作者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。
290+
- https://redis.io/topics/distlock

docs/framework/.DS_Store

0 Bytes
Binary file not shown.

docs/framework/Spring/Spring-AOP.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void add(int i, int j) {
4949

5050
代理设计模式的原理:**使用一个代理将对象包装起来**,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
5151

52-
![](/Users/apple/Desktop/screenshot/clipboard.png)
52+
![](https://cdn.jsdelivr.net/gh/Jstarfish/picBed/spring/Untitled%20Diagram-%E7%AC%AC%204%20%E9%A1%B5.svg)
5353

5454

5555

@@ -173,9 +173,7 @@ public static void main(String[] args) {
173173
- 业务模块更简洁,只包含核心业务代
174174

175175

176-
![](/Users/apple/Desktop/screenshot/clipboard1.png)
177-
178-
176+
![](https://cdn.jsdelivr.net/gh/Jstarfish/picBed/spring/aop-demo.svg)
179177

180178
### AOP 核心概念
181179

@@ -225,7 +223,7 @@ public static void main(String[] args) {
225223

226224
## 三、Spring AOP
227225

228-
- **AspectJ**Java 社区里最完整最流行的 AOP 框架
226+
- **AspectJ**Java 社区里最完整最流行的 AOP 框架
229227
- 在 Spring2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP
230228

231229

@@ -496,16 +494,12 @@ public void afterReturningAdvice(JoinPoint joinPoint, Object res){
496494

497495
- 引入通知是一种特殊的通知类型。它通过为接口提供实现类,允许对象动态地实现接口,就像对象已经在运行时扩展了实现类一样
498496

499-
![](/Users/apple/Desktop/screenshot/clipboard2.png)
500-
501-
502-
503497
- 引入通知可以使用两个实现类 MaxCalculatorImplMinCalculatorImpl,让 ArithmeticCalculatorImpl 动态地实现 MaxCalculatorMinCalculator接口。而这与从 MaxCalculatorImplMinCalculatorImpl 中实现多继承的效果相同。但却不需要修改 ArithmeticCalculatorImpl 的源代码
504498
- 引入通知也必须在切面中声明
505499
- 在切面中,通过为**任意字段**添加**@DeclareParents**注解来引入声明
506500
- 注解类型的 **value** 属性表示哪些类是当前引入通知的目标。value 属性值也可以是一个 AspectJ 类型的表达式,可以将一个接口引入到多个类中。**defaultImpl**属性中指定这个接口使用的实现类
507501

508-
> 代码可以去 starfish-learn-spring 上找
502+
> 代码在 starfish-learn-spring
509503

510504

511505

0 commit comments

Comments
 (0)