Redisson 使用手册:从 API 误区到看门狗失效,在此终结分布式锁的噩梦
不仅是 Lock 这么简单核心 API 全景Redisson 之所以受欢迎是因为它把分布式锁封装成了我们最熟悉的java.util.concurrent.locks.Lock接口风格极大地降低了学习成本。但除了最基础的lock()还有核心功能是你必须掌握的。1. 基础那把锁RLock这是90% 场景下的默认选择。它对应 Redis 底层的Hash结构。RLock lock redisson.getLock(order:1001); lock.lock(); // 阻塞式等待默认 30秒过期自带看门狗 try { // 业务逻辑 } finally { lock.unlock(); }2. 更聪明的锁tryLock(⚡️推荐)在实际业务中我们往往不希望线程无限死等浪费资源。这里有两种常见姿势姿势 A要等待 启用看门狗 (最常用)只指定waitTime不指定leaseTime。这是既想要非阻塞或有限等待又想要自动续期的最佳实践。// 参数1wait time我只愿意排队 3秒拿不到就走人 // 参数2时间单位 // 重点没传 leaseTime所以看门狗机制会自动生效 boolean res lock.tryLock(3, TimeUnit.SECONDS); if (res) { try { // 处理业务哪怕跑 5分钟 也不怕锁过期 } finally { lock.unlock(); } } else { log.warn(抢锁失败别挤了); }姿势 B要等待 自动过期 (慎用)指定了leaseTime看门狗会失效。// 参数1wait time排队 3秒 // 参数2lease time上锁后 10秒 自动强制释放注意指定 leaseTime 会让看门狗失效 // 参数3时间单位 boolean res lock.tryLock(3, 10, TimeUnit.SECONDS); if (res) { try { // 处理业务必须保证在 10秒 内完成 } finally { lock.unlock(); } }3. 文明的排队公平锁FairLock默认的锁是非公平的Non-Fair线程抢锁全靠 CPU 调度谁快谁得。但如果你的业务要求先来后到比如抢票排队请务必使用公平锁。// 内部利用 Redis 的 List作为线程等待队列和 Hash作为超时记录实现 RLock fairLock redisson.getFairLock(ticket:queue); fairLock.lock();4. 读多写少的神器读写锁ReadWriteLock这个场景太经典了商品详情页读的人多10000次/秒改库存的人少1次/秒。如果全互斥性能直接崩盘。RReadWriteLock rwLock redisson.getReadWriteLock(product:stock:101); // 读锁多个线程可以同时加读锁只要没有写锁 rwLock.readLock().lock(); // 写锁必须等所有读锁和写锁都释放了才能加全互斥 rwLock.writeLock().lock();5. 联锁MultiLock(原子性加多把锁)有时候我们需要同时锁定多个资源比如库存和余额要么都锁住要么都不锁防止死锁。RLock lock1 redisson.getLock(lock:order); RLock lock2 redisson.getLock(lock:stock); // 同时加锁lock1 lock2 RedissonMultiLock lock new RedissonMultiLock(lock1, lock2); lock.lock();二、扒开底层Hash 结构与 Lua 脚本以下源码基于Redisson 3.16版本目前生产环境主流版本分析。Redisson 为什么能实现可重入锁为什么它比我们自己写的 SETNX 强答案藏在 Redis 的数据结构里。Redisson 并没有使用简单的String类型而是使用了Hash。1. Redis 里的样子假设我们对order:1001加锁Redis 里实际存储的数据长这样KEY: order:1001 TYPE: Hash # hash 对应 value 内容 { UUID:ThreadID : 1 # 锁的持有者 : 重入次数 }KEY: 锁的名字。FIELD(Key):UUID:ThreadId。这里由客户端生成的唯一 UUID 加上当前线程 ID 拼接而成。为什么要加 UUID因为不同服务器上的 JVM 进程 ID 可能一样必须通过客户端启动时生成的 UUIDConnectionManagerId来唯一标识一个 Redisson 实例。VALUE:1。这是重入计数器。如果同一个线程再 lock 一次这里变成 2。2. 加锁的 Lua 脚本Redisson 为了保证一系列判断和写入是原子的把它封装在 Lua 脚本里发给 Redis。-- KEYS[1] 锁名称 -- ARGV[1] 过期时间 (默认 30000ms) -- ARGV[2] 锁持有者唯一ID (UUID:ThreadId) -- 情况 1锁根本不存在 if (redis.call(exists, KEYS[1]) 0) then -- 创建 Hash设置重入次数为 1 redis.call(hincrby, KEYS[1], ARGV[2], 1); -- 设置过期时间 redis.call(pexpire, KEYS[1], ARGV[1]); return nil; -- 返回 null 表示加锁成功 end; -- 情况 2锁存在且持有者就是我重入 if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then -- 重入次数 1 redis.call(hincrby, KEYS[1], ARGV[2], 1); -- 重新续期 redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; -- 情况 3锁存在但不是我 -- 返回当前锁还剩多少毫秒过期方便客户端等待 return redis.call(pttl, KEYS[1]);这段脚本完美解释了原子性这一大坨逻辑在 Redis 里是原子执行的不会插队。可重入通过hexists判断是不是自己是的话就hincrby。互斥性如果既不是新锁也不是自己的锁直接返回剩余时间让你可以去睡一会儿再来。三、拆开看门狗的黑盒源码漫游经常听说看门狗它到底长什么样其实它本质上是一个HashedWheelTimer时间轮驱动的定时任务。1. 启动入口当我们调用lock()不传时间时最终会走到这里// RedissonLock.java private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId Thread.currentThread().getId(); Long ttl tryAcquire(leaseTime, unit, threadId); // 如果 lock 成功ttl 会返回 null if (ttl null) { return; } // 如果失败会订阅一个 Redis Channel等待锁释放的消息不用死循环空转 // ... 省略订阅逻辑 }关键在tryAcquireAsync里private T RFutureLong tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) { if (leaseTime ! -1) { // 如果你传了时间就按你的时间走不启动看门狗 return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } // 没传时间leaseTime -1 // 先设置默认 30秒 过期 RFutureLong ttlRemainingFuture tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); // 加锁成功后开启续期任务 ttlRemainingFuture.onComplete((ttlRemaining, e) - { if (e null) { if (ttlRemaining null) { // 重点启动定时续期 scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; }