分布式锁从入门到放弃:我用血泪换来的经验

2026-03-23 12 0

分布式锁从入门到放弃:我用血泪换来的经验

事情是这样的,那是一个普通的加班夜,我像往常一样愉快地写着增删改查,突然产品经理走过来说:"兄弟,咱们这个库存扣减功能好像有问题,高并发时会超卖啊!"

我心想,这不简单,加个锁嘛。于是我百度了一下"Redis分布式锁",copy了一段代码,上线。

然后呢?然后我就开启了为期三天的地狱模式。库存扣减确实不超卖了,但是有时候明明有库存,却扣减失败;有时候锁一直释放不掉;有时候并发一高,整个服务就卡死。

今天,让我用血泪教训告诉你,分布式锁没那么简单。

第一坑:Redis分布式锁的"假锁"

最开始我是这么写的:

// 错误示例1:最基础的锁
if (redis.setnx("lock:" + productId, "1")) {
    try {
        // 扣减库存
        decreaseStock(productId);
    } finally {
        redis.del("lock:" + productId);
    }
}

看起来没问题?恭喜你,这段代码能坑死你。

问题1:没有设置过期时间

如果程序在try块里抛异常或者卡死了,锁永远不会释放,后面所有请求都得等着,等着,等着……直到海枯石烂。

问题2:没有原子性

你可能觉得加了finally就万无一失?Too young。假设这种情况:

// 线程A获取锁成功
// 线程A在处理业务,TTL还剩30ms
// 30ms到了,Redis自动删除锁
// 线程B获取锁成功,开始处理
// 线程A处理完了,执行del("lock")
// 线程B的锁被删了!
// 线程C获取锁成功,和线程B同时处理
// 并发事故+1

这就是著名的"锁续期"问题,也是Redisson这些客户端为什么要用watchdog机制。

第二坑:Redisson就安全了?太天真

被坑了一次之后,我学聪明了,直接上Redisson。网上都说这个库靠谱,底层实现了一套完整的分布式锁。

用了之后确实香了一段时间,直到有一次压测,我们发现了一个诡异的问题:

1000个并发请求抢一个锁,平均响应时间800ms,P99延迟直接飙到3秒。

查了半天才发现,问题出在Redis的Pub/Sub机制上。Redisson用的是发布-订阅模式来实现锁的异步通知,当并发量大了之后,Pub/Sub本身就成了瓶颈。

更坑的是,Redisson的锁是可重入的,底层用Hash结构存储:

Key: lock:product:123
Value: {
    "uuid:threadId": 1  // 重入次数
}

听起来很美好?但如果你不小心在finally里释放锁时判断条件写错了,就会出现:

// 错误:不管三七二十一就释放
} finally {
    redissonClient.getLock("lock:product:123").unlock();
}

这种情况会导致:线程A的锁被线程B给释放了,然后线程C又拿到了锁——三个线程同时在处理,库存肯定又超卖了。

第三坑:数据库锁?那是另一个坑

后来有人建议我用数据库的行锁,说简单可靠。我一想,对啊,MySQL的SELECT ... FOR UPDATE不香吗?

香是香,但有个前提:你的事务必须足够短。问题是业务复杂度上来之后,谁敢保证事务一定能快速结束?

更坑的是数据库死锁:

// 线程A:先锁商品表,再锁订单表
START TRANSACTION;
SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 获得锁
SELECT * FROM order WHERE id = 100 FOR UPDATE; -- 等待

// 线程B:先锁订单表,再锁商品表  
START TRANSACTION;
SELECT * FROM order WHERE id = 100 FOR UPDATE; -- 获得锁
SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 等待
// 死锁!

这种ABBA死锁在分布式环境下太常见了。解决方法也很简单:所有事务必须按统一顺序获取锁。说起来容易,做起来难——代码写多了,谁还记得住那么多规则?

正确姿势:我的血泪总结

踩了这么多坑之后,我总结了一套相对靠谱的实践:

1. 能不用分布式锁就不用

没错,我是认真的。很多并发问题根本不需要分布式锁:

  • Redis原子操作DECRINCR这些命令本身就是原子的,一个DECR product_stock就解决了,干嘛要加锁?
  • 消息队列:把并发请求串行化,Kafka、RocketMQ了解一下?削峰填谷一把好手。
  • 单线程设计:有些场景下一个Redis单线程就能搞定,干嘛自己给自己找麻烦?

2. 真的需要锁?选对方案

// 推荐:使用Redisson,但要正确使用

RLock lock = redissonClient.getLock("lock:" + productId);
// 等待锁超时30秒,自动解锁10秒
// watchDog会自动续期
lock.lock(30, 10, TimeUnit.SECONDS);

try {
    // 业务逻辑
} finally {
    // 必须判断是否是当前线程持有的锁!
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

3. 考虑主从切换

如果你用的是Redis主从架构,当主节点挂掉、从节点还没同步完锁信息的时候,就会出现"多把锁"的诡异问题。

解决方案:

  • RedLock:同时向N个Redis实例申请锁,超过半数成功才算获取锁。缺点是需要部署多个Redis,成本高,性能差。
  • ZooKeeper:CP模型,天然支持选举和故障切换,就是性能比Redis差一些。
  • Etcd:和ZK类似,用Raft协议保证一致性。

我的建议是:除非金融级场景,否则别整RedLock。多数业务用Redis单节点+合理的过期时间就够了。

写在最后

分布式锁看似简单,实则坑深似海。从最初的setnx到Redisson,从数据库锁到ZooKeeper,我走了太多弯路。

但最大的坑其实是:以为加了个锁就万能了。实际上,99%的并发问题都应该从架构层面解决,而不是靠锁这种"术"的层面。

产品经理又要改需求了,这次他说要做一个"秒杀系统",支持100万人同时抢购。我当场就想辞职。

算了,还是先去看看Redis文档吧。毕竟,crud boy的命,就是这么苦。


本文作者:一个被分布式锁毒打过的后端工程师

相关文章

🦞 当AI开始「整活」:最近AI圈到底在玩什么?
还在手动折腾部署?让小龙虾帮你搞定!
为什么你的API总是被前端喷?可能是你没用对的7个黄金法则
GraphQL vs REST:别再吵了,我来告诉你该怎么选
告别配置地狱!代部署AI工具服务上线,单项目¥39起
RESTful API 那些事儿:踩坑无数后的血泪总结

发布评论