
凌晨2点,手机疯狂震动。运维同事在群里艾特所有人:"订单系统炸了,大量重复订单!"我瞬间清醒,脑子里闪过一个念头——完了,分布式锁又出幺蛾子了。
排查到最后发现,问题出在我们用的那个"看起来很简单"的Redis分布式锁实现上。那一刻我深刻意识到,分布式锁这个看似基础的组件,水其实深得很。
以前单机应用多简单,一个synchronized关键字就能搞定所有并发问题。但微服务时代,多个实例同时运行,JVM级别的锁根本管不了其他机器上的线程。
你有没有遇到过这种场景:用户疯狂点击下单按钮,结果生成了好几个订单?或者库存扣减时出现超卖?这些都是典型的分布式环境下缺乏有效锁机制导致的。
刚开始接触Redis分布式锁时,大家都会写出这样的代码:
public boolean tryLock(String key) {
// 看起来很简单对吧?
return jedis.setnx(key, "locked") == ;
}
public void unlock(String key) {
jedis.del(key);
}
我敢打赌,十个新手里有九个会这么写。但这玩意儿有个致命问题:如果业务逻辑执行过程中服务挂了,锁永远不会被释放。
生产环境就这么被我搞挂过一次,所有请求都卡在获取锁的地方,系统彻底僵死。那种绝望的感觉,说多了都是泪。
吃一堑长一智,马上想到给锁加个过期时间:
public boolean tryLock(String key, int expireSeconds) {
if (jedis.setnx(key, "locked") == ) {
jedis.expire(key, expireSeconds);
return true;
}
return false;
}
但这里又有个原子性问题:如果setnx成功了,但还没来得及expire服务就挂了呢?锁还是会永远存在。
Redis 2.6.12版本开始,SET命令支持更多参数,可以原子性地设置值和过期时间:
public class RedisDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
public boolean tryLock(String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST,
SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result);
}
public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
return result.equals(1L);
}
}
注意这里用requestId作为锁的值,释放锁时会验证,防止误删别人的锁。这个细节很重要,我见过不少因为没做这个校验导致的bug。
手写分布式锁确实容易出问题,Redisson这个库帮我们封装了很多细节:
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void createOrder(String userId) {
String lockKey = "order_lock_" + userId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待10秒,锁自动释放时间30秒
if (lock.tryLock(, , TimeUnit.SECONDS)) {
// 处理订单逻辑
processOrder(userId);
} else {
throw new RuntimeException("获取锁失败,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 只释放自己持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson还有个看门狗机制,如果你不指定锁的过期时间,它会每隔10秒检查一次,如果锁还被持有就自动续期。这解决了业务执行时间不可预估的问题。
我在压测环境跑了一轮对比,结果很有意思:
可以看出,功能越丰富,性能损耗越大。但对于大部分业务场景,这点性能差异完全可以接受,毕竟稳定性比那几毫秒更重要。
根据我这些年的踩坑经验:
高并发、性能敏感的场景:手写SET+Lua脚本方案,控制精度,性能最优。
大部分业务场景:直接用Redisson,省心省力,功能完善。
对一致性要求极高的场景:考虑RedLock算法,或者干脆用ZooKeeper。
记住一点:技术选型没有标准答案,只有最适合当前业务的选择。我见过为了炫技选择复杂方案最后把自己坑惨的,也见过因为过度追求性能忽略可靠性导致线上事故的。
你在项目中用过哪种分布式锁方案?踩过什么坑?欢迎留言分享你的经验。