1. 为什么需要分布式锁
在单机环境下,我们使用Java的synchronized关键字或者ReentrantLock就能很好地解决多线程并发问题。但是当系统发展到分布式架构时,这些单机锁机制就完全失效了。想象一下,你的订单服务部署在3台服务器上,这时候synchronized只能保证单台服务器上的线程安全,完全无法控制其他服务器上的并发操作。
分布式锁要解决的核心问题就是在分布式系统中,多个服务实例对共享资源进行互斥访问。比如:
- 电商系统中的库存扣减
- 秒杀系统中的商品抢购
- 支付系统中的重复支付防止
注意:分布式锁不是银弹,它引入了额外的复杂度,只有在真正需要跨JVM互斥的场景下才应该使用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis实现分布式锁的核心原理
2.1 最基础的SETNX实现
Redis的SETNX(SET if Not eXists)命令是实现分布式锁的最基础方式:
bash复制SETNX lock_key unique_value
当key不存在时设置成功返回1,否则返回0。这个操作是原子性的,完美符合互斥锁的需求。
但这样实现有几个致命缺陷:
- 如果客户端获取锁后崩溃,锁永远不会释放(死锁)
- 没有锁续期机制,业务执行超时会导致锁提前释放
- 非阻塞式获取,需要客户端自旋尝试
2.2 带过期时间的改进版
为了解决死锁问题,我们需要给锁加上过期时间:
bash复制SET lock_key unique_value NX PX 30000
这个命令在设置锁的同时设置了30秒的过期时间,即使客户端崩溃,锁也会自动释放。但这样又引入了新的问题:
- 如果业务执行时间超过30秒,锁会自动释放,其他客户端可以获取锁
- 如果此时原客户端继续操作,可能会破坏数据一致性
2.3 Redlock算法
Redis作者Antirez提出了Redlock算法来解决分布式环境下的锁可靠性问题。核心思想是:
- 获取当前时间(毫秒)
- 依次向N个独立的Redis节点请求锁
- 计算获取锁花费的时间(当前时间减去步骤1的时间)
- 只有在大多数节点(N/2+1)上获取成功,并且总耗时小于锁过期时间,才算获取成功
- 锁的实际有效时间 = 初始有效时间 - 获取锁花费的时间
提示:生产环境建议至少5个Redis主节点,且部署在不同的物理机上。
3. 生产级Redis分布式锁实现
3.1 基于Redisson的实现
Redisson是Redis官方推荐的Java客户端,提供了完善的分布式锁实现:
java复制// 获取锁
RLock lock = redisson.getLock("myLock");
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 业务逻辑
}
} finally {
lock.unlock();
}
Redisson解决了以下几个关键问题:
- 锁续期:通过看门狗机制自动延长锁持有时间
- 可重入:同一个线程可以多次获取同一把锁
- 锁竞争:提供了公平锁和非公平锁的实现
3.2 Lua脚本保证原子性
解锁操作需要先检查锁的value是否匹配,再删除key。这个操作必须原子性执行:
lua复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
3.3 锁等待队列优化
在高并发场景下,大量客户端重试获取锁会给Redis带来压力。Redisson实现了基于pub/sub的锁等待机制:
- 获取锁失败的客户端订阅锁的channel
- 锁释放时会发布消息通知等待的客户端
- 客户端收到通知后再次尝试获取锁
这种方式比简单的轮询更高效。
4. Redis分布式锁的常见问题与解决方案
4.1 时钟漂移问题
在Redlock算法中,如果Redis节点之间的时钟不同步,可能导致锁过早失效。解决方案:
- 使用NTP服务保持时间同步
- 适当增大锁的有效时间作为缓冲
4.2 长时间阻塞导致锁失效
如果业务操作耗时超过锁的有效时间,可能导致锁被其他客户端获取。解决方案:
- 合理评估业务耗时,设置足够的锁超时时间
- 实现锁续期机制(如Redisson的看门狗)
- 将大事务拆分为小事务
4.3 主从切换导致锁丢失
在Redis主从架构下,如果主节点崩溃,从节点提升为主节点时可能会丢失锁信息。解决方案:
- 使用Redlock多节点部署
- 考虑使用Redis的WAIT命令确保数据同步
- 对于关键业务,可以考虑使用Zookeeper等CP系统
4.4 锁重入问题
同一个线程可能需要多次获取同一把锁。解决方案:
- 在锁的value中记录线程标识和重入次数
- 使用Redisson等成熟客户端,它们已经实现了可重入锁
5. Redis分布式锁的最佳实践
5.1 锁命名规范
好的锁命名应该:
- 包含业务前缀:如"order:lock:123"
- 明确锁的作用域:如全局锁、用户维度锁
- 避免使用通用名称如"lock"、"mutex"
5.2 超时时间设置
- 锁的有效时间应该大于业务执行的最长时间
- 建议设置自动续期机制
- 对于不确定耗时的操作,可以设置较短的锁时间,并在代码中定期检查是否需要续期
5.3 监控与告警
- 监控锁的获取成功率、等待时间等指标
- 设置锁等待超时的告警
- 记录锁竞争日志,用于分析性能瓶颈
5.4 降级方案
当Redis不可用时,应该考虑:
- 本地缓存降级
- 数据库乐观锁
- 排队系统替代
6. 与其他分布式锁方案的对比
6.1 Redis vs Zookeeper
| 特性 | Redis | Zookeeper |
|---|---|---|
| 性能 | 高 | 中等 |
| 一致性 | 最终一致 | 强一致 |
| 实现复杂度 | 中等 | 高 |
| 适用场景 | 高性能、允许短暂不一致 | 强一致性要求高 |
6.2 Redis vs 数据库乐观锁
数据库乐观锁适合:
- 并发量不大的场景
- 已经有数据库事务的业务
- 对性能要求不高的关键业务
Redis锁适合:
- 高并发场景
- 非事务性操作
- 需要快速失败和重试的业务
7. 真实案例:秒杀系统中的分布式锁
在秒杀系统中,我们使用Redis分布式锁来控制库存扣减:
- 用户下单时尝试获取商品锁
- 获取锁成功后检查并扣减Redis中的库存
- 生成订单后释放锁
关键优化点:
- 采用分段锁减少竞争(如将商品库存分成10段)
- 设置合理的锁超时时间(如500ms)
- 对获取锁失败的请求快速返回,避免长时间等待
这个方案在百万级QPS的秒杀活动中表现良好,库存扣减准确率100%。
