【项目深入】三、分布式锁

1、Zookeeper

1.1 原理步骤

  1. 核心机制:临时顺序节点(Ephemeral Sequential Nodes)
    • 所有竞争锁的客户端在同一个父路径(如 /locks)下,创建临时顺序子节点,例如:
      Client A → /locks/lock-0000000001
      Client B → /locks/lock-0000000002
      Client C → /locks/lock-0000000003
    • 节点名中的 10 位递增序号由 ZooKeeper 服务端自动生成,保证全局唯一且有序。
  2. 获取锁的判断逻辑,每个客户端创建完自己的节点后,
    • 列出 /locks 下所有子节点并按字典序排序。
    • 如果自己的节点是序号最小的,则立即获得锁,进入临界区。
    • 否则,找到自己节点的前一个节点(直接前驱),并对其注册一个 Watcher 监听删除事件(NodeDeleted)
  3. 等待与唤醒机制(避免羊群效应)
    • 客户端只监听 自己的直接前驱节点,而不是最小节点。
    • 当前驱节点被释放(因客户端主动删除或会话超时自动消失),ZooKeeper 仅通知该等待者。
    • 被通知的客户端重新检查:若自己现在是最小节点,则获得锁;否则继续监听新的前驱。
  4. 锁的释放
    • 主动释放:客户端调用 delete() 删除自己的临时节点。
    • 被动释放:客户端崩溃或网络断开导致会话超时,ZooKeeper 自动清理其临时节点,锁自动释放,无死锁风险。

1.2 数据模型

  • 层级结构的数据模型,ZNode,唯一路径,可以存储少量数据,持久,临时,顺序类型
  • ZooKeeper 服务端内部维护一个全局单调递增的计数器,用于为顺序节点分配唯一后缀(如 /locks/lock-0000000001)。父节点的 cversion 属性记录其子节点变更次数,用于支持 Watcher 和原子操作。

1.3 ZAB

  • ZooKeeper Atomic Broadcast,原子广播协议
  • 一致性和高可用
  • 崩溃恢复,当集群启动或者Leader宕机的时候,会触发ZAB,重新选举Leader,根据zxid和myid,新Leader和Follower同步最新的事务日志,确保状态一致
  • 消息广播,所有的写请求都由Leader处理,Leader将请求转换为事务提案,广播给所有Follower,Follower收到后返回ack,leader再提交事务广播
  • ZooKeeper 采用 WAL(预写日志)机制:所有写操作先持久化到事务日志,再更新内存状态,并定期生成快照,确保崩溃后可恢复

1.4 Watcher机制

  • 客户端可以对某个ZNode注册Watcher
  • 当这个节点变更/新增/删除,ZooKeeper会异步通知客户端
  • 只能使用一次,在使用就需要重新注册

1.5 session管理

  • 客户端连接ZooKeeper有一个唯一的sessionId
  • 会话有超时时间,期间需定期发送心跳维持链接
  • 使用了Java NIO
  • ZooKeeper 的临时节点依赖 Session,Session 超时时间需合理配置(通常 10~30s)。

2、Redis

用Redission完成分布式加锁的步骤

  • 可重入锁
  • 自动续期(watchdog 机制)
  • 联锁(MultiLock)、红锁(RedLock)
  • 公平锁、读写锁等
  • Redisson 公平锁通过 ZSet 维护按时间排序的等待队列,并利用 Redis Pub/Sub 通知机制实现 FIFO 唤醒,从而保证加锁顺序的公平性。

2.1 原理步骤

  1. 使用 SET lock_key unique_value NX PX expire_time 原子命令加锁,确保加锁和设置 TTL 是原子的,避免死锁。
  2. 执行完任务后,通过lua脚本判断是否是当前值,并释放锁

2.2 可能有的问题

  • 如果业务还没执行完,锁的时间要到期了,就用watchdog 看门狗自动延长时间
  • 锁被其他客户端误删,加锁的时候,要验证value,确保value是本线程加的
  • Redis主从切换导致锁丢失

2.3 watchdog原理

  • 其实就是后台开启了一个定时任务,Redission在加锁时,同时保存了锁的名称,加锁的线程id,加锁使用的唯一标识,以及重入次数,所以会有一个定时任务不断去查询这个线程的锁是否释放了,没有的话,继续延期
  • 所以就算业务阻塞了,也一样可以延期
  • 如果因 Full GC 或线程调度问题导致 Watchdog 未能及时续期,锁可能提前过期。此时需依赖业务幂等性或数据库唯一约束兜底
  • 开启方式就是直接操作Redission的lock.lock,关闭就是lock.unlock

2.4 Redis主从切换导致的锁丢失

  • RedLock 试图通过多数派机制提升可靠性,但在网络分区或时钟漂移等极端情况下仍可能失效。官方已不将其作为强一致方案推荐,仅适用于'尽力而为'的场景。
  • 因为Redis是AP模式,如果该问题频发,只能切换为CP模式的ZooKeeper
  • Redis的主从,就是直接复制数据,所以会存在主数据写入还未同步宕机,导致从库没有数据的问题

2.5 联锁和读写锁

  • 联锁 按顺序尝试获取 lock1、lock2、lock3
  • 如果在超时时间内所有锁都获取成功 → 返回成功
  • 如果任何一个锁获取失败 → 立即释放已获取的锁,并返回失败(或重试)
  • 调用 multiLock.unlock() 会依次释放所有子锁
  • 即使中间某个解锁失败,也会继续尝试释放其他锁(尽力而为)
  • 读写锁 使用 一个 Hash 结构 + Pub/Sub 实现:
特性 MultiLock ReadWriteLock
目的 同时锁定多个资源 区分读/写权限
锁类型 多个独立排他锁 一个锁,两种模式
并发性 无并发(全排他) 读可并发,写独占
数据结构 多个 key 单个 Hash
适用场景 跨资源事务 读多写少

3、分布式锁

  • 分布式锁其实更接近于单体服务的悲观锁
  • 优先考虑乐观锁,幂等,队列等方式处理竞争问题
相关推荐
闪电悠米2 小时前
黑马点评-Redis 消息队列-03_stream_consumer_group
开发语言·数据库·redis·分布式·缓存·junit·lua
z落落5 小时前
C# 事件(Event)+自定义带参数事件例子
开发语言·分布式·c#
我是一颗柠檬7 小时前
【Java项目技术亮点】分库分表+数据路由策略:单表5000万后的架构升级方案
java·开发语言·分布式·架构
半夜修仙8 小时前
RabbitMQ中如何保证消息的可靠性传输
java·分布式·中间件·rabbitmq·github·java-rabbitmq
小二·10 小时前
Redis 7 分布式缓存架构实战
redis·分布式·缓存
zhuhai_xigedian10 小时前
源网荷储一体化 vs 传统供用电模式:差异、优势与转型路径
大数据·人工智能·分布式·系统架构·能源
凯源智能12 小时前
屋顶分布式光伏箱变远程测控实战:宝鸡法士特项目高效交付解析
分布式
Amy1870211182313 小时前
东南亚智慧物流园区的“隐形守护者”:有源滤波柜如何驯服变频器5/7次谐波
分布式·能源
闪电悠米14 小时前
黑马点评-Redis 消息队列-04_stream_seckill_order
数据库·redis·分布式·缓存·oracle·junit·lua
HLAIA光子14 小时前
分布式锁与事务:你的微服务可能根本不需要它们
分布式·后端·微服务