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 拖垮系统

八、总结

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

相关推荐
AC赳赳老秦1 小时前
OpenClaw + 云数据库运维:自动备份、扩容、迁移 RDS/MySQL 云数据库
运维·开发语言·数据库·人工智能·python·mysql·openclaw
TDengine (老段)2 小时前
TDengine 物理计划生成 — 算子下沉、Exchange 与 Subplan 切分
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
swordbob2 小时前
MYSQL RR 解决“脏读+不可重复读“和“幻读“的本质区别
数据库·mysql
IvorySQL2 小时前
PostgreSQL 全球对话:开源链接世界,共建共治共享
数据库·postgresql·开源
Nontee2 小时前
新手数据库进阶:大白话图解四大隔离级别与底层机制
数据库·oracle
dishugj2 小时前
【YashanDB 认证】我的崖山数据库初体验:从陌生到上手的成长之路
数据库
前端 贾公子2 小时前
Claude Code 的 skills 源码解析 (上)
数据库·人工智能
吠品2 小时前
.NET 8 单文件发布:把 exe 和一堆 dll 打进一个文件里
服务器·数据库·windows
cmes_love2 小时前
期货五档tick数据下载教程期权五档高频历史数据以及分钟量化回测下载
数据库
蚂蚁数据AntData2 小时前
从ChatBI到业务记忆:重新定义数据智能的生产力边界
大数据·网络·数据库·人工智能·算法