1. 不加锁:会超扣(问题演示)
// 伪代码:两个线程同时转账,都基于旧余额判断
classAccount{
intbalance;
}
voidtransfer(Account from,Account to,int amount){
// 危险:读、判断、写中间没有互斥
if(from.balance>= amount){
from.balance-= amount;// 线程1、线程2可能同时进来
to.balance+= amount;
}
}
// A=100
// 线程1:A→B 转 80
// 线程2:A→C 转 80
// 两边都读到 100,都认为够 → 实际转出 160,A 却可能只剩 20
2. 一把一把加,顺序反了:死锁
RLocklockA=redisson.getLock("account:A");
RLocklockB=redisson.getLock("account:B");
// 线程1:A → B
newThread(()->{
lockA.lock();
sleep(100);// 模拟业务间隙
lockB.lock();// 等 B
try{
// 转账
}finally{
lockB.unlock();
lockA.unlock();
}
}).start();
// 线程2:B → A(顺序反了)
newThread(()->{
lockB.lock();
sleep(100);
lockA.lock();// 等 A
try{
// 转账
}finally{
lockA.unlock();
lockB.unlock();
}
}).start();
// 结果:线程1 拿着 A 等 B,线程2 拿着 B 等 A → 死锁
3. MultiLock:正确写法(推荐看这个)
importorg.redisson.api.RLock;
importorg.redisson.api.RedissonClient;
publicclassTransferService{
privatefinalRedissonClientredisson;
publicTransferService(RedissonClientredisson){
this.redisson= redisson;
}
/**
* 从 fromUser 转 amount 到 toUser
*/
publicbooleantransfer(LongfromUser,LongtoUser,intamount){
// 两把账户锁
RLocklockFrom=redisson.getLock("lock:account:"+ fromUser);
RLocklockTo=redisson.getLock("lock:account:"+ toUser);
// 联锁:要么都加上,要么整体失败(内部处理顺序/释放)
RLockmultiLock=redisson.getMultiLock(lockFrom, lockTo);
booleanlocked=false;
try{
// 最多等 3 秒抢锁;不指定 leaseTime → 可走看门狗
locked =multiLock.tryLock(3,TimeUnit.SECONDS);
if(!locked){
returnfalse;// 抢锁失败,稍后再试
}
// ===== 临界区:同一时刻只有一个转账在改这两个账户 =====
intfromBalance=getBalance(fromUser);// 查库/查缓存
if(fromBalance < amount){
returnfalse;// 余额不足
}
deduct(fromUser, amount);// A 减钱
add(toUser, amount);// B 加钱
saveLog(fromUser, toUser, amount);
returntrue;
}catch(InterruptedExceptione){
Thread.currentThread().interrupt();
returnfalse;
}finally{
if(locked){
multiLock.unlock();// 两把锁一起放
}
}
}
// 下面这些你用 MyBatis/MySQL 实现即可
privateintgetBalance(LonguserId){return100;}
privatevoiddeduct(LonguserId,intamount){}
privatevoidadd(LonguserId,intamount){}
privatevoidsaveLog(Longfrom,Longto,intamount){}
}
对照理解
不加锁:
两个转账同时读到余额100 → 超扣
乱加锁:
线程1锁A等B,线程2锁B等A → 死锁
MultiLock:
同时申请 A+B,都成功才进临界区
改完一起 unlock → 安全且不易死锁
和你秒杀的对应关系
// 你项目:锁的是「用户」,防止一人多单
RLocklock=redisson.getLock("lock:order"+ userId);
lock.tryLock(1,TimeUnit.SECONDS);
// 转账例子:锁的是「两个账户」,防止余额算错
RLockmulti=redisson.getMultiLock(lockA, lockB);
multi.tryLock(3,TimeUnit.SECONDS);
本质一样:改共享数据前先互斥,改完再放。
红锁呢
红锁 = RedLock,是 Redis 作者提出的「多 Redis 节点」分布式锁方案。别和 MultiLock 搞混。
和 MultiLock 的区别(先分清)
|
MultiLock(联锁) |
RedLock(红锁) | |
|
锁几个 key |
多个不同业务 key(账户A、账户B) |
同一个业务,但写到多个 Redis |
|
锁几台 Redis |
通常 1 台 |
多台独立 Redis(如 5 台) |
|
解决啥 |
同时保护多个资源、防死锁 |
单机 Redis 挂了/主从切换丢锁 |
MultiLock:1个Redis里 → lockA + lockB
RedLock :5个Redis里 → 同一把锁各写一遍,超过半数成功才算拿到
为什么要有红锁
单 Redis 锁的坑:
1. 客户端在「主节点」加锁成功
2. 主还没同步给从
3. 主挂了,从被提升成新主
4. 新主上没有这把锁
5. 另一个客户端又能加锁 → 两个人同时持锁
RedLock 想用 多数派 降低这种风险。
RedLock 怎么工作
假设 5 个独立 Redis(不是主从,是 5 个 master):
1. 记录当前时间
2. 依次向 5 个节点执行 SET key value NX PX
3. 如果「成功节点数 ≥ 3」(超过半数)
且「总耗时 < 锁有效期」
→ 加锁成功
4. 否则 → 失败,并把已加上的节点上的锁全删掉
5. 解锁 → 每个节点都删(用 Lua 判断 value)
Redis1 ✅ Redis2 ✅ Redis3 ❌ Redis4 ✅ Redis5 ❌
成功 3 个 ≥ 3 → 拿到锁
Redisson 代码
// 假设配了多个 RedissonClient,指向不同 Redis
RLocklock1=redisson1.getLock("lock:order:"+ userId);
RLocklock2=redisson2.getLock("lock:order:"+ userId);
RLocklock3=redisson3.getLock("lock:order:"+ userId);
// 红锁:同一把锁,多个 Redis 实例
RLockredLock=redisson1.getRedLock(lock1, lock2, lock3);
booleanlocked=false;
try{
// 等最多 1 秒;锁持有 10 秒(也可不写 leaseTime 用看门狗)
locked =redLock.tryLock(1,10,TimeUnit.SECONDS);
if(!locked){
returnResult.fail("系统繁忙");
}
// 业务:下单 / 转账 ...
returndoBusiness();
}finally{
if(locked){
redLock.unlock();
}
}
底层思路:每个节点都是一把普通 RLock,RedLock 负责「多数成功才算成功」。
红锁一定安全吗?
不一定。 业界有争议(Martin Kleppmann 质疑过)。
仍可能出问题的情况:
- 机器时钟跳变,锁提前过期
- 客户端 Full GC 卡住,锁过期了还以为自己有锁
- 本质仍依赖「过期时间」,不是强一致共识
所以:
- 多数业务:单 Redis + Redisson + 数据库唯一约束/幂等 就够
- 真要强一致:更倾向 ZooKeeper / etcd,或加 fencing token

335

被折叠的 条评论
为什么被折叠?



