#Redis 分布式锁
某团队使用 Redis 实现分布式锁时,遇到了一个严重问题:
// 错误实现
public String acquireLock(String key) {
return redisTemplate.opsForValue().setIfAbsent(key, "lock", 10, TimeUnit.SECONDS);
}// 释放锁时
public void releaseLock(String key) {
redisTemplate.delete(key);
}问题:
- 锁自动过期时间设为 10 秒,但业务执行需要 15 秒
- 锁在业务执行到一半时被自动释放
- 其他线程获取了锁,导致同一份数据被重复处理
- 严重时导致数据重复、资损等问题
这就是 Redis 分布式锁最常见的"锁自动释放"问题。
【架构权衡】 Redis 分布式锁看似简单,但正确实现非常困难。SET NX EX + 唯一值 + Lua 脚本释放 是标准实现,但这只是基础。真正的生产环境还需要:看门狗自动续期、高可用集群支持、RedLock 算法等。
#一、核心问题 🔴
#1.1 为什么需要分布式锁?
单机场景:synchronized 可以解决
分布式场景:synchronized 只能锁住单个 JVM,需要分布式锁分布式锁的场景:
场景1:防止重复操作
├─ 用户重复下单
├─ 幂等键控制
└─ Redis 锁用户ID
场景2:保证任务互斥
├─ 定时任务多实例部署
├─ 同一时刻只有一个实例执行
└─ Redis 锁任务名
场景3:共享资源互斥
├─ 多台机器同时修改同一份数据
├─ 库存扣减
└─ Redis 锁商品ID#1.2 分布式锁的正确实现
分布式锁的五个要求:
1. 互斥性:同一时刻只有一个客户端能获取锁
2. 不会死锁:即使持锁客户端崩溃,锁也会自动释放
3. 可重入:同一个客户端可以多次获取锁
4. 性能:高并发下的性能要可接受
5. 安全性:只能释放自己持有的锁#二、方案实现
#2.1 标准实现:SET NX EX + 唯一值
// 正确实现
@Service
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
// 获取锁
public String acquireLock(String lockKey, long expireTime, TimeUnit unit) {
// 生成唯一值(用于释放锁时校验)
String uuid = UUID.randomUUID().toString();
long timeout = unit.toSeconds(expireTime);
// SET key value NX EX timeout
// NX: 只有 key 不存在时才设置
// EX: 设置过期时间
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, uuid, expireTime, unit);
if (Boolean.TRUE.equals(result)) {
return uuid; // 获取锁成功,返回唯一值
}
return null; // 获取锁失败
}
// 释放锁
public boolean releaseLock(String lockKey, String uuid) {
// 错误的释放方式:
// redisTemplate.delete(lockKey); // ❌ 直接删除会删掉别人的锁
// 正确的释放方式:用 Lua 脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(lockKey), uuid);
return result != null && result == 1L;
}
// 续期(看门狗)
public boolean renewLock(String lockKey, String uuid, long expireTime, TimeUnit unit) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) " +
"else return 0 end";
RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(lockKey), uuid, unit.toSeconds(expireTime));
return result != null && result == 1L;
}
}#2.2 看门狗机制
// Redisson 的看门狗实现
@Service
public class RedissonDistributedLock {
@Autowired
private RedissonClient redissonClient;
public void executeWithLock(String lockKey, Runnable task) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,等待 10 秒,锁定 30 秒
// 看门狗机制:每 10 秒自动续期
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
task.run();
} else {
throw new RuntimeException("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断");
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}看门狗机制原理:
业务执行时间 < 锁过期时间:
├─ 正常执行完成
├─ 释放锁
└─ 锁自动过期
业务执行时间 > 锁过期时间:
├─ 看门狗检测到锁即将过期
├─ 自动续期(重新设置过期时间)
├─ 续期成功:继续执行
└─ 续期失败:锁已过期,被其他线程获取#2.3 RedLock 算法
// RedLock:多个 Redis 节点实现分布式锁
@Service
public class RedissonRedLock {
private RedissonClient[] redissonClients;
public boolean tryLockWithRedLock(String lockKey, long waitTime, long leaseTime) {
// 获取多个 RedissonClient 实例
RedissonClient[] clients = getRedissonClients();
int N = clients.length;
int successCount = 0;
long startTime = System.currentTimeMillis();
// 向 N 个实例获取锁
for (int i = 0; i < N; i++) {
try {
RLock lock = clients[i].getLock(lockKey);
boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
if (acquired) {
successCount++;
}
} catch (Exception e) {
// 单个实例失败,继续尝试其他实例
}
}
// 计算是否获取了多数派的锁
return successCount > N / 2;
}
public void unlockWithRedLock(String lockKey) {
// 向所有实例释放锁
for (RedissonClient client : getRedissonClients()) {
try {
RLock lock = client.getLock(lockKey);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
} catch (Exception e) {
// 单个实例失败,继续释放其他
}
}
}
}【架构权衡】 RedLock 的核心思想是:少数 Redis 节点故障不影响整体锁的正确性。但 RedLock 也有争议:它假设多个 Redis 节点是相互独立的,如果所有节点都在同一个物理机上,网络分区仍然会影响 RedLock 的正确性。
#三、生产避坑
#3.1 锁自动释放问题
// ❌ 错误:锁过期时间设置不合理
public void badLock(String lockKey) {
String uuid = acquireLock(lockKey, 10, TimeUnit.SECONDS);
if (uuid == null) return;
// 业务执行需要 15 秒
doBusiness(); // 15 秒后锁已自动释放
// 其他线程获取了锁,导致重复执行
releaseLock(lockKey, uuid);
}
// ✅ 正确:使用看门狗或预估合理的过期时间
public void goodLock(String lockKey) {
// Redisson 自动续期
RLock lock = redissonClient.getLock(lockKey);
lock.tryLock(); // 自动续期,不用担心过期
try {
doBusiness(); // 任意时长
} finally {
lock.unlock();
}
}#3.2 主从切换问题
Redis 主从切换导致锁丢失的场景:
T1: 客户端A 在主节点获取锁
T2: 主节点将锁同步到从节点
T3: 主节点崩溃
T4: 从节点晋升为主节点
T5: 客户端B 在新主节点获取锁 —— 成功!
T6: 客户端A 以为自己还持有锁 —— 但实际上已经丢了
问题:SET NX EX 只能保证单机 Redis 的正确性
主从切换会导致锁丢失解决方案:使用 RedLock(多主)或 Redisson + Sentinel/Cluster。
#四、工程代价评估
| 维度 | 评估 |
|---|---|
| 实现复杂度 | 中(标准实现简单,看门狗复杂) |
| 可靠性 | 中(单节点有风险,需要集群) |
| 性能 | 高(Redis 是内存操作) |
| 一致性 | 最终一致(不是强一致) |
| 适用场景 | 分布式锁、幂等控制 |
#五、落地 Checklist
- 唯一值:锁值必须使用唯一标识
- 原子释放:使用 Lua 脚本释放锁
- 过期时间:合理设置过期时间或使用看门狗
- 续期机制:业务执行时间可能超过锁过期时间时,使用看门狗
- 高可用:使用 Redis Sentinel 或 Cluster 避免单点故障
- 监控告警:监控锁获取失败率
- 测试验证:测试 Redis 故障、网络抖动时的锁行为