在高并发场景下,分布式锁是保证数据一致性的关键组件。但很多开发者对Redis分布式锁的理解停留在"SETNX"层面,导致线上事故频发。
本文将从原理、实现、常见误用到生产级解决方案,全面梳理Redis分布式锁的正确使用姿势。
为什么需要分布式锁?
单机应用中,我们使用synchronized或ReentrantLock就能解决并发问题。但在分布式系统中:
- 多个服务实例同时访问共享资源
- 数据库事务无法覆盖跨服务的业务逻辑
- 缓存与数据库一致性需要协调
这时就需要分布式锁来保证同一时间只有一个线程/进程能执行关键代码段。
基础实现:SETNX + EXPIRE
最简单的实现:
# 获取锁
SETNX lock_key 1
EXPIRE lock_key 30
# 释放锁
DEL lock_key
问题:
- 原子性问题:SETNX和EXPIRE不是原子操作
- 死锁风险:如果获取锁后服务崩溃,锁永远不会释放
- 误删风险:释放锁时可能删除了其他客户端的锁
改进方案:SET NX PX + Lua脚本
# 获取锁(原子操作)
SET lock_key client_id NX PX 30000
# 释放锁(Lua脚本保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
优势:
- SET命令的NX和PX参数确保原子性
- Lua脚本保证释放锁的原子性
- client_id避免误删其他客户端的锁
生产级解决方案
方案1:Redisson(推荐)
Redisson提供了成熟的分布式锁实现:
RLock lock = redisson.getLock("myLock");
lock.lock(); // 自动续期
try {
// 业务逻辑
} finally {
lock.unlock();
}
特性:
- 自动续期(Watchdog机制)
- 可重入锁
- 公平锁/非公平锁
- 超时自动释放
方案2:Redission + Spring Boot
# application.yml
redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
connectionMinimumIdleSize: 10
connectionPoolSize: 64
database: 0
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
常见误用案例
案例1:锁过期时间设置不合理
- 问题:设置过短导致业务未完成锁就过期
- 后果:多个客户端同时获取到锁,数据不一致
- 解决方案:根据业务最大执行时间 + 安全余量设置
案例2:没有处理锁续期
- 问题:长耗时任务中锁过期
- 后果:其他客户端获取锁并修改数据
- 解决方案:使用自动续期机制或手动续期
案例3:忽略网络分区问题
- 问题:网络抖动导致客户端认为获取锁成功
- 后果:脑裂问题,多个客户端都认为自己持有锁
- 解决方案:使用Redlock算法或强一致性存储
最佳实践清单
- 必须设置超时时间:防止死锁
- 使用唯一标识:避免误删其他客户端的锁
- 原子操作:获取和释放锁都需原子性
- 考虑自动续期:对于长耗时任务
- 监控告警:锁获取失败率、等待时间等指标
- 降级方案:当锁服务不可用时的备用策略
性能对比测试
| 方案 | 获取锁耗时 | 释放锁耗时 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 原生SETNX | 0.5ms | 0.3ms | 低 | 测试环境 |
| SET NX PX | 0.6ms | 0.4ms | 中 | 简单业务 |
| Redisson | 1.2ms | 0.8ms | 高 | 生产环境 |
| Redlock | 2.5ms | 1.5ms | 极高 | 金融级系统 |
总结
Redis分布式锁不是简单的"加锁-解锁"操作,而是一套完整的并发控制方案。选择合适的实现方式,结合业务场景特点,才能真正发挥分布式锁的价值。
记住:锁只是手段,一致性才是目标。
本文基于实际生产经验整理,欢迎在评论区分享你的分布式锁实战经验。