分布式锁的几种实现方式

分布式锁的几种实现方式

DB

并发下的问题:

​ 丢失更新:一个事务的更新覆盖了其它事务的更新结果。A把值从6改为2,B把值从6改为3。A的更新结果丢失。

​ 脏读:一个事务读取其它完成一半事务的记录时,就会发生脏读取。A、B读到值都为6,B把值改为2,A认为的值仍为6。

  • 悲观锁

    1
    select * from table where id = 1 for update;//行级锁 要有明确的主键
    1
    select * from table for update;//表级锁

    注:此时只有执行完了for update操作,即该事务commit后,其他select for update、update操作才会执行,但单纯的select操作完全可以执行。

  • 乐观锁

    使用版本号或者时间戳。

    1
    2
    select x1,x2 ... ,version from table where x1 = 1;
    update table set version = version + 1 where x1 = 1 and version = ?

    乐观锁不能解决脏读的问题。

    注:效率低下,只能根据业务场景适量选择。

    ​ 需要客户端不断重试获取锁。

Redis

​ 4个命令:setnx get getset del

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
long exsits = setNX(lock.key,System.curtime + timeout);
//获得锁
if(exsits == 1){
//todo business
...
...
//防止死锁,释放锁
if( doBusinessTime < timeout){
del(lock.key);
}
}else{//exsits == 0
long oldExpireTime = get(lock.key);
//锁过期
if(oldExpireTime < System.curtime){
long _oldExpireTime = getset(lock.key, System.curtime + timeout);
//锁没有被其他线程(进程)获取
if(oldExpireTime == _oldExpireTime){
//获得锁
//todo business
...
...
//防止死锁,释放锁
if( doBusinessTime < timeout){
del(lock.key);
}else{
//重试或者阻塞
}
}else{
//重试或者阻塞
}
}
}

​ 注:timeout 的值应根据业务来设,因为当业务逻辑执行时间过长,比timeout长时,依然存在并发问题;

​ 这几个命令在redis集群下情况将很复杂,将会有问题;

​ 需要客户端不断重试获取锁。

ZooKeeper

​ 求最小节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void lock(){
path = 在父节点下创建临时顺序节点
while(true){
children = 获取父节点的所有节点
if(path是children中的最小的){
//获取锁
return;
}else{
添加监控前一个节点是否存在的watcher
wait();
}
}
}
watcher中的内容{
notifyAll();
}
//释放锁
删除结点

注:可靠性最好,实现最简单的方式。但是需要额外维护成本,依业务场景选择;

​ 通过服务器端通知客户端锁被释放。