分布式锁
分布式锁: 跨多个服务器(进程/线程)的“全局互斥令牌”。谁抢到令牌,谁就有权操作共享资源(如扣库存、修改订单)。
库存扣减流程包括: 查库存->库存扣减->订单处理
分布式锁设计
Q: 分布式环境中为了防止库存超卖,通常需保证库存检查与扣减的原子性,方案有redis lua原子操作、redission分布式锁,两者如何选择?
核心原则:
- 内存操作选Lua,涉及IO等待(数据库、RPC)选Redisson。
- 追求极致性能选Lua,追求业务容错选Redisson。
RPC
RPC(Remote Procedure Call,远程过程调用)指的是通过网络请求远程服务接口所产生的网络IO等待。
具体来说,当应用触发一次RPC调用,比如请求下游微服务、调用第三方API、或者通过Dubbo/gRPC等框架请求隔壁服务,当前线程就会进入阻塞状态。
此时,CPU并没有在计算,而是在等待网络数据包到达,这段时间就是“RPC引起的IO等待”。
在性能排查中,这个指标通常意味着:
- 网络链路存在延迟(物理距离、交换机拥堵)。
- 下游服务处理慢(对方没返回,导致你的连接一直处于读等待状态)。
- 序列化/反序列化开销大(虽然属于CPU范畴,但传输数据包过大会加剧网络传输等待)。
在实际落地中,极少用Redisson锁直接扣高并发库存(因为阻塞太伤性能),但也不会只用裸Lua(因为无法落库)。推荐采用Lua预扣 + 异步落库,也是主流方案。
- 使用Lua脚本扣减Redis中的“可售库存”,返回扣减成功/失败。
- 成功后,将扣减事件发送到MQ,消费者异步创建订单并扣减DB库存(最终一致性)。此时的Lua只负责快速拦截流量,不做强一致性事务。
分布式锁测试
Q: 避免库存重复扣减的方式有哪些?如何测试?
- 前端防抖
- 网关限流: 单用户限流,拦脚本或恶意请求
- 客户端: 唯一请求id标识,搭配联合索引防重复入库
- 分布式锁: lua预扣 + sku(规格组合)加锁 + 异步mq落库
- db兜底: update语句的where条件先查库存(高并发时不可靠)
如何测试
- 高并发: 模拟多个用户同时对sku的并发请求,验证库存不会被扣成负数;检查请求对应的响应结果(验证乐观锁和数据库行锁在竞争时的可靠性)。
- 幂等: 基于同一订单号的重复发请求,检查仅第一次请求被正常处理;根据traceid查其他请求被拦截或返回缓存结果。
- 极端场景测回滚,如扣减库存成功但后续生成订单失败,或者分布式锁超时(Nacos缩短锁过期时间)导致业务未完成。
分布式锁竞争
高并发出现激烈的分布式锁竞争,伴随的现象如下:
P99抖动、RPS骤降、慢请求占比增加
- 吞吐量(RPS/TPS)断崖式下跌: 并发数增加时,吞吐量不再线性增长,反而趋于平缓甚至急剧下降。
- 平均响应时间(RT)呈指数级飙升: 尤其是 P99(99分位线)和 P999 飙升,成功率正常,但“慢请求”占比暴增。
- 获取锁失败率激增: 如果代码设置了超时时间(如 tryLock(100ms)),监控上会看到大量 TimeoutException 或“获取锁失败”的自定义错误码。
- 若锁内有lua脚本原子操作,redis CPU 使用率飙升。
排查步骤:
-
APM工具看哪一段链路慢: 网关慢还是业务内部慢
- SkyWalking 的 “服务” 仪表盘,观察被压服务的 “慢查询” 或 “高耗时” 端点(Endpoint),检查响应时间(Avg/ P99)曲线与压测时显示的抖动时间点是否吻合。
- SkyWalking 的 “数据库” 或 “缓存” 面板,如果 Redis 的响应时间正常(<1ms),初步排除网络和存储瓶颈,把嫌疑锁定在“应用内部排队”。
-
链路追踪
SkyWalking 的 “追踪(Trace)” 界面,筛选出该接口的 “慢追踪” 记录(例如筛选耗时 > 3s 的采样请求),观察链路火焰图、Span 中是否存在大量的 waiting 或 blocked 标记。
结论: 如果后续业务代码耗时极短,而总耗时极长,说明请求在进入业务方法之前就被卡住了。此时,矛头直指分布式锁(Redisson/ZK)的 lock 获取操作。
-
锁竞争排查-Arthas
# 压测以复现现象 # 1. 持续抓取当前最繁忙的阻塞线程(间隔1秒,连抓5次) thread -n 10 -i 1000 5 # 大量业务线程处于 WAITING/BLOCKED 状态,卡在分布式锁的获取方法上。 # 2. 找到繁忙线程后还需要找内部耗时慢的热点方法,找热力图栈顶的宽方法。 profiler start # 压测持续1-2分钟后 profiler stop --format html # 3. 链路追踪深挖,搭配jad或源码定位锁粒度 trace com.xxx.PrizeService claimPrize '#cost > 50' -n 10 --skipJDKMethod false
根因定位: 锁的 Key 范围过大(例如锁住了整个商户号,导致该商户下的所有用户请求串行化)。
优化建议: 推动开发改为细粒度锁(如商户号+商品ID)或引入分段锁(Striped Lock)。
watchdog
为了解决分布式锁“业务执行时间不确定”而设计的自动续期机制。
在业务未完成时自动续期,避免锁提前释放。适用于业务执行时间无法准确预估的长任务(如批处理、文件上传)。
如果能精确预估业务耗时(如常规数据库增删改查),建议显式传入 leaseTime (租赁期限)。这样可以省去看门狗频繁续期带来的网络开销(每10秒一次 RTT),性能更优。
触发条件: 未显式传入 leaseTime ,达到续期间隔时触发。
- 默认锁超时时间: internalLockLeaseTime = 30 秒(可通过配置修改)。
- 续期间隔: 每 internalLockLeaseTime / 3,即 10 秒执行一次续期。
锁竞争除了锁的 Key 范围过大,加上看门狗对热点key频繁续期叠加影响,伴随的现象也可能有所不同:
- 吞吐量(RPS): 呈“断崖式”下跌,不仅不增长,甚至在并发数稳定后,RPS 依然持续缓慢下滑(普通竞争是趋于平稳,这里是持续衰退)。
- 响应时间(RT): P99 曲线呈现“爬楼梯”上升形态,每过 10 秒(看门狗默认续期间隔),响应时间就跳跃一次,持续往上堆叠。
- 错误类型: 除了常规的 TimeoutException,还会高频出现 RedisConnectionException 或 Unexpected exception during expire。这是因为 Redis 忙于处理续期命令,导致正常的加锁请求被丢弃或超时。
排查步骤:
-
链路跟踪
通过APM链路跟踪,可观察到也是业务代码耗时极短,而总耗时极长,伴随 RedissonLock.tryAcquire 或 LockAsync 方法出现了循环重试的痕迹。
-
锁竞争排查-Arthas
# 压测以复测现象 dashboard -i 1000 # 观察线程池状态 # redisson-netty 线程池的活跃线程数异常增高(正常只有几个,现在变成几十个)。这些线程正是看门狗用来发送续期命令(pexpire)的 IO 线程 # 看门狗续期的核心方法: org.redisson.RedissonLock#renewExpiration # 监控续期方法的调用次数和耗时,过滤出耗时超过 500ms 的续期操作 watch org.redisson.RedissonLock renewExpiration "{params, cost}" -n 20 '#cost>500' # 正常情况: 每10秒续期一次,但发现热点key被频繁续期
根因定位: 锁被持有更久 -> 等待队列更长 -> 看门狗续期消耗 Redis CPU -> 所有命令(包括 tryLock)变慢 -> 死循环
优化建议: 1.调整锁粒度,加快冲突解决;2.tryLock显式传入leaseTime,避免看门狗频繁续期。