1. 为什么setnx不能直接做分布式锁?
去年双十一大促前,我们团队在压测时发现一个诡异现象:某些订单会被重复处理。排查后发现,问题出在自以为"万无一失"的Redis分布式锁实现上——我们直接用setnx命令加锁,结果在高并发下翻车了。今天就来聊聊这个坑,以及如何正确实现Redis分布式锁。
1.1 setnx的致命缺陷
setnx(SET if Not eXists)看似是天然的分布式锁方案:当key不存在时设置值并返回1,key存在时返回0。但裸用setnx存在三个致命问题:
- 死锁风险:如果加锁后客户端崩溃,没有主动释放锁,这个锁就永远无法释放
- 非原子操作:setnx和expire是两个独立命令,中间可能发生崩溃
- 误删风险:锁可能被其他客户端误删(比如A客户端处理超时,锁自动释放后被B获取,此时A又手动删锁)
// 错误示范:分两步操作存在竞态条件 redis.setnx("order_lock", "1"); redis.expire("order_lock", 30);1.2 Redis版本演进带来的解决方案
Redis 2.6.12版本后提供了更完善的原子操作:
SET key value EX seconds NX这个命令将setnx和expire合并为原子操作,解决了大部分问题。但在实际生产中,我们还需要考虑更多边界情况:
- 锁续期问题(看门狗机制)
- 可重入锁实现
- 集群环境下的可靠性
2. 分布式锁的正确实现姿势
2.1 基于Lua脚本的原子操作
对于老版本Redis,可以通过Lua脚本保证原子性:
-- KEYS[1]锁key, ARGV[1]随机值, ARGV[2]过期时间 if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end关键点:value要使用唯一标识(如UUID),避免误删其他客户端的锁
2.2 Redisson的最佳实践
生产环境推荐直接使用Redisson客户端,它内置了完善的分布式锁实现:
RLock lock = redisson.getLock("orderLock"); try { // 尝试加锁,最多等待100秒,锁自动释放时间30秒 boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS); if (res) { // 业务处理 } } finally { lock.unlock(); }Redisson实现了这些关键特性:
- 自动续期(看门狗机制)
- 可重入锁
- 公平锁支持
- 红锁(RedLock)算法
3. 高并发下的锁优化技巧
3.1 锁粒度控制
我们曾经犯过的错误:把整个订单处理流程用一个粗粒度锁保护,导致系统吞吐量骤降。正确的做法是:
- 按订单ID加锁(细粒度锁)
- 读写分离(读锁共享,写锁独占)
- 减少临界区代码
// 好的实践:按订单ID加锁 String lockKey = "order_" + orderId + "_lock";3.2 锁等待策略
直接阻塞等待锁会导致线程堆积,更好的做法是:
- 快速失败(tryLock)
- 指数退避重试
- 熔断降级
// 指数退避示例 long waitTime = 50; while (!lock.tryLock()) { Thread.sleep(waitTime); waitTime = Math.min(waitTime * 2, 1000); // 上限1秒 }4. 生产环境踩坑实录
4.1 集群脑裂问题
我们曾经在Redis主从切换时遇到锁失效问题。解决方案:
- 使用RedLock算法(多个独立Redis实例)
- 设置合理的锁超时时间
- 添加校验机制(如处理前再次检查订单状态)
4.2 GC导致的锁失效
某次Full GC导致应用暂停15秒,锁自动过期后被其他客户端获取,恢复后又继续执行业务逻辑。解决方法:
- 监控GC情况
- 设置合理的超时时间(一般业务10-30秒)
- 实现锁续期机制
4.3 锁重入问题
同一个线程多次获取锁时需要注意:
// Redisson已处理重入问题 public void methodA() { lock.lock(); try { methodB(); } finally { lock.unlock(); } } public void methodB() { lock.lock(); // 可重入 try { // ... } finally { lock.unlock(); } }5. 性能优化指标参考
经过优化后,我们的分布式锁性能指标:
| 场景 | QPS | 平均耗时 | 注意事项 |
|---|---|---|---|
| 单Redis节点 | 15000 | 2ms | 避免大key |
| Redis集群 | 8000 | 5ms | 网络开销增加 |
| RedLock(3节点) | 3000 | 15ms | 可靠性优先 |
关键发现:在保证可靠性的前提下,锁持有时间控制在100ms以内对系统性能影响最小。
6. 终极解决方案建议
经过多次迭代,我们的分布式锁最佳实践:
- 新项目直接用Redisson
- 老系统用Lua脚本实现原子操作
- 关键业务使用RedLock
- 必须添加锁监控(获取次数、等待时间、死锁检测)
最后分享一个诊断脚本,用于检查锁状态:
# 查看锁信息 redis-cli --eval lock_info.lua order_123_lock # lock_info.lua local key = KEYS[1] local val = redis.call('get', key) if not val then return {'nil', 0} else return {val, redis.call('ttl', key)} end记住:分布式系统没有银弹,任何锁方案都需要根据业务特点调整。在我们电商场景中,最终采用了Redisson+适当业务幂等的组合方案,既保证了数据一致性,又维持了系统的高可用性。