Redis 分布式锁实现流程

Redis 分布式锁:初级实现的问题与分析

1. 代码实现回顾

加锁逻辑 (tryLock)

利用 Redis 的 setnx (setIfAbsent) 实现互斥。

java 复制代码
@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标识
    long threadId = Thread.currentThread().getId();
    // set lock threadId nx ex timeoutSec
    // nx: 互斥 (不存在才设置)
    // ex: 设置超时时间 (防止死锁)
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    
    // 防止拆箱空指针异常
    return Boolean.TRUE.equals(success);
}

释放锁逻辑 (unlock)

直接删除 Redis 中的 key。

java 复制代码
@Override
public void unlock() {
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

调用(传入用户主键做key)

java 复制代码
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
    boolean isLock = lock.tryLock(120);  //超时自动释放
    if (!isLock) {
        //获取锁失败, 返回错误或重试
        return Result.fail("一个人只允许下一单");
}
//获取成功,
try {
    //事务,获取代理对象
    IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
    //创建订单(一人一单)
    return proxy.creatVoucherOrder(voucherId);
} finally {
    lock.unlock();
}

2. 当前代码引发的三大核心问题

问题一:锁超时释放(业务未执行完)

  • 现象:线程 1 获取锁,业务执行时间(150s)超过了锁的超时时间(120s)。
  • 后果:Redis 自动释放锁,此时线程 1 的业务还在跑,锁已经没了。

问题二:并发安全被破坏

  • 现象
    1. 线程 1 锁超时被自动释放。
    2. 线程 2 趁机获取锁,开始执行业务。
    3. 此时,线程 1 和线程 2 同时在操作同一资源。
  • 后果一人一单规则失效,可能产生数据错乱或超卖。

问题三:误删别人的锁(最严重)

  • 现象
    1. 线程 1 业务执行完毕。
    2. 线程 1 执行 unlock()
    3. 但此时锁已经被线程 2 持有了。
  • 后果:线程 1 删掉了线程 2 的锁。线程 3 进来再次获取锁,并发问题雪上加霜。

3. 问题根源分析

  1. 锁超时与业务时间不匹配:无法准确预估业务执行时间,固定超时时间难以应对所有情况。
  2. 无「锁归属校验」unlock() 时只管删除 key,不判断这把锁是不是自己加的,极易误删。
  3. 缺「锁续期」机制:业务没跑完,锁不会自动续命。

4. 解决方案

方案一:改进释放锁(增加归属判断)

在删除锁之前,判断 Redis 中的 value 是否为当前线程 ID。

java 复制代码
public void unlock() {
    String key = KEY_PREFIX + name;
    String currentValue = stringRedisTemplate.opsForValue().get(key);
    String threadId = Thread.currentThread().getId() + "";
    
    // 判断锁是不是自己的
    if (threadId.equals(currentValue)) {
        stringRedisTemplate.delete(key);
    }
}

⚠️ 注意 :虽然解决了误删,但 判断删除 是分两步执行的,非原子操作,在高并发下仍有极小概率出错。

方案二:引入锁续期(看门狗机制)

  • 原理:开启一个后台线程(守护线程),定期(如每 10 秒)检查业务是否还在执行。
  • 动作:如果业务未结束,自动重置锁的过期时间。
  • 效果:保证业务执行完之前,锁永远不会过期。

方案三:设置合理的超时时间

  • 根据业务平均耗时设置较长的超时时间(如 30s),但这只是缓解,不能彻底解决问题。

5. 一句话总结

当前初级实现会导致 「锁提前过期 → 多线程并发 → 误删他人锁」 的连锁反应,直接破坏分布式锁的互斥性。必须引入标识校验锁续期机制 才能在生产环境安全使用。

🔍 为什么不用 threadId,而推荐用 UUID?

核心原因:threadId 在分布式场景下存在重复风险,无法保证全局唯一,而 UUID 可以做到绝对唯一。


1. 单机 vs 分布式的差异

  • 单机环境
    同一时刻,JVM 内的 threadId 是唯一的,不会重复。
  • 分布式环境 (多实例/多机器):
    不同 JVM 进程、不同机器上的线程,threadId 完全可能重复。
    比如:机器A的线程ID是 1001,机器B的线程ID也可能是 1001
    如果这同时还是一个user用户账户:

1.分布式锁照样获取成功

java 复制代码
 SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);

2.unlock判断锁是不是自己的再释放还是无效的

→ 这就会导致:不同机器的不同线程,却持有相同的 threadId,锁归属判断直接失效。

2. 你提到的那句话的影响

"但当一个线程终止后,JVM 可能会在未来将它的 ID 分配给一个新的线程。"

这句话是单机层面的风险:

  • 线程A结束后,JVM 可能把 threadId=1001 回收,再分配给新线程B。
  • 如果锁还没过期,线程B就会误以为自己持有线程A的锁,导致误删。

结合分布式场景后,问题更严重:

  • 单机内 threadId 会复用
  • 多机间 threadId 会重复
    → 最终导致 锁归属判断完全不可靠

3. UUID 为什么更安全?

  • 全局唯一:UUID 是基于时间、机器、随机数生成的,在分布式环境下几乎不可能重复。
  • 线程+实例唯一 :可以用 UUID + threadId 组合,进一步保证"某个实例的某个线程"的绝对唯一标识。
  • 避免复用风险:UUID 不会被 JVM 回收复用,彻底解决线程ID复用导致的误判问题。

4. 代码层面的对比

❌ 旧版(用 threadId,有风险)
java 复制代码
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
  • 分布式下 threadId 可能重复
  • 单机下 threadId 可能被复用
✅ 推荐版(用 UUID,安全)
java 复制代码
// 生成全局唯一的线程标识
String threadId = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
  • 分布式环境下绝对唯一
  • 不会被 JVM 复用
  • 锁归属判断 100% 可靠
java 复制代码
public interface ILock {

    /**
     *    尝试获取锁
     *     @param  timeoutSec 锁持有的超时时间, 过期后自动释放
     *     @return true代表获取锁成功;false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);
    /**
     *  释放锁,解决了锁误删
     */
    void unlock();
}
java 复制代码
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private String name;

public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
     this.stringRedisTemplate = stringRedisTemplate;
     this.name = name;
 }

 @Override
 public boolean tryLock(long timeoutSec) {
     //获取线程标识: UUID + 线程Id
     String threadId = ID_PREFIX + Thread.currentThread().getId();

     //set lock thread1 nx ex 10   (nx是互斥,ex是设置超时时间)
     //这里redis存的value 一石二鸟, 解决了释放锁时判断是不是自己所属的问题。
     Boolean success = stringRedisTemplate.opsForValue()
             .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
     //不用判断获取锁是否成功
     // 但返回包装类自动拆箱有风险,若是null,得返回false啊
     return Boolean.TRUE.equals(success);
 }

 @Override
 public void unlock() {
     //获取线程标识
     String threadId = Thread.currentThread().getId() + ID_PREFIX;
     //获取锁中的标识
     String threadValue = stringRedisTemplate.opsForValue().get(KEY_PREFIX + threadId);
     //一致才释放锁
     if(threadId.equals(threadValue)) {
         stringRedisTemplate.delete(KEY_PREFIX + name);
     }
 }
}
相关推荐
波波七2 小时前
maven导入spring框架
数据库·spring·maven
程序猿阿伟2 小时前
《OpenClaw端口通信失效全解:监听修改与防火墙规则落地指南》
服务器·数据库·windows
进击的雷神2 小时前
突破增量抓取困境:基于数据库状态判断的高效新闻爬虫设计
数据库·爬虫·spiderflow
一叶飘零_sweeeet2 小时前
击穿 MySQL 事务隔离级别:底层实现原理 + 生产级架构选型避坑指南
数据库·mysql·架构·mysql事务隔离级别
虾..2 小时前
Linux 五种IO模型
linux·服务器·数据库
程序边界2 小时前
深度Oracle替换工程实践的技术解读(上篇)
数据库·oracle
2401_831824962 小时前
RESTful API设计最佳实践(Python版)
jvm·数据库·python
zjeweler2 小时前
redis_tools_gui_v1.2 —Redis图形化漏洞利用工具
数据库·redis·web安全·缓存·安全性测试
摇滚侠2 小时前
Spring Data Redis 主从集群 哨兵集群 分片集群 yml 配置
redis·python·spring