黑马点评-分布式锁-02_simple_redis_lock_setnx

发布时间:2026/6/2 14:49:09

黑马点评-分布式锁-02_simple_redis_lock_setnx 黑马点评分布式锁二Redis 锁为什么要用 setIfAbsent、过期时间和线程标识本文继续整理黑马点评 Redis 实战篇第 4 章「分布式锁」。上一篇讲清楚了为什么synchronized只能解决单 JVM 内的并发问题到了多实例部署时就需要分布式锁。这一篇进入4.2 Redis 分布式锁的实现核心思路和4.3 实现分布式锁版本一Redis 锁到底怎么写SimpleRedisLock是谁调用的setIfAbsent、过期时间、线程标识分别解决什么问题1. 这篇文章解决什么问题学 Redis 分布式锁时很容易只记住一句用 SETNX 加锁。但真正看代码时会冒出一串问题SimpleRedisLock 是谁 new 出来的 构造方法里的 name 是谁传的 name 是用户 id 吗 tryLock 里的 key 到底长什么样 setIfAbsent 到底相当于什么 Redis 命令 为什么加锁要带过期时间 为什么 value 里还要保存线程标识 为什么不能直接写 lock:order:10 - 1本文就围绕这些问题讲清楚。先给结论Redis 分布式锁的版本一本质是用一个 Redis key 表示一把业务锁谁先通过setIfAbsent成功创建这个 key谁就拿到锁加锁时要设置过期时间防止死锁value 里保存线程标识是为了后续安全解锁。2. 先把业务调用链放回来工具类不能脱离业务讲。如果只看SimpleRedisLock很容易不知道谁调用它 为什么传这个 name tryLock 成功后执行什么 unlock 在哪里调用讲义中的业务代码大致是这样LonguserIdUserHolder.getUser().getId();SimpleRedisLocklocknewSimpleRedisLock(order:userId,stringRedisTemplate);booleanisLocklock.tryLock(1200);if(!isLock){returnResult.fail(不允许重复下单);}try{IVoucherOrderServiceproxy(IVoucherOrderService)AopContext.currentProxy();returnproxy.createVoucherOrder(voucherId);}finally{lock.unlock();}这段代码的业务含义是1. 当前用户发起秒杀请求。 2. 根据 userId 创建一把下单锁。 3. 先尝试抢锁。 4. 抢不到说明同用户请求正在处理直接失败。 5. 抢到锁才进入真正创建订单逻辑。 6. 不管业务成功还是失败finally 中释放锁。流程图如下否是用户发起秒杀请求获取当前 userIdnew SimpleRedisLock(order: userId)tryLock(timeoutSec)是否加锁成功?返回不允许重复下单调用 createVoucherOrder查询是否已下单扣减库存创建订单finally unlock所以SimpleRedisLock不是孤立工具类。它是秒杀下单业务为了防止同一用户重复下单而使用的锁实现。3. ILock 接口为什么只有两个方法项目中有一个锁接口publicinterfaceILock{booleantryLock(longtimeoutSec);voidunlock();}它只有两个方法。这很符合锁的本质1. 获取锁 2. 释放锁tryLock 是什么tryLock表示尝试获取锁。输入timeoutSec锁的过期时间单位是秒输出true获取锁成功 false获取锁失败它不是一直阻塞等待。讲义这里强调的是尝试一次成功返回 true失败返回 false。unlock 是什么unlock表示释放锁。业务执行完后要调用它。如果不释放锁其他请求就可能一直拿不到锁。4. SimpleRedisLock 是什么SimpleRedisLock实现了ILockpublicclassSimpleRedisLockimplementsILock{privateStringname;privateStringRedisTemplatestringRedisTemplate;}它可以理解成用 Redis 手写出来的一把简易分布式锁。它的构造方法是publicSimpleRedisLock(Stringname,StringRedisTemplatestringRedisTemplate){this.namename;this.stringRedisTemplatestringRedisTemplate;}这里有两个参数。namename是业务锁名称。在秒杀下单中传入order:userId如果userId 10那么name order:10它不是单纯的用户 id而是业务类型 用户 id这样语义更清楚order:10 表示用户 10 的下单锁stringRedisTemplate这是 Spring Data Redis 提供的 Redis 操作对象。SimpleRedisLock需要用它去 Redis 里创建锁 key、删除锁 key。5. Redis 锁的 key 到底长什么样SimpleRedisLock中定义了前缀privatestaticfinalStringKEY_PREFIXlock:;加锁时会拼出 keyStringkeyKEY_PREFIXname;如果业务代码传入newSimpleRedisLock(order:userId,stringRedisTemplate)并且userId 10那么最终 Redis key 是lock:order:10这个 key 的含义是用户 10 的下单锁所以你可以这样理解锁的 key 表达“这把锁保护的是哪个业务对象”。在这里保护的是同一个用户的下单行为6. 为什么同一个用户要竞争同名锁假设用户 10 连续点击两次秒杀按钮。两个请求分别由线程 A 和线程 B 处理。它们都会执行newSimpleRedisLock(order:userId,stringRedisTemplate)因为userId都是 10所以两边的锁 key 都是lock:order:10这就是同名锁。同名不是 bug而是故意这样设计。因为我们希望同一个用户的两个请求竞争同一把锁。如果两个请求生成两个不同 key那它们就不会互斥一人一单又会失效。7. tryLock 的核心代码tryLock的代码大致如下OverridepublicbooleantryLock(longtimeoutSec){StringkeyKEY_PREFIXname;StringthreadIdID_PREFIXThread.currentThread().getId();BooleansuccessstringRedisTemplate.opsForValue().setIfAbsent(key,threadId,timeoutSec,TimeUnit.SECONDS);returnBoolean.TRUE.equals(success);}这段代码做了四件事1. 拼锁 key。 2. 生成当前线程标识。 3. 使用 setIfAbsent 尝试写入 Redis。 4. 返回是否加锁成功。下面一行一行拆。8. setIfAbsent 是什么核心代码是BooleansuccessstringRedisTemplate.opsForValue().setIfAbsent(key,threadId,timeoutSec,TimeUnit.SECONDS);setIfAbsent字面意思是如果不存在才设置。也就是key 不存在设置成功返回 true key 已存在设置失败返回 false它对应 Redis 思想就是SET lock:order:10 threadId NX EX 1200其中NXkey 不存在才设置 EX设置过期时间单位是秒以前常说的SETNX也是类似思想SETNX lock:order:10 threadId但更推荐把设置值和过期时间合成一条命令SET lock:order:10 threadId NX EX 1200这样可以保证加锁和设置过期时间一起完成。9. setIfAbsent 为什么适合做锁锁最核心的要求是同一时刻只能有一个线程拿到锁。Redis 的setIfAbsent刚好满足这个要求。假设两个线程同时抢线程ASET lock:order:10 A NX EX 1200 线程BSET lock:order:10 B NX EX 1200Redis 对同一个 key 的命令执行是串行的。所以只会有一个线程成功。比如线程 A 先成功Redis 中出现 lock:order:10 - A线程 B 再执行时发现 key 已存在就失败。流程图Redis线程B线程ARedis线程B线程ASET lock:order:10 A NX EX 1200trueSET lock:order:10 B NX EX 1200false所以谁先成功创建锁 key谁就拿到锁。10. 为什么锁必须设置过期时间如果只写SETNX lock:order:10 A但不设置过期时间会有什么问题假设线程 A 拿到锁后服务突然宕机或者代码异常退出没有执行unlock()。那么 Redis 中会一直存在lock:order:10 - A后续所有请求都会因为 key 已存在而加锁失败。这就是死锁。所以加锁时必须设置过期时间setIfAbsent(key,threadId,timeoutSec,TimeUnit.SECONDS)它的意义是即使持锁线程异常退出Redis 也会在超时后自动释放锁。11. 为什么加锁和设置过期时间要放在一条命令里有一种错误写法是BooleansuccessstringRedisTemplate.opsForValue().setIfAbsent(key,threadId);stringRedisTemplate.expire(key,timeoutSec,TimeUnit.SECONDS);看起来也能设置过期时间。但这里有一个危险窗口setIfAbsent 成功 ↓ 服务还没来得及 expire ↓ 服务宕机 ↓ 锁永不过期所以必须用带过期时间的原子写法setIfAbsent(key,threadId,timeoutSec,TimeUnit.SECONDS)这样加锁和设置 TTL 是一次 Redis 操作。12. 为什么 value 不能随便写 1一开始可能会觉得锁 key 存在就说明有人拿锁。 value 写什么都无所谓。比如lock:order:10 - 1但后面解锁时会出问题。假设线程 A 拿到了锁lock:order:10 - 1过一会儿锁过期了线程 B 又拿到同一把业务锁lock:order:10 - 1此时线程 A 恢复执行它怎么知道这把锁已经不是自己的不知道。因为 value 都是1。所以 value 不能随便写。它应该保存当前持锁者的唯一标识13. 线程标识是怎么生成的项目中有一行privatestaticfinalStringID_PREFIXUUID.randomUUID().toString(true)-;它会在类加载时生成一个随机前缀。可以把它理解成当前 JVM 实例的唯一标识然后加锁时再拼当前线程 idStringthreadIdID_PREFIXThread.currentThread().getId();比如ID_PREFIX a8f3c91b- Thread.currentThread().getId() 17最终threadId a8f3c91b-17这个值表示某个 JVM 实例中的某个线程14. 为什么不能只用线程 id因为线程 id 只在当前 JVM 内有意义。比如JVM A 里可以有线程 17 JVM B 里也可以有线程 17如果只存17跨 JVM 时就可能撞。所以要用JVM 随机前缀 线程 id这样更能表示到底是哪一个服务实例里的哪一个线程持有锁。15. return Boolean.TRUE.equals(success) 是为什么setIfAbsent返回的是Boolean包装类型不是基本类型boolean。理论上它可能是true false null如果直接写returnsuccess;可能触发自动拆箱。如果success是null就会出现空指针异常。所以项目写returnBoolean.TRUE.equals(success);含义是只有 success 明确等于 true才返回 true。 其它情况都返回 false。这是一个小细节但很实用。16. 版本一的 unlock讲义中最开始的解锁版本很朴素publicvoidunlock(){stringRedisTemplate.delete(KEY_PREFIXname);}它的意思是业务执行完后直接删除锁 key。如果一切顺利这当然能释放锁。但这个版本有明显隐患它不判断这把锁是不是自己加的。这会引出下一篇重点问题误删别人的锁。17. 当前版本的整体流程把 Redis 锁版本一串起来流程是否是秒杀请求进入获取 userId创建 SimpleRedisLock: order:userId拼 Redis key: lock:order:userId生成 threadIdSET key threadId NX EX timeout加锁成功?返回不允许重复下单执行业务: createVoucherOrderfinally unlock删除锁 key这套流程已经能解决多 JVM 下同一个用户并发请求竞争同一把 Redis 锁。但还没有彻底解决安全解锁问题。18. 本篇易错点1. name 不是单纯 userIdname是业务锁名称。在下单场景中设计为order:userId最终 Redis key 是lock:order:userId2. 锁 key 表达“锁谁”lock:order:10表示用户 10 的下单锁。同一个用户的请求应该竞争同一个 key。3. 锁 value 表达“谁持有”value 保存线程标识。它不是随便写的。后续解锁时要靠它判断这把锁是不是我加的。4. 过期时间不是可选项没有过期时间一旦持锁线程异常退出就可能死锁。5. setIfAbsent 要带过期时间一起执行加锁和设置过期时间分成两步会产生中间宕机风险。19. 面试怎么回答如果面试官问Redis 分布式锁的基本实现思路是什么可以这样回答可以用一个 Redis key 表示一把锁多个服务实例都去竞争这个 key。获取锁时使用SET key value NX EX timeout只有 key 不存在时才能设置成功设置成功说明拿到锁失败说明锁已经被别人持有。value 通常保存当前线程或实例的唯一标识过期时间用于避免服务异常导致死锁。如果面试官问为什么加锁要设置过期时间可以这样回答如果持锁线程在业务执行过程中异常退出或者服务宕机没有执行解锁逻辑那么锁 key 会一直留在 Redis 中后续请求永远拿不到锁。设置过期时间后即使持锁线程异常Redis 也能自动删除锁避免死锁。如果面试官问为什么锁 value 要保存线程标识可以这样回答因为释放锁时需要判断这把锁是不是自己加的。如果 value 只是固定的1线程就无法区分当前锁属于谁可能在自己的锁过期后误删别人的新锁。保存线程标识可以为后续安全解锁提供依据。20. 总结本篇讲的是 Redis 分布式锁最基础的一版。核心是这句话用 Redis key 表示锁用setIfAbsent抢锁用过期时间避免死锁用线程标识记录锁的持有者。这一版已经比本地锁更进一步。因为锁状态放到了 Redis 中多个 JVM 都能看到同一把锁。但它还没有完全安全。如果解锁时只是delete(key)就可能误删别人的锁。下一篇继续讲为什么会误删 为什么“先判断再删除”还不够 为什么最后要用 Lua

相关新闻