基于Redis的锁一般分单节点或集群多节点二中情况,下面分别介绍一下。
Redis单节点(客户端:Jedis)
一、正确的加锁方式
jedis.set(String key, String value, String nxxx, String expx, int time)
第一个参数为:key
第二个参数为:值(一般为一个随机数,这也是有讲究的,知道为什么?)
第三个参数为:设置为NX,代表key不存在操作,存在的情况下不处理返回,从而确保只有一个客户端可以获得锁;
第四个参数为:设置为PX 用于标识指定超时时间
第五个参数为:超时时间,单位:毫秒(为什么要指定超时时间?......要是我挂了,不能执行删除命名,那是不是锁就不释放了!!)
注意:这里为什么不用setNx呢?jedis的setnx不能把设置值和指定过期时间放在一起执行,即非原子性,有风险呀。
二、释放锁
String lua_script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(lua_script, Collections.singletonList(key), Collections.singletonList(获取锁时生成的随机数));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
1、Redis支持直接运行Lua脚本语言,更多lua见 https://www.runoob.com/lua/lua-tutorial.html
2、为什么不通过get--del等命令来处理呢?---> 因为Lua可以确保原子性,这个命令在一个事务端中
既然是单节点,那对可用性就做不到保障了,集群在这时就诞生了,那对于在集群环境下分布式锁要注意什么呢?
Redis官方推出了RedLock,我们对它认识一下
RedLock是通过同一个key和value,在种master节点上申请锁,当在超过一半的节点上获取时,即代表该进程成功获取到了锁。
大致实现如下(假设这里有5个redis master,锁自动释放时间(TTL)为10秒):
1、开始获取锁量首先获取当前时间戳;
2、客户端进程用相同的key,value去所有的redis master 服务中获取锁,获取锁会设置一个超时时间(超时时间远小于【锁自动释放时间】,防止master节点阻塞过长时间引起系统性风险),当超过获取时间时,直接去下一个节点获取锁;
3、客户端获得所有可获得的锁的时间减去第一步记录的时间戳,小于TTL且至少在3个master上成功获取锁,才是真正的锁成功;
4、若获取锁成功,则锁的真正有效时间为TTL减去(之前获取锁总消耗时间+系统时间波动);
5、如果客户端获取锁失败,在所有的结点上进行锁释放,否则会影响其它客户端获取锁。释放锁时需判断这个锁的value是不是自己设置的,如果是才删除。
RedLock性能及崩溃恢复的相关解决方法
1.如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,另一客户端能够再次获取到锁,这样违法了锁的排他互斥性;
2.如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒-次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;
3.有效解决既保证锁完全有效性及性能高效及即使断电情况的方法是redis同步到磁盘方式保持默认的每秒,在redis无论因为什么原因停掉后要等待TTL时间后再重启(学名:延迟重启) ;缺点是 在TTL时间内服务相当于暂停状态;