ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redisson分布式锁实战:tryLock参数详解与看门狗机制剖析

Redisson分布式锁实战:tryLock参数详解与看门狗机制剖析 接手过分布式系统的同学十有八九都踩过“定时任务重复执行”的坑。明明配置了每天凌晨执行一次结果两台实例同时跑起来库存扣了两次、账单发了两封、奖励领了一份又一份。我的第一反应也是加个分布式锁但自己拿着SETNX写来写去死锁、误删、过期时间怎么定一个接一个地踩。后来换成了Redisson的tryLock这些坑才算真正填平了。这篇就把Redisson分布式锁的用法、原理、还有tryLock方法的各种参数细节一次说透。先说结论Redisson的分布式锁本质上是把Redis的原子操作封装成了可重入、可续期、可容错的完整实现API设计对Java开发者相当友好你几乎不需要了解Lua脚本细节就能用好它。但“用好”不等于“会用”tryLock那几个重载方法里藏着的参数含义、超时行为、看门狗机制搞不清楚照样会在生产环境出大问题。这篇文章不光是讲API怎么调更会拆解Redisson锁底层的实现思路分析为什么这么设计把我实际项目中踩过的坑和排查思路一并记录下来。无论你是刚接触分布式锁的新手还是在生产环境里被锁折腾过的老手这篇都能帮你少走弯路。1. Redisson分布式锁设计与思路拆解1.1 为什么不用SETNX手写分布式锁很多人在刚开始接触分布式锁时都会先看到网上那种极简方案——用Redis的SET key value NX EX加锁再用DEL解锁。单看这段逻辑确实很完美NX保证只有第一个客户端能设置成功EX保证锁不会永久持有加锁解锁都是原子操作。但这只是入门玩具放到真实业务里马上就会露馅。我举几个实际会遇到的场景第一如果业务执行时间超过锁的过期时间锁自动释放另外一台机器趁虚而入就出现两个客户端同时持有“同一把锁”的局面。这时候你还没法判断到底是自己的锁别人也能把锁删掉——这就是经典的“锁误删”问题。网上流传的用UUID作为value来校验的方案能解决一部分问题但需要判断删除两步操作在非原子的情况下依然有缝隙。第二如果加锁后业务抛出异常没有正常释放锁锁只能靠过期时间自动回收。如果过期时间设得太短大促期间业务拥堵、GC停顿稍微久一点锁提前失效设得太长宕机后锁要等很久才能被下一个请求拿到系统等于在“假死”。自己去调这个时间永远是左右为难。第三可重入问题。一个方法里先加了锁调用另一个方法又去获取同一把锁如果锁实现不支持重入当场就死锁。自己写代码处理这个要维护线程标识和计数器复杂度一下就上来了。我并不是说手写方案就一定不行而是它在工程上有太多边界情况需要处理。这些边界情况单看概率不高但在微服务架构下任何一个边界被触发都可能是线上事故。1.2 Redisson锁的核心机制Lua脚本与看门狗Redisson对上面的问题给出了系统性的答案。它把加锁和解锁的核心逻辑都用Lua脚本实现发送到Redis服务端原子执行彻底绕开了“多步操作非原子”的问题。加锁的核心Lua脚本长这样-- KEYS[1] 锁的key -- ARGV[1] 锁的过期时间 -- ARGV[2] 客户端唯一标识UUID 线程ID if (redis.call(exists, KEYS[1]) 0) then redis.call(hset, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; return redis.call(pttl, KEYS[1]);这里用的是Hash结构field是客户端唯一标识value是重入计数。第一次加锁就hset加计数当前线程再次进入就把计数加一同时刷新过期时间。这样就同时解决了可重入和锁误删的问题。再看它的“看门狗”机制这个设计挺绝的。默认情况下Redisson加锁时如果没有显式指定leaseTime就会启用一个后台定时任务每10秒检查一次锁是否还在持有中如果还在就把锁的过期时间重置回30秒。也就是说只要你的业务线程还活着锁就不会因为超时被Redis强行回收一旦线程崩溃看门狗跟着销毁锁自然在30秒后过期释放。这个机制解决了我前面说的“过期时间怎么定都不合适”的问题。你不用再绞尽脑汁去估算业务最长执行时间Redisson会帮你动态续期。1.3 Redisson锁对应用场景的适配Redisson锁并不是只有一种它根据不同的业务场景提供了多种实现。我们平时最常用的RLock是“非公平可重入锁”也就是先到先得不支持在等待队列里插队。除此之外还有公平锁FairLock、读写锁ReadWriteLock、信号量Semaphore、闭锁CountDownLatch等。这些实现都封装在RedissonClient里接口设计贴近JDK自带的并发工具上手成本很低。我自己的经验是普通业务防重、任务互斥直接用RLock就够了如果遇到读多写少、读操作不要求互斥的场景可以考虑ReadWriteLock如果是控制某个资源的并发数量比如限制同时最多3个任务执行用Semaphore更合适。文章后面我会以最常用的RLock为主线讲解其他类型实现思路是一致的。2. tryLock方法全参数解析2.1 tryLock重载方法全景RLock接口里的tryLock有多个重载版本每一个参数的取舍背后其实都对应着一种对业务场景的理解。先把全景列出来方法签名等待时间锁持有时间中断行为tryLock()不等待拿不到立即返回看门狗30秒自动续期可中断tryLock(long time, TimeUnit unit)指定时间内持续尝试看门狗30秒自动续期可中断tryLock(long waitTime, long leaseTime, TimeUnit unit)指定时间内持续尝试指定时间后自动释放可中断tryLock(long waitTime, long leaseTime, TimeUnit unit, boolean interruptsImmediately)指定时间内持续尝试指定时间后自动释放可配置最容易忽略的是第一个无参的tryLock()。从方法名上看它是“尝试获取锁”但它的语义其实是“尝试一次拿不到就立刻放弃”。这个方法对标的是ReentrantLock里的tryLock()适合那种“抢不到锁就不做了”的场景比如用户点了多次提交第二次请求拿不到锁直接返回“处理中”而不是傻等。带waitTime参数的版本才是真正“尝试一段时间”的逻辑。它在waitTime内循环向Redis发送获取锁的命令直到拿到锁或超时为止。这适合那种需要等前面任务执行完、自己再接着干的场景。2.2 waitTime与leaseTime的深度理解很多人在看tryLock源码时会错误理解waitTime和leaseTime这两个参数我详细拆一下。waitTime是你愿意为获取锁等待的最长时间。比如传了3秒意思是在这3秒内如果锁被其他人持有当前线程会阻塞等待不断重试获取3秒后还没拿到就不再等了返回false。这里的关键点是“等待是一种主动重试不是睡眠到点就醒”。leaseTime是你持有锁的最长时间也就是锁的“租约时间”。如果传了leaseTime看门狗机制会被关闭锁在指定时间后强制释放不管业务有没有执行完。如果不传就用默认的看门狗时间30秒起由后台任务动态续期。这两个参数是独立的不是“等待3秒持有5秒”这样简单堆叠。实际使用中很多同学会把waitTime设得很小比如100毫秒以为这样高并发下能快速失败。但要注意Redisson实现waitTime内的重试是基于Semaphore 订阅通知的锁被释放时Redis会发布一条消息等待中的线程收到消息后再尝试获取锁。因此waitTime非常小时如果锁还没释放线程可能在订阅上还没反应过来就已经超时了。leaseTime则要特别留意那个经典的并发场景业务执行时间超过锁的租约时间。如果设置了固定leaseTime锁会被Redis自动删除后面的请求就能拿到同一把锁原业务又不知道自己已经“失去锁”了两个任务同时在跑和没有锁没区别。Redisson默认不传leaseTime时用看门狗动态续期就是为了规避这个问题。2.3 异常与中断行为的边界情况Redisson的锁支持线程中断。线程在等待获取锁的过程中如果被interrupt()就会抛出InterruptedException。对于带interruptImmediately参数的重载方法它控制的是“在等待获取锁期间被中断时是否立即响应中断还是说先尝试获取一次锁再响应中断”。绝大多数情况下我们不需要自定义interruptImmediately使用默认值即可。但有一种场景值得考虑如果你的业务在等待锁时收到了关闭信号希望立即放弃等待、快速释放资源那就可以显式传入true。如果你希望即使被中断了也要保证“要么拿到锁执行完要么放弃等待”的一致性就可以选择先拿到锁再响应中断。2.4 lock()和tryLock()到底怎么选lock()和tryLock()的差异本质上是“必须拿到”和“尽力而为”的差异。lock()拿不到锁会一直阻塞等待直到拿到为止tryLock()则允许你设置一个最大等待时间拿不到就直接放弃。有人说lock()更省事调用后不用判空直接进业务逻辑但它有一个隐藏风险——如果使用不当比如拿锁线程长期不释放锁其他所有请求都会无限期阻塞最后把整个服务拖垮。这就像排队买票前面的人一直不离开后面所有人都等死。我的建议是除了那种竞争极少、锁持有时间极短的场景比如唯一的定时任务调度业务接口一律用tryLock并设置合理的waitTime。等不到就快速失败返回友好提示宁可这次请求不处理也不要让线程无限等待。3. Redisson分布式锁实操与核心环节实现3.1 环境准备与依赖引入实操之前先把环境搞定。你需要一个Redis服务单机版就够演示生产环境建议至少做到主从哨兵Redisson天然支持主从模式但要注意主从切换时锁可能在一瞬间丢失。如果对锁的安全性要求极高可以考虑Redisson的RedLock多节点模式但这种方案成本较高且官方也承认它在极端场景下不是绝对安全。我个人的观点是绝大多数业务场景不必上RedLock主从哨兵已经能覆盖99%以上的需求剩下那1%宁可加数据库唯一约束做兜底也不要把系统搞得太复杂。Maven项目引入依赖dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependency然后构建Redisson客户端Configuration public class RedissonConfig { Bean public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setDatabase(0); return Redisson.create(config); } }3.2 基础加锁解锁标准写法先看最常用的标准写法。我用一个秒杀扣减库存的例子来演示这也是分布式锁最经典的使用场景。Service public class StockService { Autowired private RedissonClient redissonClient; private static final String STOCK_LOCK_KEY stock:lock:product-123; public boolean deductStock(String orderId) { RLock lock redissonClient.getLock(STOCK_LOCK_KEY); boolean isLocked false; try { // 等待2秒拿不到就放弃不传leaseTime由看门狗自动续期 isLocked lock.tryLock(2, TimeUnit.SECONDS); if (!isLocked) { log.warn(获取分布式锁失败订单: {}, orderId); return false; } // 核心业务逻辑查询库存、扣减库存、生成订单 int stock getStock(); if (stock 0) { return false; } deductStockFromDB(); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(获取锁被中断, e); return false; } finally { if (isLocked lock.isHeldByCurrentThread()) { lock.unlock(); } } } }这里有一个细节值得注意tryLock(2, TimeUnit.SECONDS)只传了waitTime没有传leaseTime所以这把锁走的是看门狗续期模式——业务执行多久锁就能自动续多久从根上避免“业务没跑完锁先没了”的问题。finally块里的isHeldByCurrentThread()判断也很有必要。因为tryLock可能失败返回false此时直接unlock()会抛出IllegalMonitorStateException所以必须先判断当前线程是否持有锁。这是一个非常容易被忽略的坑网上很多示例代码根本就没写这个判断。3.3 指定leaseTime的场景怎么设置才安全有些场景我们确实需要指定leaseTime。比如你不想让锁的持有时间无限续期避免业务线程因为死循环而永远占着锁不放。又比如你希望一个耗时极短的操作在锁释放后能立刻被下一个人抢到而不是等看门狗检查周期10秒才释放。指定leaseTime最常见的坑是“时间设短了”。我见过一个真实案例一个数据同步任务平时3秒跑完结果某天数据库慢查询原任务跑了25秒但锁在第10秒就自动释放了另一台机器上的任务立刻拿到锁两边同时对同一份数据做同步最后产生了脏数据。所以如果非要指定leaseTime建议先给业务放上充足余量——比如预期耗时3秒至少设置30秒甚至更长。但余量设大了也有问题如果线程在持有锁期间宕机Redis要等很久才能让锁被其他线程拿到影响系统恢复速度。这才是真正的两难。我现在的习惯是能交给看门狗就交给看门狗不自己指定leaseTime。只有当业务中有明确的、不可跨越的最大执行时间上限时我才会指定一个保守的值并且配合监控面板盯着实际执行耗时。3.4 可重入场景的验证Redisson的锁是可重入的这一点一定要结合实际代码验证一下不然遇到一个方法里锁嵌套另一个方法就抓瞎了。public void processOrder(String orderId) { RLock lock redissonClient.getLock(order:lock: orderId); lock.lock(); try { // 参数校验 validate(orderId); // 调用另一个也加锁的方法 updateOrderStatus(orderId); // 这个方法内部又 lock 了一次 } finally { lock.unlock(); } } public void updateOrderStatus(String orderId) { RLock lock redissonClient.getLock(order:lock: orderId); lock.lock(); try { // 更新状态 } finally { lock.unlock(); } }这个例子中processOrder拿到锁后调用updateOrderStatus又尝试获取同一把锁。如果是自己用SETNX实现的锁这里就直接死锁了。Redisson的可重入机制通过Hash结构里的计数器实现同线程重复加锁计数加一解锁时计数减一减到零才会真正删除锁。这里要特别提醒锁的key一定要设计成同一个。很多人在两个方法里用了不同的前缀比如一个用order:lock:一个用orderLock:表面上看不出问题但其实它们是两把完全不同的锁自然也就不存在重入这一说了。3.5 结合Spring Boot的注解式锁如果项目里多处需要用到分布式锁每次都写一遍tryLock的样板代码会显得很冗余。我习惯封装一个自定义注解把加锁逻辑通过AOP抽离出来Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DistributedLock { String key(); // 锁的key long waitTime() default 3; // 等待时间默认3秒 TimeUnit timeUnit() default TimeUnit.SECONDS; }AOP切面里实现统一加锁解锁逻辑业务方法只需要关注核心逻辑可读性和维护性都会好很多。这里要注意一个细节key需要支持SpEL表达式比如#orderId这样才能根据方法参数动态生成锁的key。如果不用SpEL而方法参数每次都不一样那这个锁就完全没有意义了。AOP切面的大致实现思路是在Around里解析锁key调用tryLock拿到锁后执行目标方法finally里释放锁。需要注意的依然是isHeldByCurrentThread判断以及AOP切面本身会不会因为类内部调用而失效——Spring AOP默认是代理模式this调用是不会走切面的得用Resource注入自身代理类或者改用AspectJ模式。4. 常见问题与排查技巧实录4.1 锁失效问题Redisson客户端连接中断我在一次压测中遇到过很奇怪的现象本地测试加锁解锁都正常一旦压测流量上来部分请求直接抛RedisConnectionException锁就莫名其妙失效了。排查后发现Redisson默认的连接池大小有限高并发下连接被占满新的加锁请求没法在超时时间内获取连接只能报错。这不是Redisson的问题而是使用姿势的问题。解决思路有两条一是调大连接池配置setConnectionPoolSizesetConnectionMinimumIdleSize适当放大二是确保业务中锁的持有时间尽量短快进快出连接释放得越快池子越不容易被打满。注意连接池不是越大越好每个连接都会占用Redis服务端的资源设置过大会把Redis拖垮。一般建议连接池大小控制在业务并发峰值的1/5到1/10然后结合压测结果调整。4.2 锁误删问题isHeldByCurrentThread判断前文提到过finally里unlock()前一定要判断当前线程是否持有锁。这个问题我再展开讲讲因为它是初学者最容易踩的坑。看这段“错误”代码boolean isLocked lock.tryLock(2, TimeUnit.SECONDS); try { // 业务逻辑 } finally { lock.unlock(); // 这里假设你一定拿到了锁 }如果tryLock没拿到锁等待2秒超时isLocked是false但finally还是会执行unlock()。此时当前线程并没有持有锁Redisson内部会检查这个锁的field里有没有当前线程的标识没有就抛IllegalMonitorStateException。而这个异常会覆盖业务本身可能返回的结果导致调用方收到的不是“获取锁失败”而是“系统异常”影响对故障的正确判断。更稳妥的写法是下面这样finally { if (isLocked lock.isHeldByCurrentThread()) { lock.unlock(); } }4.3 看门狗续期没生效的场景有的同学反馈“我用了默认的看门狗为什么锁还是提前释放了”我排查过几个案例发现几乎都是同一个原因——在代码里显式传了leaseTime。看门狗的触发条件是不传leaseTime。只要你传了任意一个非零的leaseTimeRedisson就认为你“手动接管”了锁的过期控制看门狗会被直接跳过。所以代码里出现了类似下面这种写法就不要再指望看门狗帮你护着了lock.tryLock(5, 10, TimeUnit.SECONDS);这个写法意味着最多等5秒拿锁拿到后最多持有10秒10秒到点强制释放不管业务是否执行完。还有一种情况是Redisson版本差异。老版本里看门狗逻辑可能没有新版本这样完善如果你的项目使用了较老的Redisson版本建议升级到3.20以上的主流版本这个机制已经相当成熟了。4.4 锁的粒度设计过大与过小都不行锁的粒度直接决定系统性能。分布式锁的粒度越细并发能力越强但漏锁风险也越高粒度越粗越安全但吞吐量断崖式下降。我用实际例子说明。扣减库存的场景锁的key用product:123还是stock:123:sku:456差别非常大。前者相当于对整个商品的库存操作串行化即使有两个不同SKU的并发请求也只有一个能进来后者只锁对应SKU的库存不同SKU可以并行扣减吞吐量明显更好。但粒度太细也会有坑。比如用户下单可能涉及多件商品你分别锁每件商品的库存两个订单A、B同时下单A先锁了商品1、再等商品2B先锁了商品2、再等商品1就出现死锁了。这种场景要么提前对所有商品按固定顺序排序加锁要么直接用订单维度的大锁。我个人的经验是先问自己“这个锁真正要互斥的是什么资源”然后尽可能把锁控制在这个资源的最小维度上。跨多个资源的场景要么统一排序要么就直接上粗粒度锁简单可靠往往比高性能更重要。4.5 常见问题速查表现象可能原因排查/解决方式锁提前失效显式设置了leaseTime不传leaseTime启用看门狗unlock抛异常tryLock未成功直接解锁解锁前判断isHeldByCurrentThread等待获取锁超时waitTime设置过短增大waitTime或优化锁持有时间连接获取失败连接池过小调大connectionPoolSize不同实例互斥失败锁key不一致统一锁key生成规则锁永久不释放看门狗 死循环业务监控线程执行时间业务侧加超时熔断5. 一个生产场景的完整实践记录5.1 场景描述定时任务的分布式防重我接到过一个比较典型的业务需求每天凌晨3点系统需要对前一天的用户账单做聚合统计并生成报表推送。系统部署了两台实例做负载均衡如果两台实例同时执行聚合任务数据就会被重复计算报表金额翻倍。这个场景用Redisson的tryLock再合适不过了。核心需求是“这个任务每天只有一个实例能执行”不需要等太久抢不到锁的那台实例直接放弃就好。我给这个任务设计的锁参数是waitTime1秒不指定leaseTime。为什么waitTime设1秒因为定时任务触发时另一台实例可能正在执行上一个月的任务但正常情况下这个任务几十秒内就能跑完等于说1秒内大概率能拿到锁。如果1秒都拿不到说明要么另一台实例的任务出现了异常卡顿要么Redis本身有问题此时再继续等下去没有意义应该直接放弃并发送告警。为什么不指定leaseTime因为聚合统计任务的数据量不可控月初月末数据量大可能要跑好几分钟。设置定死的leaseTime要么太保守导致故障恢复慢要么太小导致锁提前失效。干脆交给看门狗自动续期只在业务层加一个全局超时限制兜底。5.2 业务代码实现Component public class BillAggregateJob { Autowired private RedissonClient redissonClient; private static final String BILL_AGGREGATE_LOCK job:lock:bill-aggregate; Scheduled(cron 0 0 3 * * ?) public void execute() { RLock lock redissonClient.getLock(BILL_AGGREGATE_LOCK); boolean isLocked false; try { isLocked lock.tryLock(1, TimeUnit.SECONDS); if (!isLocked) { log.warn(账单聚合任务已在其他实例执行本机跳过); return; } aggregateAndPush(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(账单聚合任务获取锁被中断, e); } finally { if (isLocked lock.isHeldByCurrentThread()) { lock.unlock(); } } } private void aggregateAndPush() { // 核心业务逻辑 } }这套方案上线后跑了半年到目前为止没有出现过双实例同时执行的情况。看门狗在长任务场景下的作用是实打实的有一次因为数据库慢查询任务跑了近20分钟锁依然牢牢握在持有方手里另一台实例始终没有抢到。5.3 监控与告警只用分布式锁还不够我更建议配套监控。这里分享三个我长期在用的指标第一个是lock.acquire.failed.count记录tryLock返回false的次数。正常情况下这个值应该趋近于0如果突然飙升说明锁的持有时间过长或者有死锁风险。第二个是lock.hold.time.milliseconds记录每次拿到锁到释放锁的时间差。这个值配合业务性能监控可以判断业务是否出现了性能退化。第三个是lock.acquire.wait.milliseconds记录从发起tryLock到成功拿到锁的时间。如果这个值长期接近waitTime上限说明锁竞争非常激烈应该考虑优化锁粒度或拆分热点key。Redisson本身没有自带这些指标但它的RLock入口可以做AOP切面统一埋点采集耗时和异常再接入Prometheus或类似监控体系。没有监控的分布式锁就像没有仪表盘的汽车你不知道它什么时候会出问题出了什么问题。5.4 关于RedLock的个人思考聊到分布式锁有一部分同学会问要不要用RedLock来替代主从哨兵模式RedLock的核心思路是同时在多个Redis节点上获取锁半数以上节点成功才认为锁获取成功。这样某个节点宕机锁也不会丢失。这个方案理论上很美好但它在实际工程中是存在争议的。Redis主从切换时的锁丢失问题RedLock不能完全避免因为客户端在某个节点拿到锁后这个节点还没来得及同步到从节点就宕机了锁依然会丢。而且RedLock要求部署至少3个独立Redis节点成本和复杂度都是成倍增加。站在工程落地角度我更倾向于在业务层做兜底。比如在数据库表上加唯一约束、在最终写入时做幂等校验锁只是第一道防线而非唯一防线。这样即使锁在极端情况下失效也不会产生不可挽回的后果而系统的复杂度保持在可控范围内。6. 关于公平锁和读写锁的扩展认知6.1 公平锁FairLock的等待队列RedissonClient还提供了getFairLock它实现了公平锁语义——等待时间最长的线程最先获得锁。名字听着很好但在高并发场景下公平锁反而可能拖慢整体吞吐。为什么因为公平锁需要在Redis里维护一个等待队列每次释放锁都要从队列中挑出等待最久的线程通知它来拿锁。这中间多出的RTT在高频加锁释放的场景下会累积成可观的性能损失。我的建议是除非业务真的依赖“先来后到”的顺序比如某些定时调度框架的节点竞选否则优先使用非公平锁。非公平锁在“锁刚好被释放”的瞬间允许后来的线程抢到虽然理论上存在线程饥饿的风险但实际业务中这种饥饿几乎不会发生。6.2 读写锁ReadWriteLock的适用与不适Redisson的读写锁语义和JDK的ReentrantReadWriteLock基本一致读锁是共享锁多个线程可以同时持有写锁是排他锁写锁获取时不能有读锁存在读锁获取时不能有写锁存在。我在一个配置中心的场景里用过它多个实例同时读取配置不互斥但有一个实例更新配置时所有读线程都要停止避免读到中间状态。这个场景如果用普通的互斥锁读请求之间也会互相阻塞性能非常不划算。换成RReadWriteLock后读操作全部并行只有写操作才独占吞吐量提升明显。但读写锁也有它的陷阱如果读锁持有时间过长写锁会一直被阻塞造成“写饥饿”。在分布式环境下读锁的持有线程可能因为网络抖动等原因迟迟不释放写锁等了很久才拿到此时业务数据可能已经变化了。所以读写锁只适合读多写少、读操作很快的场景。6.3 信号量Semaphore的并发数量控制RSemaphore不是锁但它和锁经常被配合使用。它用于限定一个资源最多能被多少个客户端同时访问。比如某个下游接口只支持5个并发超过就会被限流那就可以用信号量来控制。它的tryAcquire和trySetPermits接口和JDK的Semaphore很像。有一个容易踩坑的点是信号量的许可数存储在Redis里如果某个客户端获取许可后崩溃销毁许可不会自动归还只能靠release或等待超时。因此要给信号量设置合理的过期时间或者在做运维时把许可数重置。这比锁要棘手一些使用面也窄一些。7. 写在最后Redisson的分布式锁用起来很顺手但真正想在生产环境里稳定运行还是要理解它背后的几个关键设计Lua脚本的原子性、看门狗的续期机制、可重入的计数器结构。tryLock的各种参数重载不是让你随便挑一个用而是给了你针对不同业务场景精确调度的能力。我个人这几年的体会是分布式锁这种东西功能实现永远不是最大的难点难点在于预判各种异常场景业务执行多久、线程会不会中断、Redis会不会抖动、锁会不会被误删。把这些都想清楚了代码怎么写都是对的没想清楚即使每一个API都调用正确线上还是会出问题。如果读完这篇文章只能记住三句话我希望是第一能用看门狗就不手动指定leaseTime第二解锁前一定要判断当前线程是否持有锁第三锁只是保证一致性的第一道防线最终数据安全还是要靠业务层的幂等和兜底。
返回列表