多个线程执行事务时,我们可以设定一个统一的资源,只有拿到这个才有资格继续执行,这个资源就是锁。单体架构下,锁存在共享内存中,这样所有线程都可以访问到。分布式环境下,多机器之间没有共享内存,如何实现分布式锁?
分布式锁的首要目标就是找到所有机器都认可的资源,让不同机器上的线程都有机会争抢。
常见的方式利用MySQL数据库、Redis、ZooKeeper
分布式应当具备以下条件
- 异常或者超时自动删除,避免死锁
- 互斥性,只有一个客户端能够持有锁
- 分布式环境下高性能、高可用、容错机制
这三点在redis官网讲述分布式锁也有类似表达
redis.cn Redis分布式锁
按照我们的思路和设计方案,算法只需具备3个特性就可以实现一个最低保障的分布式锁。
- 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
- 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
- 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.
Redis实现分布式锁
主要利用的是redis的几个命令
setnx
set if not exist
set key value
原子性操作,当且仅当 key 不存在,将 key 的值设为 value ,并返回1;
若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0
expire
expire key time
设置过期时间
get
查询key
set
set key value
修改key值为value,如果key存在,则将key对应的vlaue值修改为value放回ok;如果不存在,则添加key,值为value
del
del key [key...]
删除key,支持一次删除多个key。返回删除成功的个数。
例1,age存在,则del age会删除age并返回1;
例2,name存在,age不存在,贼del age放回0;del name age返回1
基于Redis的简单分布式锁
- lock
setnx操作,上锁;如果返回1,表示上锁成功;否则,失败;
- expire
设置超时时间,防止过长时间持有锁,执行事务过程中宕机等问题导致死锁
- 执行操作
- unlock
del key,删除key,释放锁
if(setnx(key,1) == 1){
// 1
expire(key,30
try {
do something ......
}catch(){
}
finally {
// 2
del(key)
}
}
但是此方案有重大隐患:
- 由于setnx 和 expire两个操作不具备原子性,极端情况下,setnx执行了,在执行expire前宕机(代码1处),将产生死锁
解决方式:
redis2.6后,改进了set命令
set key value [EX seconds][PX milliseconds][NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回oK,失败返回(ni1) 如果key存在,参数加nx,将失败
XX:key存在时设置va1ue,成功返回oK,失败返回(ni1)
127.0.0.1:6379> set num 3 EX 100 nx // 如果num不存在,就新增值并设置值3,100s后过期
OK
127.0.0.1:6379> ttl num // 查看过期时间
(integer) 83
127.0.0.1:6379> get num
"3"
127.0.0.1:6379> set num 4 nx // 如果num不存在,就设置num值为4
(nil) // 失败
127.0.0.1:6379> set num 4 xx 如果num存在,就设置num值为4
OK // 成功
127.0.0.1:6379> get num
"4"
127.0.0.1:6379> ttl num // 由于16行的语句,没有设置过期时间,num,就一直有效
(integer) -1
过期时间与是否正确解锁
- 由于设置了过期时间,可能出现,线程A执行还没结束,锁就自动过期了,之后线程B拿到锁,接着线程A执行结束释放锁,此时释放的就是线程B的锁
解决方式:
取线程的唯一标识与锁绑定,解锁时需要解锁线程与锁对应才可解锁
加锁时将key对应的value值设为线程id,释放锁时判断此id是否为当前线程id
加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
doSomething.....
解锁:
if(threadId .equals(redisClient.get(key))){
del(key)
}
// 解锁时,判断和删除也不是原子操作,可以引入lua脚本,将两个操作放入一个lua脚本中,实现原子性
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
问:如果储存锁的redis实例挂掉了怎么办?
使用多台机器存储锁数据,采用RedLock(红锁)算法。
注意,这里说的多台机器不是指集群,这些机器彼此毫无关联,使用「大多数」机制,超过半数的机器确认获取到锁,才是真正的获取到锁。
设置过期时间的Redis分布式锁
Redisson实现的分布式锁
github.com/redisson/redisson/wiki/8.分布式锁和同步器
redis 官方关于分布式锁的讲解(redlock)