聊聊分布式锁的正确打开方式------从单点Redis到RedLock
说起来,分布式锁这个话题,我当年第一次遇到的时候还挺懵的。那时候觉得,不就是加个锁嘛,单机环境下synchronized或者ReentrantLock搞定的事,放到分布式环境能有多复杂?
结果一实践才发现,想简单了。
先说说什么是分布式锁
单机器上线程同步的问题,我们可以用JVM内置的锁来解决。但如果服务部署在多台机器上,每台机器的JVM都是独立的,锁只能管自己那一亩三分地。这时候怎么办?
答案是分布式锁------把锁放到一个公共的地方,让所有机器都来这儿抢。
Redis因为读写性能高、语义简洁顺理成章成了最常用的分布式锁载体。用SET key value NX PX timeout这条命令,加锁成功了就是成功了,失败了就是失败了,简单粗暴有效。
单点Redis锁的问题
但是问题来了------Redis挂掉怎么办?
如果只有一台Redis,所有请求都冲着它去。一旦这台Redis因为各种原因(机器故障、Redis进程崩溃、网络抖动导致的主从切换失败......)不可用了,你的分布式锁就直接失效了。业务还在跑,但锁已经没了,各种并发问题随之而来。
这种情况,生产环境真的能忍吗?反正我忍不了。
于是,RedLock横空出世。
RedLock是怎么玩的
RedLock是Redis作者Salvatore Sanfilippo(也就是Antirez)提出的算法,核心思想一句话就能说清楚:别把鸡蛋放在一个篮子里,搞多个独立的Redis节点,锁要在大多数据节点上加成功才算数。
通常建议用奇数个节点,比如5个。加锁的时候,客户端会同时向这5个节点发起加锁请求。如果有超过一半的节点(即至少3个)加锁成功,且整个过程耗时没超过锁的过期时间的一半,那才认为加锁成功。
这么做的好处是:即使其中2个节点挂了,剩下3个还在,锁服务依然正常。这就是RedLock的容错性。
来点实际的------Java怎么用
说到实现,Java里用Redisson框架是最省事的。我当年第一次跑通RedLock的时候还挺激动的,终于不用自己写那些乱七八糟的分布式锁逻辑了。
java
public class RedLockDemo {
public static void main(String[] args) {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:6379",
"redis://127.0.0.1:6380",
"redis://127.0.0.1:6381");
RedissonClient redissonClient = Redisson.create(config);
RedissonRedLock redLock = redissonClient.getRedLock("resource");
try {
boolean lockAcquired = redLock.tryLock(5, 5000, TimeUnit.MILLISECONDS);
if (lockAcquired) {
// 抢到锁了,干活
} else {
System.out.println("没抢到,下次再来吧");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (redLock.isLocked()) {
redLock.unlock();
}
redissonClient.shutdown();
}
}
}
代码看着挺简洁的,但背后的实现还是值得唠唠。
RedLock背后的实现原理
如果你去看Redisson的源码,会发现RedissonRedLock其实是继承自RedissonMultiLock的。MultiLock就是联锁,可以同时操作多个锁,统一管理。
java
public class RedissonRedLock extends RedissonMultiLock {
@Override
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
@Override
protected int minLocksAmount(final List<RLock> locks) {
return locks.size() / 2 + 1;
}
@Override
protected long calcLockWaitTime(long remainTime) {
return Math.max(remainTime / locks.size(), 1);
}
}
说白了,RedLock就是让多个锁绑定在一起工作。加锁的时候要全部加锁成功才算数,解锁的时候也是一次性全解开。这种"要么全成功要么全失败"的语义,保证了多个锁之间的一致性。
RedLock也有它的烦恼
听起来挺美的,但RedLock也不是银弹,实际使用中有两个比较明显的坑:
1. 性能问题
每次加锁都要等待多个节点响应才行。以前只要跟一个Redis通信,现在要跟5个通信。这个等待时间,在高并发场景下还是挺肉疼的。
2. 并发安全问题------GC停顿
这个问题比较隐蔽,说的是这么个场景:
- 客户端A向3个节点发起加锁请求
- 节点还没来得及回复,客户端A的GC来了(Stop-The-World,全程停顿)
- 因为加锁耗时超过了过期时间,锁自动释放了
- 客户端B进来,成功加锁
- 客户端A的GC结束,收到之前的响应,误以为自己加锁成功了
好家伙,这时候客户端A和客户端B同时认为自己持有同一把锁,并发问题就这么来了。
关于这个问题,业内讨论了很久,也没有一个完美的解决方案。
现在的RedLock已经被废弃了
因为上面说的这些问题争议比较大,Redisson官方已经废弃了RedLock。官方文档里明确说了不推荐使用。
那该怎么选
如果你的业务需要分布式锁同时又要避免单点故障,其实有更稳妥的方案:
- ZooKeeper / etcd:用他们天然的分布式锁能力,一致性有保障
- Consul:同样支持分布式锁
- Redisson的联锁模式:虽然RedLock废弃了,但MultiLock本身还在,只是需要自己保证节点的高可用
关键是要结合自己的业务场景和技术栈来选型,不要盲目追新。
总结
RedLock作为分布式锁的一种探索,在特定历史时期解决了实际问题。但随着时间推移,大家发现它并不完美。技术选型这事儿,从来都是权衡利弊,没有最好的,只有最适合的。
如果大家在实际项目中用过RedLock或者其他分布式锁方案,欢迎留言交流踩过的坑。