参考:https://blog.csdn.net/fengyuyeguirenenen/article/details/123752418
redis分布式锁由来:
单机版的服务使用synchronize和lock是没问题的
如果一上集群,每个服务各自加锁的对象是不一样的,这时我们加的锁就不在各自的服务里,需要借助外力,redis进场。
秒杀下单啥的,抢红包都得用分布式锁
####方案一:SETNX+EXPIRE实现
缺点:将setnx和expire两个命令分开了,不是原子操作。
如果执行完setnx加锁之后,expire之前,进程崩了,那么这个锁就一直存在了,别的进程会永远获取不到锁
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
expire(key_resource_id,100); //设置过期时间
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
为了解决上面的setnx后,expire前的锁释放不了问题,可以利用上面的setnx时候的value值
将value值赋值为当前系统时间+过期时间
当产生了上面的锁永久存在情况后,后面的进程获取不到锁,这时就在上面的setnx步骤后面加个判断,获取当前锁的内容get lock,然后手动判断这个锁是否已经过期(即校验value).
- 下面的代码还考虑了多线程同时进行新的赋值操作,最后还进行了一步对比,可以好好理解一下
- 仅当前线程设置的时间与当前redis获取到的时间完全相同,才能保证这段时间里,没有其他线程影响该锁
缺点:
1.过期时间是由客户端自己生成的,在分布式的情况下,每个客户端的时间必须同步
2.如果锁过期的情况下,并发的多个客户端同事请求过来,都执行getSet方法,最后只能有一个客户端加锁成功(这一点代码其实也特意处理过了)
3.该锁没有保存持有者的唯一标识,可能会被别的客户端释放
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
这个就是方案一的一摸一样,只不过用了脚本,是两个变成了原子操作
Lua是redis2.6版本最大的亮点,解决了redis之前只能分别操作的缺点,原子化多个操作
原谅我确实看不懂。。。。
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end;
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
" redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);
这个也相当于完善了方案一,用set的拓展命令完成了原子性,因为这个set的命令也是能满足原子性的。
SET key value [EX seconds] [PX milliseconds] [NX |XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值
缺点:
- 锁过期了,但是业务还没有执行完(假设现在是线程a获取到了锁,锁的过期时间是100,但是100s后线程a的任务还没有完成,这时线程b来获取这个锁,就能获取到了。。。)
- 锁被别的线程误删(假设线程a执行完后,去释放锁,但是这里会产生一种情况,这个锁的过期时间是50s,线程a需要100s才完成,所以在50s后其实就是线程b拥有了这个锁,然后到了100s的时候,线程a不知道锁已经过期了,进行释放锁的操作,delete了key,就相当于释放了线程b的锁)
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
为了完善上一条的锁可能会被其他线程删除(上一条的缺点2),我们给value设置一个标记当前线程的唯一随机数,删除的时候进行校验呗
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}
但是上面的finally里面的判断操作和删除操作也不是原子操作,所以还得用lua脚本让他变成原子操作。
上一个的操作还会有【锁过期释放,业务没有执行完】的问题(方案4的缺点一)
我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每个一点时间检查锁是否还在,存在就对锁的过期时间延长,防止锁过期提前释放
核心就是只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,每隔10s检查一下,如果线程1还持有锁,就对锁的过期时间延长,防止锁过期提前释放。
前面六种都是基于单机版的讨论,但是redis一般都是集群部署
所以当面对集群部署的时候,假如现在主节点有锁后,这时还没有往slave节点同步锁前,主节点挂了,这里重新选举了新master,丢失了锁,这样其他线程就能获取到该锁,显然是不行的