分布式锁如何选择?

技术分享
321 0

前言

分布式锁是在分布式系统中实现互斥访问共享资源的一种机制。选择适当的分布式锁实现方案取决于具体的应用场景、性能需求以及系统架构。以下是一些常见的分布式锁实现方案,以及它们的优缺点:

  1. 基于ZooKeeper的分布式锁:

    • 优点:

      • 强一致性:ZooKeeper保证了数据的一致性,可以有效避免分布式系统中的数据不一致问题。
      • 可靠性:ZooKeeper是一个高可用的分布式协调服务,具备较好的容错性。
    • 缺点:

      • 性能开销:ZooKeeper的性能相对较低,适用于对性能要求不是特别高的场景。
      • 部署复杂性:需要额外部署和维护ZooKeeper集群,增加了系统的复杂性。
  2. 基于Redis的分布式锁(Redission):

    • 优点:

      • 高性能:Redis是一个内存数据库,读写速度非常快,适用于对性能要求较高的场景。
      • 简单部署:Redis通常已经被用作系统的缓存组件,可以方便地集成在现有系统中。
    • 缺点:

      • 可靠性:由于Redis是一个内存数据库,不同于ZooKeeper的持久化存储,可能存在数据丢失的风险。
      • 实现复杂性:需要处理锁的超时、续租等细节,容易出现死锁或锁失效的问题。
  3. 基于数据库的分布式锁(mybatis plus 悲/乐观锁):

    • 优点:

      • 可靠性:数据库通常具有较好的持久化能力,可以保证锁的可靠性。
      • 事务支持:可以使用数据库的事务机制来保证锁的原子性。
    • 缺点:

      • 性能开销:相对于内存数据库,数据库的读写速度较慢,不适用于高并发的场景。
      • 部署复杂性:需要额外的数据库资源,并且数据库的高可用性配置可能较为繁琐。
  4. 其他分布式锁方案:

    • 基于文件系统的锁: 使用分布式文件系统如HDFS,但性能通常较低。
    • 基于主从同步的锁: 利用主从复制机制,但需要考虑同步延迟等问题。

一些推荐的实践原则:

  • 锁的可重入性: 考虑是否支持同一线程对同一把锁的重复获取。
  • 锁的超时处理: 考虑锁的超时机制,防止因为一些异常情况导致锁无法释放。
  • 死锁处理: 实现死锁检测和解除机制,避免因为程序bug导致的死锁。
  • 监控和管理: 提供监控和管理工具,方便对锁进行监控和调整。

选择合适的分布式锁方案需要综合考虑系统的性能需求、可靠性要求以及部署和维护的复杂性。在实际应用中,可以根据具体场景选择最适合的分布式锁方案,由于业务代码逻辑不严谨,线上时不时会爆出几个死锁case。那么,究竟什么样的分布式锁实现,才算是比较好的方案?

一、什么情况的业务下需要使用分布式锁?

比如一些涉及到资金修改转变,交易订单抢购,多人操作的信息改变等业务

二、什么情况不需要使用锁?

比如一些查询业务,MySQL的InnoDB引擎默认启动MVCC,MVCC用于数据库管理系统(DBMS)中,以支持多个事务同时进行读写操作而不发生冲突。MVCC通过在系统中维护多个版本的数据,使得每个事务在读取数据时可以看到一个一致性的快照,而不受其他事务的影响。

常见分布式锁方案对比

分类方案实现原理优点缺点
基于数据库基于mysql 表唯一索引1.表增加唯一索引 2.加锁:执行insert语句,若报错,则表明加锁失败 3.解锁:执行delete语句完全利用DB现有能力,实现简单1.锁无超时自动失效机制,有死锁风险 2.不支持锁重入,不支持阻塞等待 3.操作数据库开销大,性能不高
基于MongoDB findAndModify原子操作1.加锁:执行findAndModify原子命令查找document,若不存在则新增 2.解锁:删除document实现也很容易,较基于MySQL唯一索引的方案,性能要好很多1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员 2.锁无超时自动失效机制
基于分布式协调系统基于ZooKeeper1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点 2.解锁:删除节点1.由zk保障系统高可用 2.Curator框架已原生支持系列分布式锁命令,使用简单需单独维护一套zk集群,维保成本高
基于缓存基于redis命令1. 加锁:执行setnx,若成功再执行expire添加过期时间 2. 解锁:执行delete命令实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入
基于redis Lua脚本能力1. 加锁:执行SET lock_name random_value EX seconds NX 命令 2. 解锁:执行Lua脚本,释放锁时验证random_value -- ARGV[1]为random_value, KEYS[1]为lock_nameif redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。不支持锁重入,不支持阻塞等待

表格中对比了几种常见的方案,redis+lua基本可应付工作中分布式锁的需求。然而,当偶然看到redisson分布式锁实现方案(传送门),相比以上方案,redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作,不禁佩服作者精巧的构思和高超的编码能力。

分布式锁需满足几个条件

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下几个条件:

  1. 互斥性(Mutual Exclusion): 在任何时刻,只能有一个客户端持有锁。这确保了同一时刻只有一个客户端能够访问被锁定的资源,维护了数据的一致性。
  2. 无死锁(No Deadlock): 即使某个客户端在持有锁的过程中发生了故障,没有主动释放锁,其他客户端仍能够加锁。这样可以防止因为一个客户端的故障导致整个系统陷入死锁状态。
  3. 解铃还须系铃人(Ownership): 加锁和解锁必须由同一个客户端完成。一个客户端不能解锁由其他客户端加的锁,防止误操作导致不一致的情况。
  4. 容错性(Fault Tolerance): 分布式锁的实现应该具备容错性,即便在系统中的部分节点发生故障,只要大多数节点正常运行,客户端就应该能够获取和释放锁。这有助于确保在部分节点失效的情况下系统仍能正常工作。
  5. 性能(Performance): 分布式锁的实现应该具备良好的性能,以确保在高并发和大规模系统中,锁的获取和释放不成为系统的性能瓶颈。低延迟和高吞吐量是分布式锁的一个重要性能指标。
  6. 公平性(Fairness): 分布式锁的实现可能支持公平性,即等待时间较长的请求有更高的优先级获得锁。这有助于避免某些请求一直无法获取锁的情况,提高系统的公平性。
  7. 可扩展性(Scalability): 随着系统规模的扩大,分布式锁的实现应该具备良好的可扩展性,能够适应不断增长的并发请求和节点数量。
  8. 持久性(Persistence): 在某些场景下,分布式锁的状态可能需要持久化,以防止锁状态的丢失。这对于需要长时间占用锁的场景或需要在节点故障后保持锁状态的场景很重要。

Redisson分布式锁的实现

Redisson 分布式重入锁用法

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例:

最后更新 2024-01-26
评论 ( 0 )
OωO
隐私评论