Redis分布式锁误删情况说明

一、前言:你是否遇到过"业务还没执行完,锁却消失了"?

在使用 Redis 实现分布式锁时,很多开发者会写出如下代码:

java 复制代码
// 加锁
redisTemplate.opsForValue().set("lock:order", "locked", Duration.ofSeconds(10));

// 执行业务逻辑(可能耗时 15 秒)
doBusiness();

// 解锁
redisTemplate.delete("lock:order");

看似没问题,但在高并发或网络延迟场景下,极有可能导致"锁被误删"------即 A 线程删除了 B 线程持有的锁!

本文将深入剖析误删的根本原因 ,并给出生产级安全解决方案。


二、什么是"锁误删"?为什么它很危险?

锁误删:线程 A 删除了本应由线程 B 持有的锁。

🧨 后果:

  • 锁提前释放 → 其他线程提前获取锁 → 并发安全失效
  • 可能引发超卖、重复下单、数据错乱等严重资损问题

三、误删场景还原:一个真实案例

假设优惠券秒杀中,两个请求同时处理同一用户:

时间 线程 A(请求1) 线程 B(请求2)
T1 获取锁成功(value=A) 尝试获取锁,失败(等待)
T2 开始执行业务(预计 15s) ---
T3 锁自动过期(10s 到期) ---
T4 --- 获取锁成功(value=B)
T5 业务执行完毕,调用 DEL lock ---
T6 删除了线程 B 的锁! ---
T7 --- 线程 C 立即获取锁,与线程 B 并发执行 → 超卖!

💥 关键问题 :线程 A 在 T5 时不知道锁已不属于它,却直接删除了 key!


四、根本原因分析

❌ 错误 1:解锁时不校验持有者身份

java 复制代码
redis.delete("lock"); // 谁都能删!

❌ 错误 2:锁过期时间 < 业务执行时间

  • 导致锁提前释放,其他线程趁机获取
  • 原持有者仍以为自己持有锁

❌ 错误 3:未使用唯一标识作为 value

  • 若所有线程都用 "locked" 作为 value,无法区分持有者

五、正确做法:用唯一 ID + Lua 脚本安全解锁

✅ 核心原则:

只有加锁的线程,才能删除自己的锁!

步骤 1:加锁时使用唯一 value(如 UUID)

java 复制代码
String lockValue = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent("lock:coupon:1001", lockValue, Duration.ofSeconds(30));

步骤 2:解锁时通过 Lua 脚本校验 value

Lua 复制代码
-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

步骤 3:Java 调用 Lua 解锁

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static {
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setScriptText(
        "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
        "return redis.call('DEL', KEYS[1]) " +
        "else return 0 end"
    );
    UNLOCK_SCRIPT.setResultType(Long.class);
}

public void unlock(String lockKey, String lockValue) {
    redisTemplate.execute(UNLOCK_SCRIPT, 
                         Collections.singletonList(lockKey), 
                         lockValue);
}

✅ 优势:

  • GET + DEL 原子执行,避免中间状态
  • 只有 value 匹配的线程才能删除锁
  • 彻底杜绝误删

六、进阶问题:业务执行时间 > 锁过期时间?

即使解决了误删,若业务耗时超过锁 TTL,仍会提前释放锁。

解决方案:锁自动续期(Watchdog 机制)

类似 Redisson 的 watchdog 功能:只要线程还活着,就自动延长锁有效期。

简易实现思路:
java 复制代码
// 加锁后启动后台线程
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    // 如果业务未完成,且仍持有锁,则续期
    if (businessNotDone && isLockOwner()) {
        redisTemplate.expire("lock:xxx", Duration.ofSeconds(30));
    }
}, 10, 10, TimeUnit.SECONDS);

⚠️ 注意:需配合 volatile 标志位控制生命周期,避免内存泄漏。


七、其他注意事项

1. 不要使用固定字符串作为 value

java 复制代码
// 危险!所有实例 value 相同
set("lock", "1", EX 10)

✅ 正确:每个请求生成唯一 ID(UUID / ThreadID + IP)

2. 避免 Redlock 过度设计

  • Redis 官方已不推荐 Redlock(复杂且性能低)
  • 对于大多数场景,单 Redis 实例 + 唯一 value + Lua 解锁足够安全

3. 监控锁持有时间

  • 记录加锁/解锁时间,告警异常长持有
  • 避免死锁或慢 SQL 拖垮系统

八、总结

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
IvorySQL12 小时前
PostgreSQL 日报|修复截断 zstd 备份检测(10 月 9 日)
数据库·postgresql
刘胡子大叔12 小时前
SQL 脚本的导入顺序
数据库·sql
代码什么用13 小时前
Spring对IoC的实现
数据库·spring
禾小西14 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
hz5678915 小时前
涉密视频会议设备配置指南:终端、音视频采集与配套设施选型
服务器·网络·数据库·安全·实时音视频·信息与通信·智能硬件
广州浮点FLOATLIC15 小时前
许可证服务器迁移后软件打不开:研发 IT 怎样定位连接问题
linux·服务器·数据库
程序员Sunday15 小时前
MySQL 为什么使用 B+ 树索引?把范围查询、回表和覆盖索引连起来
数据库·mysql
半杯咖啡半行码15 小时前
Qt开发实战:数据库、MV 模式、QProcess与串口通信全攻略
数据库·qt
小米里的大麦16 小时前
16 MySQL 事务
数据库·mysql
西柚小萌新16 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库