一、错误示范:先 setnx 再 expire
// ❌ 非原子:setnx 成功但 expire 前宕机 → 死锁
if (redis.setnx("lock", "1") == 1) {
redis.expire("lock", 30);
// 业务
redis.del("lock");
}
二、正确姿势:原子命令
SET lock_key unique_value NX PX 30000
String result = jedis.set(key, requestId, "NX", "PX", 30000);
if ("OK".equals(result)) {
try {
// 业务逻辑
} finally {
// Lua 保证原子删除
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(requestId));
}
}
为什么必须校验 value:防止 A 线程超时后,B 获得锁,A 恢复后误删 B 的锁。
三、锁续期(Watch Dog)
业务执行时间超过锁超时时间的解决方案:private void scheduleExpirationRenewal(String key) {
Thread renewal = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(10000); // 每 10s 续一次
jedis.pexpire(key, 30000);
} catch (InterruptedException e) {
break;
}
}
});
renewal.setDaemon(true);
renewal.start();
}
四、直接使用 Redisson
@Resource
private RedissonClient redisson;
public void seckill(Long productId) {
RLock lock = redisson.getLock("seckill:" + productId);
try {
// 默认 30s,看门狗每 10s 自动续期
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请重试");
}
// 扣库存
} finally {
lock.unlock();
}
}
Redisson 帮我们做了:
- 原子加锁(Lua 脚本)
- 哈希结构支持可重入
- Watch Dog 自动续期
- 订阅解锁消息,减少无效轮询
五、Redlock 之争
Redis 作者提出的 Redlock(多数派加锁)被 Martin Kleppmann 撰文质疑:- 时钟跳跃可能导致锁失效
- GC Stop-The-World 可能让客户端"以为"还持有锁
- 追求性能与容错:单实例 + 主从复制足够(接受极小概率丢失)
- 追求强一致:使用 Zookeeper / etcd 或数据库乐观锁
评论(2)