简介
在微服务场景下,为了防止多个进程及线程并发访问共享资源,如支付、下单等操作,会引入分布式锁来保证业务的并发安全。
Redis实现分布式的要求
1、互斥性;
防止多个进程及线程并发访问共享资源,使得资源串行访问操作。
2、设置锁过期时间;
为了防止锁悬挂,因为服务宕机,锁不释放问题,其它请求就无法获取锁。
3、自动续锁超时时间;
防止业务超时,超过锁过期时间自动释放,打破互斥性。
4、多条指令需要原子性;
lua脚本实现多个指令的加锁、解锁及续锁的原子性。
5、可重入性;
使用线程ID信息来保证同一线程请求锁的可重入性。
6、锁误删:自己把别人持有的锁删了;
多个客户端释放锁,如何防止自己删别人的或者别人删自己申请的锁。
获取锁之前,生成全局唯一id,判断是否是自己的id来避免。
7、锁等待:发布订阅机制通知等待锁的线程;
Redisson看门狗续锁实现分布式锁
以RedissonLock为例来分析
org.redisson.RedissonLock#tryLock()
org.redisson.RedissonLock#unlock
的实现。
tryLock方法调用分析:
当锁超时时间为-1时,而且获取锁成功时,会启动看门狗定时任务自动续锁:
每次续锁都要判断锁是否已经被释放,如果锁续期成功,自己再次调度自己,持续续锁操作。
为了保证原子性,用lua实现的原子性加锁操作:
lua加锁流程:
获取锁或锁重入,lua返回nil即Java的NULL值,如果获取锁失败,则返回锁的ttl时间。
根据返回值编码的设置:
RedisStrictCommand<Boolean> EVAL_NULL_BOOLEAN = new RedisStrictCommand<Boolean>("EVAL", new BooleanNullReplayConvertor());
返回NULL代码获取锁成功,后续开启续锁流程,否则返回false,表示获取锁失败。
unlock方法调用分析:
为了保证原子性,用lua实现的原子性释放锁操作:
释放锁流程:
发布订阅机制是为了通知调用org.redisson.RedissonLock#lock()方法等待锁的线程。
以上我们注意到,线程id信息携带了一个UUID随机数,是为了防止集群环境下,线程ID相同导致误删问题。
为了保证原子性,用lua实现的原子性续锁操作:
单实例锁VS集群锁
上面介绍的锁实现只能使用于单Redis实例,不支持Redis集群。 并且如果锁所在的Redis实例挂掉了之后,采用哨兵模式进行主备切换。但由于Redis的主从复制(replication)是异步的,这可能会出现在数据同步过程中,master宕机,slave来不及同步数据就被选为master,从而数据丢失。
Redis的作者提供了红锁来实现集群锁,算法思想:https://redis.io/docs/reference/patterns/distributed-locks/;
Redisson看门狗续锁实现分布式锁-避坑
1、不要传递自定义锁超时时间,否则不会续锁;
2、加锁和释放锁要在同一个线程,否则影响可重入性逻辑判断,导致续锁、释放锁失败;
3、单实例宕机,主从切换问题导致锁丢失;
做了主从,或者使用了哨兵模式,基于redis的分布式锁的功能,就会出现问题。
如何避免:
1、使用红锁解决,当然红锁的实现也有自己的问题;
2、使用锁的Redis实例单独分配且业务隔离,尽量保证不宕机,监控此实例,及时响应报警处理;
3、业务实现幂等来兜底;
小结
Redis学习资料:
redis: https://url97.ctfile.com/d/36436597-51573286-ccffc3?p=1988 (访问密码:1988)