黑马点评-Redisson-02_reentrant_lock

黑马点评-Redisson-02_reentrant_lock 黑马点评 Redisson 二可重入锁到底解决了什么问题本文整理自我学习黑马点评 Redis 实战篇第 5 章「分布式锁 Redisson」的 5.3 小节。学到这一节时我一开始最困惑的地方是讲义前面明明在讲黑马点评优惠券秒杀怎么突然贴了一段 Lua这段 Lua 是我们项目自己写的吗黑马点评业务真的用到了它吗Redisson 为什么要用 Redis Hash 存锁而不是像我们手写锁那样存一个字符串这篇文章就围绕这些困惑展开重点讲清楚 Redisson 可重入锁到底解决什么问题以及它底层为什么要记录“重入次数”。1. 先把最大的疑惑说清楚这段 Lua 不是业务代码讲义 5.3 里出现了一段 Luaif(redis.call(exists,KEYS[1])0)thenredis.call(hset,KEYS[1],ARGV[2],1);redis.call(pexpire,KEYS[1],ARGV[1]);returnnil;end;if(redis.call(hexists,KEYS[1],ARGV[2])1)thenredis.call(hincrby,KEYS[1],ARGV[2],1);redis.call(pexpire,KEYS[1],ARGV[1]);returnnil;end;returnredis.call(pttl,KEYS[1]);刚看到这里时很容易懵这段是我项目里写的吗 它是服务给哪个业务接口的 为什么突然开始讲 Redis Hash 和 Lua 和优惠券秒杀有什么关系先给结论这段 Lua 是 Redisson 框架内部实现RLock加锁逻辑的一部分不是黑马点评项目自己写的业务 Lua。也就是说我们业务代码里写的是RLocklockredissonClient.getLock(lock:order:userId);booleanisLocklock.tryLock();而tryLock()底层为了完成加锁会在 Redis 中执行类似讲义展示的 Lua。调用链可以理解成黑马点评业务代码 ↓ 调用 lock.tryLock() Redisson 的 RLock 实现 ↓ 执行内部 Lua Redis 中写入 / 判断 / 更新锁数据所以 5.3 不是新增了一个业务流程而是在解释Redisson 的RLock为什么比我们手写的简单 Redis 锁更成熟它到底是怎么支持“可重入”的。2. 什么叫可重入锁先不看 Redis先看一个普通 Java 场景。假设有两个方法publicvoidmethodA(){lock.lock();try{methodB();}finally{lock.unlock();}}publicvoidmethodB(){lock.lock();try{// 执行业务}finally{lock.unlock();}}如果同一个线程进入methodA()它先拿到了锁。然后methodA()内部又调用methodB()methodB()又尝试拿同一把锁。如果这把锁不可重入就会出现很奇怪的问题当前线程已经持有锁。 当前线程再次申请同一把锁。 锁发现“已经有人持有了”于是拒绝。 当前线程被自己挡住。这就是“自己把自己锁死”。所以可重入锁的意思是同一个线程已经持有某把锁时可以再次获取这把锁不会被自己阻塞。注意这里有一个关键限定必须是同一个线程。可重入不是说任何线程都能进。其他线程依然要被挡在外面。3. 为什么可重入需要“计数”可重入锁不能只是简单地说“同一个线程可以再次进入”。它还必须知道这个线程到底重复拿了几次锁。比如同一个线程拿了两次锁第一次 lock重入次数 1 第二次 lock重入次数 2那释放时也必须对应释放两次第一次 unlock重入次数从 2 减到 1锁还不能删 第二次 unlock重入次数从 1 减到 0这时才能真正释放锁如果第一次unlock()就直接删除锁会发生什么外层方法还没执行完。 内层方法 unlock 时直接删了锁。 其他线程就能进来。 临界区被破坏。所以可重入锁的核心不是“允许重复拿锁”这么简单而是允许同一线程重复拿锁并且必须记录重复次数释放时次数递减减到 0 才真正释放。4. Java 里的可重入思想state / count讲义里提到Java 的Lock底层会借助类似state的变量记录重入状态。可以这样理解state 0没有线程持有锁 state 1某个线程第一次持有锁 state 2同一个线程重入了一次 state 3同一个线程又重入了一次释放时则反过来unlock 一次state - 1 直到 state 0锁才真正释放synchronized也有类似思想同一个线程重复进入同一把锁保护的代码块时会有重入计数退出一次计数减一完全退出后锁才释放。Redisson 要在 Redis 里实现可重入也必须设计一套类似的计数机制。问题是Redis 里应该怎么存这个计数5. 为什么 Redisson 不用简单 String 存锁我们前面手写SimpleRedisLock时Redis 里的锁大概长这样lock:order:10 uuid-17这个结构可以表达这把锁属于 uuid-17 这个线程。但它不好表达uuid-17 这个线程已经重入了几次。如果要支持可重入Redisson 需要同时保存两类信息1. 这把锁是谁持有的。 2. 这个持有者已经重入了几次。所以 Redisson 使用 Redis Hash 结构。大概长这样lock:order:10 { uuid:17 : 1 }这里分三层看大 keylock:order:10 表示这把锁本身。 小 key / fielduuid:17 表示当前持锁线程。 value1 表示当前线程重入次数。如果同一个线程再次获取这把锁就变成lock:order:10 { uuid:17 : 2 }这就是 Redisson 可重入锁最关键的存储设计用 Redis Hash 的 value 保存重入次数。6. 讲义 Lua 的三个参数是什么意思讲义中说这段 Lua 有三个关键参数KEYS[1]锁名称 ARGV[1]锁失效时间 ARGV[2]id : threadId也就是锁的小 key放到例子里看假设当前锁是用户 10 的下单锁KEYS[1] lock:order:10 ARGV[1] 30000 ARGV[2] uuid:17它们分别表示lock:order:10我要操作哪一把锁 30000这把锁的过期时间单位毫秒 uuid:17当前尝试拿锁的线程是谁这三个参数合起来Redisson 就能判断这把锁是否存在 如果存在是不是我这个线程持有 如果是我持有应该把重入次数加几7. 逐行看懂 Redisson 可重入锁 Lua7.1 第一种情况锁不存在if(redis.call(exists,KEYS[1])0)thenredis.call(hset,KEYS[1],ARGV[2],1);redis.call(pexpire,KEYS[1],ARGV[1]);returnnil;end;exists是什么它是 Redis 命令用来判断 key 是否存在。输入是什么锁名称比如lock:order:10。输出是什么不存在返回0存在返回1。为什么用先判断这把锁有没有人持有。例子EXISTS lock:order:10如果返回0说明当前锁不存在可以加锁。接着执行redis.call(hset,KEYS[1],ARGV[2],1);意思是写入 HashHSET lock:order:10 uuid:17 1表示当前线程第一次拿到这把锁重入次数为 1。然后执行redis.call(pexpire,KEYS[1],ARGV[1]);pexpire是什么它是 Redis 的毫秒级过期时间命令。输入是什么key 和毫秒数。输出是什么设置结果。为什么用防止持锁线程异常退出后锁永远不释放。例子PEXPIRE lock:order:10 30000表示这把锁 30 秒后自动过期。最后returnnil;在 Redisson 这里nil不是失败而是表示加锁成功或重入成功。7.2 第二种情况锁存在但属于当前线程if(redis.call(hexists,KEYS[1],ARGV[2])1)thenredis.call(hincrby,KEYS[1],ARGV[2],1);redis.call(pexpire,KEYS[1],ARGV[1]);returnnil;end;hexists是什么它是 Redis Hash 命令用来判断某个 field 是否存在。输入是什么Hash 大 key 和 field。输出是什么存在返回1不存在返回0。为什么用判断这把锁是不是当前线程已经持有。例子HEXISTS lock:order:10 uuid:17如果返回1说明lock:order:10 这把锁已经由 uuid:17 这个线程持有。这时候 Redisson 允许当前线程重入。于是执行redis.call(hincrby,KEYS[1],ARGV[2],1);hincrby是什么它是 Redis Hash 命令用来让某个 field 的值增加指定数值。输入是什么Hash 大 key、field、增加值。输出是什么增加后的值。为什么用同一个线程重入一次重入计数就要 1。例子HINCRBY lock:order:10 uuid:17 1如果原来是lock:order:10 { uuid:17 : 1 }执行后变成lock:order:10 { uuid:17 : 2 }然后再次pexpire刷新锁过期时间。这一步可以理解为当前线程既然还在使用这把锁就重新设置一下过期时间。7.3 第三种情况锁存在但属于别人returnredis.call(pttl,KEYS[1]);如果前两个条件都不满足就说明锁存在。 但 Hash 里没有当前线程的 field。 所以锁不是当前线程持有的。这时当前线程不能进入临界区。pttl是什么它是 Redis 命令用来获取 key 剩余过期时间单位是毫秒。输入是什么锁名称。输出是什么剩余 TTL。为什么用告诉 Redisson 这把锁大概还有多久过期后续可用于等待和重试。例子PTTL lock:order:10返回25000表示这把锁大约还有 25 秒过期。8. 可重入加锁流程图这段 Lua 的逻辑可以画成这样不存在存在有没有线程尝试获取锁锁 key 是否存在?HSET 锁名 当前线程 1PEXPIRE 设置过期时间返回 nil加锁成功Hash 中是否有当前线程 field?HINCRBY 重入次数 1PEXPIRE 刷新过期时间返回 nil重入成功PTTL 返回锁剩余时间当前线程获取锁失败如果压成一句话锁不存在就创建锁锁存在但属于自己就重入计数 1锁存在但属于别人就返回剩余过期时间。9. 黑马点评业务中真的用到了可重入吗这个问题很重要。在黑马点评当前讲义的秒杀下单代码里常见写法是RLocklockredissonClient.getLock(lock:order:userId);booleanisLocklock.tryLock();try{returnproxy.createVoucherOrder(voucherId);}finally{lock.unlock();}这段业务代码表面上没有明显出现“同一个线程重复获取同一把锁”的场景。所以更准确的说法是黑马点评这段秒杀业务不一定强依赖可重入但 Redisson 提供的是通用成熟锁所以默认支持可重入。这就像synchronized也是可重入的。你不是每次业务都一定用到它的可重入特性但它作为一把通用锁必须具备这个能力。所以 5.3 的意义不是说黑马点评这个业务必须可重入否则跑不了。而是说Redisson 的 RLock 是成熟分布式锁它要支持更通用的嵌套加锁场景。10. 如果没有可重入会怎样假设有这样一个业务结构publicvoidcreateOrder(){lock.lock();try{deductStock();}finally{lock.unlock();}}publicvoiddeductStock(){lock.lock();try{// 扣库存}finally{lock.unlock();}}如果锁不可重入那么同一个线程执行流程会变成1. createOrder 拿到锁。 2. createOrder 调用 deductStock。 3. deductStock 再次尝试拿同一把锁。 4. 锁发现已经存在于是拒绝。 5. 当前线程等待自己释放锁。 6. 但自己正在等待无法继续执行到 unlock。 7. 死锁。这就是可重入锁要解决的问题。11. 易错点易错点一把 5.3 的 Lua 当成业务 Lua这段 Lua 不是我们项目自己写在resources下的脚本而是 Redisson 框架内部用于实现RLock的逻辑。易错点二以为可重入是“所有线程都能重复进”可重入只允许同一个线程重复获取同一把锁。其他线程仍然不能进入。易错点三只记住hincrby 1忘了解锁也要-1可重入一定是成对的。加锁几次解锁几次。只有计数减到 0锁才真正释放。易错点四混淆大 key 和小 key大 key锁名比如 lock:order:10 小 key线程标识比如 uuid:17 value重入次数比如 1、2、3易错点五以为return nil是失败在 Redisson 的这段加锁 Lua 中返回nil通常表示加锁成功或重入成功。返回剩余 TTL 才表示锁被别人持有当前线程没拿到锁。12. 面试回答问什么是可重入锁可以这样回答可重入锁是指同一个线程已经持有某把锁时可以再次获取这把锁而不会被自己阻塞。它通常通过重入计数实现线程每获取一次锁计数加一每释放一次锁计数减一直到计数为零才真正释放锁。问Redisson 的可重入锁是怎么实现的可以这样回答Redisson 使用 Redis Hash 结构保存锁信息。大 key 表示锁名称Hash 的 field 表示持锁线程标识value 表示该线程的重入次数。加锁时如果锁不存在就hset当前线程并设置次数为 1如果锁已经存在且 field 是当前线程就通过hincrby将重入次数加 1如果锁属于其他线程则返回锁的剩余过期时间。问为什么 Redisson 不用简单 String 存锁可以这样回答简单 String 只能方便地表示“锁属于谁”不方便记录同一个线程的重入次数。可重入锁需要同时记录持锁线程和重入计数所以 Redisson 使用 Hash 结构更合适。13. 总结5.3 这一节看起来突然讲了一段 Lua但它不是黑马点评项目自己写的业务 Lua而是 Redisson 内部实现RLock可重入能力的核心逻辑。Redisson 可重入锁的本质是用 Redis Hash 表示锁。 大 key 表示锁名。 小 key 表示当前持锁线程。 value 表示重入次数。当锁不存在时当前线程可以第一次加锁当锁存在且属于当前线程时说明这是重入计数加一当锁存在但属于其他线程时当前线程不能进入只能等待或失败。真正理解这一节后再看后面的 WatchDog 和锁重试就不会那么突兀了。因为第 5 章不是在新增黑马点评业务而是在逐步解释Redisson 为什么是一把比手写 Redis 锁更成熟的分布式锁。