一、前言:当业务需要同时锁定多个资源
在电商、金融等复杂系统中,我们常遇到这样的场景:
- 创建订单 :需同时锁定 用户账户 + 商品库存
- 转账操作 :需同时锁定 转出账户 + 转入账户
- 库存扣减 :需同时锁定 SKU 库存 + 仓库配额
如果只对单个资源加锁,可能出现:
线程A:锁定用户1001 → 扣库存失败(库存被B锁住)
线程B:锁定商品2001 → 扣用户余额失败(用户被A锁住)
→ 死锁!
解决方案 :MultiLock(联锁) ------ Redisson 提供的多锁原子获取机制。
本文将深入剖析 Redisson MultiLock 的设计原理、使用方式与注意事项。
二、什么是 MultiLock?
MultiLock 是 Redisson 提供的一种组合锁 ,它允许你将多个
RLock对象组合成一个逻辑锁,只有当所有子锁都成功获取时,才算加锁成功。
核心特性:
- ✅ 原子性:要么全部获得,要么全部失败
- ✅ 自动释放:解锁时自动释放所有子锁
- ✅ 支持异构锁:可混合 Redis、ZooKeeper 等不同类型的锁(但通常用于同源 Redis 锁)
三、快速使用示例
java
@Autowired
private RedissonClient redissonClient;
public void createOrder(Long userId, Long productId) {
// 创建多个独立的锁
RLock userLock = redissonClient.getLock("user:lock:" + userId);
RLock productLock = redissonClient.getLock("product:lock:" + productId);
// 组合成 MultiLock
RLock multiLock = redissonClient.getMultiLock(userLock, productLock);
try {
// 加锁:必须同时获得 userLock 和 productLock
multiLock.lock();
// 业务逻辑:检查余额、扣库存、生成订单...
doCreateOrder(userId, productId);
} finally {
// 自动释放所有子锁
multiLock.unlock();
}
}
✅ 效果:避免因部分加锁导致的死锁或数据不一致
四、MultiLock 底层原理深度解析
1. 加锁流程:逐个尝试 + 失败回滚
Redisson 的 MultiLock.lock() 实现逻辑如下(简化版):
java
public void lock() {
List<RLock> acquiredLocks = new ArrayList<>();
try {
for (RLock lock : locks) {
lock.lock(); // 依次加锁
acquiredLocks.add(lock);
}
} catch (Exception e) {
// 一旦某个锁失败,立即释放已获得的锁(回滚)
for (RLock lock : acquiredLocks) {
lock.unlock();
}
throw e;
}
}
🔑 关键点 :顺序加锁 + 失败回滚,保证"全有或全无"
2. 解锁流程:逆序释放
java
public void unlock() {
// 通常按加锁的逆序释放(非强制,但更安全)
Collections.reverse(locks);
for (RLock lock : locks) {
lock.unlock();
}
}
💡 虽然 Redis 锁释放无严格顺序要求,但逆序释放是良好实践
五、支持 tryLock:带超时的 MultiLock
java
// 最多等待 3 秒获取所有锁,获得后持有 30 秒
boolean isLocked = multiLock.tryLock(3, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// 业务
} finally {
multiLock.unlock();
}
} else {
log.warn("无法同时获取用户和商品锁");
}
⚠️ 注意:
tryLock的 waitTime 是总等待时间,不是每个锁单独等待- Redisson 内部会动态分配剩余时间给后续锁
六、MultiLock vs RedLock:别混淆!
| 特性 | MultiLock | RedLock |
|---|---|---|
| 目的 | 锁定多个不同业务资源 | 在多个 Redis 节点上加同一把锁(高可用) |
| 锁类型 | 不同 key(如 user_lock, product_lock) | 相同 key(如 my_lock) |
| 使用场景 | 防止业务死锁 | 防止单点 Redis 故障导致锁丢失 |
| 是否推荐 | ✅ 强烈推荐 | ⚠️ 谨慎使用(有争议) |
📌 一句话区分:
- MultiLock = "我要同时锁 A 和 B"
- RedLock = "我要在 5 个 Redis 里都锁住 X"
七、最佳实践与避坑指南
✅ 1. 固定加锁顺序,避免死锁
java
// 错误:可能 A 锁 user、B 锁 product;B 锁 user、A 锁 product → 死锁
RLock lock1 = getLock(userId);
RLock lock2 = getLock(productId);
// 正确:按 ID 升序加锁
String lockKey1 = "lock:" + Math.min(userId, productId);
String lockKey2 = "lock:" + Math.max(userId, productId);
✅ 2. 锁粒度尽量细
- 不要锁整个"用户表",而是锁"用户ID"
- 减少锁竞争,提升并发
❌ 3. 避免嵌套 MultiLock
java
// 危险:容易引发复杂死锁
multiLock1.lock();
multiLock2.lock(); // 可能与 multiLock1 的子锁冲突
✅ 4. 合理设置超时时间
tryLock(waitTime, leaseTime)中,leaseTime应大于最大业务耗时
八、性能与可靠性分析
| 指标 | 说明 |
|---|---|
| 网络开销 | N 个锁 → N 次 Redis 请求(串行) |
| 失败概率 | 锁越多,整体失败概率越高 |
| 吞吐量 | 低于单锁,但远高于无锁并发冲突重试 |
| 适用规模 | 建议 ≤ 5 个资源联合加锁 |
💡 建议 :仅在强一致性要求高的场景使用 MultiLock
九、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!