写点什么

架构误区系列 5:滥用分布式锁

作者:agnostic
  • 2022-11-19
    上海
  • 本文字数:1418 字

    阅读完需:约 5 分钟

分布式锁是在分布式系统中比较常见的一个组件,同时也是各面试官比较喜欢问的问题 ^_^ 分布式锁主要作用就是对关键资源的重入保护。但是,分布式锁并不是处处适用,有很多场景下其实是没必要用到这么重的架构组件的。


最近在远期交易系统的设计中有这么一个修改订单场景:

  1. 用户在界面上进行修改。

  2. 后台收到请求后落快照片并按照用户的输入修改交易单。

  3. 调用中台和渠道交互。

  4. 根据返回的信息修改交易单信息和推进状态。


设计这个逻辑的同学为了将调用中台的逻辑放在数据库事务之外(这点考虑是 ok 的,保证不会有长事务影响系统的并发度),同时为了保证不会有并发修改导致脏数据的情况,在第一步之前和第四步之后分别增加了一个加订单锁和释放订单锁的逻辑。同时,订单锁是通过数据库实现的分布式锁:

  1. 先查询 key 为订单 id 的锁是否存在和是否被锁定。

  2. 如果否,创建或加锁(加锁定时候会带上上一个版本号)。

  3. finally 里面释放锁。

  4. 锁有超时机制。


首先,我们先来看一下这个锁实现是否正确。

我们平时碰到很多面试官都喜欢问用 redis 实现分布式锁 ^_^ 其实在实践中没有人会用缓存去实现一个分布式锁。第一,缓存会有淘汰机制,会导致锁被非正常释放。其次,缓存无法感知应用的状态,无法在应用异常的时候自动释放锁,只能用超时机制;超时这个东西就很影响锁的并发度。

在上面这个 case 中,虽然用数据库实现避免了记录被淘汰导致锁非正常释放的风险,但是超时这个还是没有很好的解决:超时过长影响并发,超时过短起不到防止重如的风险(上一个锁定的操作没做完,下一个请求由于判断到锁已经“超时”而开始执行)。

所以,如果要真的实现分布式锁,就老老实实用 etcd、zk 这种有临时阶段和 watcher 机制的中间件。同时保证 lock 和 unlock 的原子性。


其次,我们再来看一下这个 case 是否有用分布式锁的必要。

我们知道,我们一般对于交易类的存储都会进行分片的设计。一般用 userId 或者 orderId 进行分片。这个 case 中不管是 userId 还是 orderId,都可以得到,所以可以很容易的确定这个事务在那个分片中执行,直接在 1/2 步和第 4 步分别启动两个数据库事务,对 order 进行加锁,就可以解决重入的问题。同时因为是对 order 加锁,整个系统可以保持很好的并发度。在调用下游系统前后,对订单状态进行变更,在第一个事务正常第二个事务异常的状态下,可以通过单据状态进行重试。当然,这个时候下游需要保证幂等性。如果下游没有幂等性保证,可以用掉单回差的逻辑,这个就是另一趴话题了。


从上面的例子可以看出,如果是对于单分片的操作,完全没必要用分布式锁,直接用数据库锁+状态机就可以实现避免重入和支持重试,同时可以让系统保证很好的并发度。如果这种场景下引入分布式锁,系统的并发反而会受到分布式锁并发度的制约。

另外,如果要保证多个分片之间的数据库一致性,可以采用两个模式:

  1. TCC:一般人都叫 TCC 分布式事务,其实我认为 TCC 是一个编排器而已。

  2. 单库任务+重试调度。


分布式锁的使用场景,其实比较有限:

  1. 需要控制外部资源的重入,这个时候用分布式锁比较合理。但是这种场景也需要明确外部资源不能重入,如果外部服务支持幂等,其实这个需求是不存在的伪需求。

  2. 对于长周期的“事务”处理。用 DB 事务和 TCC 对于小时级的长事务都不是太适用。这种场景下可以考虑用分布式锁来避免并发。但是大部分场景下,也可以用状态机来解决这个问题。


总之,利用分布式锁防止并发(重入),适用场景非常有限。同时,分布式锁的实现,尽量不要“独创”用什么 DB、缓存、消息中间件啥的,规规矩矩用 etc/zk 来搞是正道。


发布于: 刚刚阅读数: 5
用户头像

agnostic

关注

还未添加个人签名 2019-02-14 加入

还未添加个人简介

评论

发布
暂无评论
架构误区系列5:滥用分布式锁_分布式锁_agnostic_InfoQ写作社区