在高并发世界里,分布式锁就像公司唯一卫生间的钥匙。想象一下,双十一零点,10个程序员捂着肚子冲向厕所——这时候,谁先抢到"锁",谁就能优雅地解决问题(字面意义和隐喻意义上都是)。下面我们用抢厕所的视角,拆解分布式锁的奥义!
1. 基础操作:Redis 锁の初体验
如何优雅地抢坑位?
- 占坑(加锁): 使用 SET key value NX EX timeout 命令,相当于在厕所门口贴张纸条:"有人,30秒后自动销毁"。
String lockKey = "厕所:坑位:" + 程序员工号;
String clientId = "打工人_" + UUID.randomUUID(); // 用UUID防止冒名顶替
boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
// NX:没人用才占坑,EX:30秒后纸条自燃
- 释放坑位(删锁): 用Lua脚本保证原子操作,防止手抖删了别人的锁——这相当于离开时不仅要撕纸条,还得对暗号!
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1]) // 暗号对上了才撕纸条
else
return 0 // 否则可能是隔壁老王贴的纸条,不能动!
end
2. 锁の进阶生存指南
问题1:蹲坑时间太长被踢出门?——Watchdog续命大法
场景: 你带手机进厕所刷短视频,30秒到了纸条自燃,下一个人破门而入...画面太美不敢看。
解决方案: 召唤一只电子狗(Redisson的Watchdog),每10秒检查一次你是否还在坑位,如果是就续费30秒。
RLock lock = redissonClient.getLock(lockKey);
lock.lock(); // 启动电子狗盯梢
try {
// 放心刷半小时短视频吧(不建议)
} finally {
lock.unlock(); // 离开时记得关门!
}
问题2:一个线程反复抢坑位?——可重入锁
场景: 你在厕所里突然想再拿手机,结果发现手机还在外面桌上——于是你试图出门拿手机,但系统认为你已经在厕所里,拒绝让你再进(锁不可重入的悲剧)。
解决方案: 用Hash结构记录"进入次数",像进出办公室打卡一样:
-- 加锁Lua脚本(支持反复横跳)
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[1], 1); -- 第一次进入
redis.call('pexpire', KEYS[1], ARGV[2]);
return 1;
end;
if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[1], 1); -- 第N次进入
redis.call('pexpire', KEYS[1], ARGV[2]);
return 1;
end;
return 0; -- 别人占着坑呢!
问题3:Redis集群脑裂?——RedLockの民主投票
场景: 老板A说厕所没人,老板B说有人,老板C掉线了——这时候该听谁的?(Redis主从切换导致锁状态混乱)
解决方案: RedLock算法:在5个独立Redis实例上投票,超过半数同意才算抢到坑位。
// 相当于找了5个老板同时审批你的如厕申请
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock(lockKey);
lock.lock(); // 获得3个老板的"同意"才能进去
3. Redis锁 vs ZooKeeper锁:便利店 vs 老派管家
ZooKeeper锁の优雅姿势
- 临时顺序节点: 在ZK的/toilet_lock下创建临时节点,像排队取号。只有最小的号能进,其他人监听前一个号——相当于在厕所门口排号等叫号。
- 断线自动释放: 如果你突然断网(比如手机没电),ZK会自动删除你的号——防止你"占着茅坑不拉屎"。
Battle结果
对比项 | Redis锁 | ZooKeeper锁 |
速度 | 快如闪电(内存操作) | 慢如龟速(要写磁盘) |
一致性 | "大概也许可能"一致 | "死也要一致"(ZAB协议保证) |
上手难度 | 简单如抢红包 | 复杂如IKEA说明书 |
适用场景 | 秒杀抢购(短平快) | 银行转账(不能出错) |
4. 高阶骚操作
技巧1:把大厕所拆成格子间(分段锁)
- 把100个库存拆成16个格子间(stock_1到stock_16),分散竞争压力:
int segment = productId.hashCode() % 16; // 对产品ID取模分区
String lockKey = "厕所:坑位:" + productId + ":" + segment;
// 现在可以16个人同时抢了!
技巧2:设置等待时间(防止憋死)
boolean res = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 最多等10秒,30秒后自动释放(建议带止泻药)
技巧3:抢不到就摆烂(退避算法)
int maxRetries = 3; // 最多试3次
long baseDelay = 100; // 初始等100ms
for (int i = 0; i < maxRetries; i++) {
if (tryLock()) break;
Thread.sleep(baseDelay * (1 << i) + random.nextInt(100));
// 第一次等100ms,第二次200ms...越来越佛系
}
技巧4:读多写少?搞VIP通道(读写锁)
RReadWriteLock rwLock = redisson.getReadWriteLock("rwLock");
rwLock.readLock().lock(); // 读锁:多人可同时看库存
rwLock.writeLock().lock(); // 写锁:改库存时独占厕所
5. 翻车现场与救命指南
翻车场景 | 保命方案 |
锁过期了但还没擦完屁股 | Watchdog续命 + 预估时间(少刷短视频) |
手抖删了别人的锁 | UUID标识 + Lua脚本(对暗号再删) |
Redis集群脑裂多人进厕所 | RedLock投票(虽然可能降低抢坑效率) |
抢锁的人太多系统卡死 | 拆格子间 + 读写锁(增加坑位数量) |
终极总结
- 选型建议: 大多数情况用Redisson(自带电子狗、可重入、支持RedLock),就像选自动挡汽车。 非要强一致性?上ZooKeeper(但准备好接受它的龟速)。
- 三大哲学:
- 锁要细:宁可锁10个小柜子,不锁整个仓库(减少排队)
- 超时设:给自己留后路,防止社会性死亡
- 幂等保:万一锁失效,业务操作要能重试(比如扣库存用CAS)
- 骚操作提醒: 极端场景下,可以试试不用锁——比如用Redis的DECR直接扣库存(但记得先SETNX初始化啊兄dei!)
最后友情提示:任何锁方案都要实测!不然上线后半夜接报警电话的就是你(别问我是怎么知道的)
Tags:opsforhash