一、分布式锁到底是个啥玩意儿?
想象一下你和小伙伴们一起抢购限量版球鞋的场景。如果所有人都同时点击购买按钮,系统很可能会乱套。这时候就需要一个"排队管理员"来协调,而这个管理员在分布式系统中就是分布式锁。
分布式锁本质上是一个跨进程、跨机器的互斥机制,用来保证在分布式环境下同一时间只有一个客户端能对共享资源进行操作。就像厕所门上的"有人/无人"标识牌,只不过这个标识牌要能被所有楼层的用户看到。
二、Redis实现分布式锁的花式操作
Redis实现分布式锁主要有三种经典姿势:
2.1 SETNX + EXPIRE 基础版
// 技术栈:Java + Jedis
public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
// SETNX:如果key不存在则设置,返回1;key已存在则不做操作,返回0
Long result = jedis.setnx(lockKey, requestId);
if (result == 1L) {
// 设置过期时间防止死锁
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
这种实现简单直接,但有个致命问题:SETNX和EXPIRE不是原子操作!如果在设置过期时间前程序崩溃,锁就永远无法释放了。
2.2 SET命令进阶版
// 技术栈:Java + Lettuce
public boolean tryLock(RedisCommands<String, String> commands,
String lockKey,
String requestId,
int expireTime) {
// 使用SET命令的NX和EX选项实现原子操作
String result = commands.set(lockKey, requestId,
SetArgs.Builder.nx().ex(expireTime));
return "OK".equals(result);
}
这个版本解决了原子性问题,但还有个隐患:如果业务执行时间超过锁的过期时间,其他客户端就可能获取到锁,导致多个客户端同时持有锁。
2.3 Redlock算法豪华版
Redlock是Redis官方推荐的分布式锁算法,需要至少5个独立的Redis主节点:
// 技术栈:Java + Redisson
Config config = new Config();
config.useRedissonSentinel()
.setMasterName("mymaster")
.addSentinelAddress("redis://127.0.0.1:26379")
.addSentinelAddress("redis://127.0.0.1:26380");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
try {
// 尝试获取锁,最多等待100秒,锁自动释放时间30秒
boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
// 执行业务逻辑
}
} finally {
lock.unlock();
}
Redisson已经帮我们封装好了所有复杂逻辑,包括锁续期、等待机制等,是生产环境的首选方案。
三、Zookeeper实现分布式锁的优雅姿势
Zookeeper通过其临时顺序节点的特性,提供了另一种实现分布式锁的思路。
3.1 基础实现方案
// 技术栈:Java + Curator
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");
try {
// 获取锁,最多等待60秒
if (lock.acquire(60, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
lock.release();
}
Curator是Netflix开源的Zookeeper客户端,它提供的InterProcessMutex已经实现了完整的分布式锁逻辑。
3.2 实现原理剖析
Zookeeper实现分布式锁的核心流程:
- 所有客户端在/locks节点下创建临时顺序节点
- 获取/locks下所有子节点,判断自己是否是最小节点
- 如果是,获取锁;如果不是,监听前一个节点的删除事件
- 获得锁的客户端完成操作后删除自己的节点
- 其他客户端收到通知后重复步骤2
这种实现天然具备以下优势:
- 锁释放及时:客户端断开连接时临时节点自动删除
- 避免惊群效应:每个客户端只监听前一个节点
- 公平锁:按照申请顺序依次获得锁
四、Redis和Zookeeper的华山论剑
4.1 性能对比
Redis完胜!Redis基于内存操作,吞吐量可以达到10万+/秒,而Zookeeper因为要写磁盘,性能通常在1万+/秒左右。
4.2 可靠性对比
Zookeeper更可靠!Redis在故障转移时可能出现锁失效问题,而Zookeeper的ZAB协议保证了强一致性。
4.3 适用场景
- Redis适合:对性能要求高、可以容忍偶尔锁失效的场景
- Zookeeper适合:对可靠性要求极高、性能可以妥协的场景
4.4 注意事项
使用Redis分布式锁时:
- 一定要设置合理的过期时间
- 建议使用Redisson等成熟框架
- 考虑锁续期机制
使用Zookeeper分布式锁时:
- 注意Zookeeper集群的部署和调优
- 处理好session过期问题
- 监控节点数量避免雪崩
五、如何选择适合自己的方案?
选择困难症?看看这个决策树:
- 你的系统对性能极其敏感吗?
- 是 → 选择Redis
- 否 → 进入问题2
- 你的系统绝对不能接受锁失效吗?
- 是 → 选择Zookeeper
- 否 → 进入问题3
- 你的团队更熟悉哪个技术?
- Redis → 选择Redis
- Zookeeper → 选择Zookeeper
其实现在很多大型系统会同时使用两种方案:用Redis处理高频、低关键度的锁,用Zookeeper处理低频、高关键度的锁。
六、实战中的那些坑
6.1 Redis锁的续期问题
// 技术栈:Java + Redisson
// 自动续期示例
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒过期,看门狗每10秒续期一次
// 手动续期示例
lock.lock(10, TimeUnit.SECONDS); // 10秒后自动过期,无续期
6.2 Zookeeper的羊群效应
虽然Zookeeper通过顺序监听避免了惊群效应,但如果锁竞争激烈,大量客户端同时创建节点仍然会给Zookeeper带来压力。解决方案是增加重试间隔的随机性。
6.3 网络分区问题
Redis在发生网络分区时可能出现多个客户端同时持有锁的情况。而Zookeeper在发生网络分区时,少数派的客户端将无法获得锁,保证了安全性。
七、总结与建议
经过这一番折腾,我们可以得出几个结论:
- 没有完美的方案,只有适合的方案
- Redis性能好但可靠性稍差,Zookeeper可靠性高但性能稍差
- 生产环境建议使用成熟框架而不是自己造轮子
- 一定要根据业务特点选择合适的方案
最后送大家一句话:分布式锁虽好,但不要滥用哦!很多场景其实可以通过乐观锁、CAS等更轻量的方案解决。
评论