你的分布式锁安全吗?
首先你必须弄清楚,用分布式锁来做什么?是解决效率问题,还是一致性问题;不能混为一谈,跟个追星族一样,哪个工具火就用哪个,比如Redis很火,什么都用它,锁就想当然的用Redlock;如果是效率问题,那可能还好一些;但是如果是一致性问题,那你得认真对待,你目前的实现方案可能会某一天让你痛苦不堪!
你为什么要用分布式锁
效率
比如分布式多节点,虽然业务有幂等性保护,但是不想让一次任务被调度多次(打印太多错误日志,数据库主键约束,耗费资源)保证准确完整
为保证数据状态的准确一致,如果多个节点同时被调度,会造成数据损坏或丢失,或者系统运行状态不可控
这两种情况,一定要区别对待!
如果是后者,你用的方案不是强一致,那不是自欺欺人吗?比如很流行的Redlock本身并非强一致性,却要解决一致性问题,参见Jepsen test Redis, How to do distributed locking
如果是前者,出于效率优化,却大费周折搞了个大规模集群,增加问题的复杂度,或多或少有点大炮打蚊子
问题分析
一定要谨慎谨慎,再谨慎!不能github上找一个就用!比如下面这个用Redis实现的,他可没告诉你不适于强一致性要求;你发现问题了吗?
public synchronized void lock() { |
我敢说90%多都是这么实现的,setnx => get => getSet
, 官方文档也是这么给的http://redis.io/commands/setnx
System.currentTimeMillis
比较,通过和系统本地时间比较判定超时时间,有待商榷;尤其是大规模集群,时间准同步基本不可能,或多或少都有偏差,网络问题造成delay很难避免synchronized
多个jvm还有用吗?
考虑这个场景:
- A,B节点同时判定锁超时
- A节点
getSet
成功,成功获取到锁返回 - B节点
getSet
, 比较oldValueStr.equals(currentValueStr)
,获取锁失败,进入下一轮回 - B节点时钟跳前,执行
getSet
,比较oldValueStr.equals(currentValueStr)
成功,成功获取到锁返回
这样是不是A,B节点就同时获取到锁了?虽说概率极低,但是还是有这种可能
继续看unlock方法
public synchronized void unlock() { |
我们知道为了避免死锁,都会设置锁超时时间;这是对的,但是这个unlock实现是有问题的
- A节点成功获取锁
- A节点full gc造成锁超时
- B节点成功获取到锁
- A节点full gc后,任务执行完,调用unlock
- C节点成功获取到锁,此时B,C节点同时占有锁
虽说超时问题确实很难解决,因为很难断定A节点是真的宕机还是处理很慢,但是问题在于A节点删除了别人的锁
解决方法
该解决方法仅限于解决效率问题,不适用于强一致性要求,同时超时情况下有可能多节点同时持有锁;但是会很好的避免上述状况
不要用System.currentTimeMillis
用http://redis.io/commands/set, 把【超时时间,key, token
】原子塞进Redis,把超时判断逻辑交给Redis(其实Redis也是用系统时间判断的https://github.com/antirez/redis/blob/edd4d555df57dc84265fdfb4ef59a4678832f6da/src/server.c#L390-L404)
public synchronized boolean tryLock() { |
unlock加入token校验
利用lua脚本,保证原子性
private static final String DELETE_IF_OWNED = |
最后
分布式锁服务实现确实很难,但是至少要先明确用来做什么,效率 or 准确性;这样才能选型,若准确性需求,那选择的工具最起码要天生支持一致性(Paxos,Raft,数据库事务机制), 然后再考虑超时等其他问题;
超时问题是个通病,前面也说到了,目前还没发现很好的解决方法,很多异常检测算法也是概率性的;所以如果需求必须保证准确性,基于数据库事务机制设计会稳妥一些,不可避免侵入业务逻辑,合理权衡