RedissonClient必须通过Provider封装注入,锁key需带业务维度分段(如活动ID、用户分段),tryLock参数要合理配置,且优先考虑数据库约束等轻量方案替代分布式锁。

Kratos 里直接 new RedissonClient 或在 wire.go 里硬塞实例,redissonClient 必定为 nil;锁 key 若只用 "lock:product:"+productId,高并发下会串行排队,吞吐归零。
RedissonClient 注入必须走 Provider 封装
Kratos 的 wire 依赖注入不支持直接构造 RedissonClient,它是个重量级对象,需通过 redisson.Config 构建,且生命周期要与 App 对齐。漏掉 Provider 封装或 wire 声明,service 层拿到的 redissonClient 就是 nil,调 tryLock() 直接 panic。
正确做法是:
- 在
data/redis/下定义NewRedissonClient函数,接收*redis.Options并返回*redisson.Client - 在
wire.go的ProviderSet中显式添加:wire.NewSet(NewRedissonClient) - 确保该 Provider 被
data.NewData拉起,早于 service 初始化
锁 key 必须带业务维度分段,不能裸用商品 ID
秒杀场景下,若锁 key 写成 "seckill:prod:"+productId,所有请求都会争同一把锁,不是防超卖,是制造瓶颈。多实例部署时虽能保数据一致,但 QPS 会被压到个位数。
合理设计应组合「库存分段 + 用户标识」,例如:
lockKey := fmt.Sprintf("seckill:202610:prod:%d:seg:%d", productId, userId%5)
注意要点:
- 必须包含活动时间戳或活动 ID(如
202610),避免不同批次复用旧锁导致误删 - 分段数不能拍脑袋定:按预估峰值 QPS / 单段处理极限反推(例:峰值 1000 QPS,单段扛 200 → 至少 5 段)
- 某段库存已空时,必须主动
lock.Unlock()并尝试下一组 key,否则请求卡死
tryLock 的两个参数极易配错
tryLock(waitTime, leaseTime, timeUnit) 中,waitTime 和 leaseTime 是高频出错点:
-
waitTime设太小(如100ms)→ 高并发下大量请求直接失败,返回“稍后再试”,用户体验断层 -
leaseTime设太短(如3s)→ 业务逻辑还没跑完锁就自动释放,其他协程抢入,导致超卖 - 务必校验
lock.IsHeldByCurrentThread()再调lock.Unlock(),否则可能误删别人持有的锁
分布式锁不是万能解,先看有没有更轻量方案
很多超卖问题其实不用分布式锁就能解决:
- 数据库唯一约束(如订单号唯一索引)可拦截重复下单
- 乐观版本号(
version字段 +UPDATE ... WHERE version = ?)适合库存扣减 - 幂等设计(如支付流水号全局唯一)比加锁更稳、更易测
只有当明确需要「多实例互斥执行一段非幂等逻辑」时(如定时任务防重跑、缓存击穿兜底重建),才引入分布式锁。一旦引入,就必须统一锁语义——不同服务用不同前缀、不同超时、甚至有的不加锁,等于没锁。











