平常开发中,秒杀下单、抢红包等等业务场景,都须要用到分布式锁。而Redis很是适合做为分布式锁使用。本文将分七个方案展开,跟你们探讨Redis分布式锁的正确使用方式。若是有不正确的地方,欢迎你们指出哈,一块儿学习一块儿进步。git
公众号:捡田螺的小男孩github
什么是分布式锁redis
方案一:SETNX + EXPIRE算法
方案二:SETNX + value值是(系统时间+过时时间)安全
方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)服务器
方案四:SET的扩展命令(SET EX PX NX)markdown
方案五:SET EX PX NX + 校验惟一随机值,再释放锁网络
方案六: 开源框架:Redisson多线程
方案七:多机实现的分布式锁Redlock并发
github地址,感谢每颗star
分布式锁其实就是,控制分布式系统不一样进程共同访问共享资源的一种锁的实现。若是不一样的系统或同一个系统的不一样主机之间共享了某个临界资源,每每须要互斥来防止彼此干扰,以保证一致性。
咱们先来看下,一把靠谱的分布式锁应该有哪些特征:
提到Redis的分布式锁,不少小伙伴立刻就会想到setnx
+ expire
命令。即先用setnx
来抢锁,若是抢到以后,再用expire
给锁设置一个过时时间,防止锁忘记了释放。
SETNX 是SET IF NOT EXISTS的简写.平常命令格式是SETNX key value,若是 key不存在,则SETNX成功返回1,若是这个key已经存在了,则返回0。
假设某电商网站的某商品作秒杀活动,key能够设置为key_resource_id,value设置任意值,伪代码以下:
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
加锁,正要执行expire
设置过时时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,别的线程永远获取不到锁啦。
为了解决方案一,发生异常锁得不到释放的场景,有小伙伴认为,能够把过时时间放到setnx
的value值里面。若是加锁失败,再拿出value值校验一下便可。加锁代码以下:
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;
}
复制代码
这个方案的优势是,巧妙移除expire
单独设置过时时间的操做,把过时时间放到setnx的value值里面来。解决了方案一发生异常,锁得不到释放的问题。可是这个方案还有别的缺点:
- 过时时间是客户端本身生成的(System.currentTimeMillis()是当前系统的时间),必需要求分布式环境下,每一个客户端的时间必须同步。
- 若是锁过时的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,可是该客户端锁的过时时间,可能被别的客户端覆盖
- 该锁没有保存持有者的惟一标识,可能被别的客户端释放/解锁。
实际上,咱们还可使用Lua脚原本保证原子性(包含setnx和expire两条指令),lua脚本以下:
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);
复制代码
这个方案仍是有缺点的哦,至于哪些缺点,你先思考一下。也能够想下。跟方案二对比,哪一个更好?
除了使用,使用Lua脚本,保证SETNX + EXPIRE
两条指令的原子性,咱们还能够巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]
),它也是原子性的!
SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能得到锁,而其余客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过时时间,时间单位是秒。
- PX milliseconds: 设定key的过时时间,单位为毫秒
- XX: 仅当key存在时设置值
伪代码demo以下:
if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
复制代码
可是呢,这个方案仍是可能存在问题:
既然锁可能被别的线程误删,那咱们给value值设置一个标记当前线程惟一的随机数,在删除的时候,校验一下,不就OK了嘛。伪代码以下:
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); //释放锁
}
}
}
复制代码
在这里,判断是否是当前线程加的锁和释放锁不是一个原子操做。若是调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
为了更严谨,通常也是用lua脚本代替。lua脚本以下:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
复制代码
方案五仍是可能存在锁过时释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过时时间设置长一些就能够啦。其实咱们设想一下,是否能够给得到锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过时时间延长,防止锁过时提早释放。
当前开源框架Redisson解决了这个问题。咱们一块儿来看下Redisson底层原理图吧:
只要线程一加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,若是线程1还持有锁,那么就会不断的延长锁key的生存时间。所以,Redisson就是使用Redisson解决了锁过时释放,业务没执行完问题。
前面六种方案都只是基于单机版的讨论,还不是很完美。其实Redis通常都是集群部署的:
若是线程一在Redis的master节点上拿到了锁,可是加锁的key还没同步到slave节点。刚好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就能够获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis做者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:
搞多个Redis master部署,以保证它们不会同时宕掉。而且这些master节点是彻底相互独立的,相互之间不存在数据同步。同时,须要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。
咱们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。
RedLock的实现步骤:以下
- 1.获取当前时间,以毫秒为单位。
- 2.按顺序向5个master节点请求加锁。客户端设置网络链接和响应超时时间,而且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间通常在5-50毫秒之间,咱们就假设超时时间是50ms吧)。若是超时,跳过该master节点,尽快去尝试下一个master节点。
- 3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),获得获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都得到锁,而且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
- 若是取到了锁,key的真正有效时间就变啦,须要减去获取锁所使用的时间。
- 若是获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在全部的master节点上解锁(即使有些master节点根本就没有加锁成功,也须要解锁,以防止有些漏网之鱼)。
简化下步骤就是:
Redisson实现了redLock版本的锁,有兴趣的小伙伴,能够去了解一下哈~