【单机锁】实现原理

1.互斥锁 sync.Mutex 的实现原理;

1.1获取策略有如下两种:

• 阻塞/唤醒:将当前 goroutine 阻塞挂起,直到锁被释放后,以回调的方式将阻塞 goroutine 重新唤醒,进行锁争夺;
• 自旋 + CAS:基于自旋结合 CAS 的方式,重复校验锁的状态并尝试获取锁,始终把主动权握在手中.
上述方案各有优劣,且有其适用的场景:
在这里eeee插入图片描述

1.2sync.Mutex的方案

1.2.1具体方案如下:

• 首先保持乐观,goroutine 采用自旋 + CAS 的策略争夺锁;
• 尝试持续受挫达到一定条件后,判定当前过于激烈,则由自旋转为 阻塞/挂起模式.

1.2.2转换的条件:

• 自旋累计达到 4 次仍未取得战果;
• CPU 单核或仅有单个 P 调度器;(此时自旋,其他 goroutine 根本没机会释放锁,自旋纯属空转);
• 当前 P 的执行队列中仍有待执行的 G. (避免因自旋影响到 GMP 调度效率).

1.2.3运行的两种模式:

正常模式/非饥饿模式:这是 sync.Mutex 默认采用的模式. 当有 goroutine 从阻塞队列被唤醒时,会和此时先进入抢锁流程的 goroutine 进行锁资源的争夺,假如抢锁失败,会重新回到阻塞队列头部.(值得一提的是,此时被唤醒的老 goroutine 相比新 goroutine 是处于劣势地位,因为新 goroutine 已经在占用 CPU 时间片,且新 goroutine 可能存在多个,从而形成多对一的人数优势,因此形势对老 goroutine 不利.)
饥饿模式:这是 sync.Mutex 为拯救陷入饥荒的老 goroutine 而启用的特殊机制,饥饿模式下,锁的所有权按照阻塞队列的顺序进行依次传递. 新 goroutine 进行流程时不得抢锁,而是进入队列尾部排队.

1.2.4两种模式的转换条件

• 默认为正常模式;
• 正常模式 -> 饥饿模式:当阻塞队列存在 goroutine 等锁超过 1ms 而不得,则进入饥饿模式;
• 饥饿模式 -> 正常模式:当阻塞队列已清空,或取得锁的 goroutine 等锁时间已低于 1ms 时,则回到正常模式.
小结:

正常模式灵活机动,性能较好;饥饿模式严格死板,但能捍卫公平的底线. 因此,两种模式的切换体现了 sync.Mutex
为适应环境变化,在公平与性能之间做出的调整与权衡. 回头观望,这一项因地制宜、随机应变的能力正是许多优秀工具所共有的特质.

1.2.5唤醒标识:

sync.Mutex 通过一个 mutexWoken 标识位,标志出当前是否已有 goroutine 在自旋抢锁或存在 goroutine 从阻塞队列中被唤醒;倘若 mutexWoken 为 true,且此时有解锁动作发生时,就没必要再额外唤醒阻塞的 goroutine 从而引起竞争内耗。缓解竞争压力和性能损耗。

1.3源码走读

数据结构:
方法:
lock

快速路径通过 CompareAndSwapInt32
尝试以原子的方式获取未被锁定的锁。如果这一步失败,表示锁已被持有或处于某种特殊状态(例如,饥饿模式),则会进入慢速路径 lockSlow

• 首先进行一轮 CAS 操作,假如当前未上锁且锁内不存在阻塞协程,则直接 CAS 抢锁成功返回;
• 第一轮初探失败,则进入 lockSlow 流程,下面细谈.
lockSlow
a.自旋获取锁:

  • 如果锁已被持有且未进入饥饿模式,并且当前 goroutine 可以自旋(runtime_canSpin),则进入自旋。
  • 尝试设置 mutexWoken 标志,告知 Unlock 不唤醒其他阻塞的 goroutine。
  • 执行自旋操作(runtime_doSpin),增加自旋计数并更新 old 状态。

b.更新状态:

  • 构建新的状态 new。
  • 如果不在饥饿模式,尝试设置 mutexLocked 标志,表示尝试获取锁。
  • 如果锁已被持有或处于饥饿模式,增加等待者计数。
  • 如果当前进入饥饿模式且锁已被持有,设置 mutexStarving 标志。
  • 如果 goroutine 被唤醒,清除 mutexWoken 标志。

c.状态更新成功

  • 如果状态更新成功且锁未被持有或不在饥饿模式,跳出循环,表示成功获取锁。
  • 否则,进入阻塞等待(runtime_SemacquireMutex),并更新 starving 状态。
  • 如果在饥饿模式下被唤醒,修复不一致的状态,退出饥饿模式。

d.状态更新失败:

  • 如果状态更新失败,重新获取锁状态并继续循环。
    竞态条件检测:
  • 如果启用了竞态条件检测,调用 race.Acquire 检查竞态条件。

Unlock

  • 通过原子操作解锁;
  • 倘若解锁时发现,目前参与竞争的仅有自身一个 goroutine,则直接返回即可;
  • 倘若发现锁中还有阻塞协程,则走入 unlockSlow 分支.
    unlockSlow
  • 状态一致性检查:确保解锁操作正确,锁未被意外解锁。
  • 非饥饿模式处理:
    • 如果没有等待的 goroutine 或锁状态复杂,则不需要进一步操作。
    • 如果有等待的 goroutine,尝试更新状态并唤醒一个。
  • 饥饿模式处理:
    • 直接将锁的所有权传递给下一个等待的 goroutine,并允许其立即运行。

2. sync.RWMutex 的实现原理

2.1核心机制

  • 从逻辑上,可以把 RWMutex 理解为一把读锁加一把写锁;
  • 写锁具有严格的排他性,当其被占用,其他试图取写锁或者读锁的 goroutine 均阻塞;
  • 读锁具有有限的共享性,当其被占用,试图取写锁的 goroutine 会阻塞,试图取读锁的 goroutine 可与当前 goroutine 共享读锁;
  • 综上可见,RWMutex 适用于读多写少的场景,最理想化的情况,当所有操作均使用读锁,则可实现去无化;最悲观的情况,倘若所有操作均使用写锁,则 RWMutex 退化为普通的 Mutex.

2.2读锁源码走读

RLock

  • 基于原子操作,将 RWMutex 的 readCount 变量加一,表示占用或等待读锁的 goroutine 数加一;
  • 倘若 RWMutex.readCount 的新值仍小于 0,说明有 goroutine 未释放写锁,因此将当前 goroutine 添加到读锁的阻塞队列中并阻塞挂起.

RUnlock

  • 基于原子操作,将 RWMutex 的 readCount 变量加一,表示占用或等待读锁的 goroutine 数减一;
  • 倘若 RWMutex.readCount 的新值小于 0,说明有 goroutine 在等待获取写锁,则走入 RWMutex.rUnlockSlow 的流程中.

rUnlockSlow

  • 对 RWMutex.readerCount 进行校验,倘若发现当前协程此前未抢占过读锁,或者介入读锁流程的 goroutine 数量达到上限,则抛出 fatal;
  • 基于原子操作,对 RWMutex.readerWait 进行减一操作,倘若其新值为 0,说明当前 goroutine 是最后一个介入读锁流程的协程,因此需要唤醒一个等待写锁的阻塞队列的 goroutine.

2.3写锁源码走读

Lock

  • 对 RWMutex 内置的互斥锁进行加锁操作;
  • 基于原子操作,对 RWMutex.readerCount 进行减少 -rwmutexMaxReaders 的操作;
  • 倘若此时存在未释放读锁的 gouroutine,则基于原子操作在 RWMutex.readerWait 的基础上加上介入读锁流程的 goroutine 数量,并将当前 goroutine 添加到写锁的阻塞队列中挂起.

Unlock

  • 基于原子操作,将 RWMutex.readerCount 的值加上 rwmutexMaxReaders;
  • 倘若发现 RWMutex.readerCount 的新值大于 rwmutexMaxReaders,则说明要么当前 RWMutex 未上过写锁,要么介入读锁流程的 goroutine 数量已经超限,因此直接抛出 fatal;
  • 因此唤醒读锁阻塞队列中的所有 goroutine;(可见,竞争读锁的 goroutine 更具备优势)
  • 解开 RWMutex 内置的互斥锁.
<think>嗯,用户问的是Redission实现分布式锁的原理,这个我得好好理清楚。首先,Redission是一个基于Redis的Java客户端,它提供了很多分布式相关的功能,其中分布式锁是比较常用的部分。那我得先回忆一下分布式锁的基本概念,为什么需要它,以及常见的实现方式。 分布式锁主要是为了解决在分布式系统中多个节点竞争同一资源时的同步问题。比如,在微服务架构中,多个服务实例可能需要同时修改某个共享资源,这时候就需要一种机制来确保同一时间只有一个实例能进行操作。传统的单机锁无法跨JVM工作,所以需要基于外部存储系统实现,比如Redis、Zookeeper等。 Redission实现分布式锁,核心应该还是基于Redis的特性。Redis的单线程模型和原子操作非常适合用来做分布式锁。不过,具体的实现细节可能需要更深入的分析。记得Redission的分布式锁叫RedLock吗?或者是不是RedLock是另一种算法?可能记混了。需要确认一下。 对了,Redission的分布式锁实现主要依赖Redis的SET命令,结合NX和PX参数,这样可以保证设置键的值和过期时间的原子性。NX表示只有当键不存在时才设置,PX设置过期时间,防止锁被长时间占用导致死锁。但Redission可能还做了一些优化和改进,比如自动续期、可重入等特性。 然后,锁的释放也需要保证原子性,通常是通过Lua脚本来执行,因为Lua脚本在Redis中是原子执行的。Redission应该会在释放锁的时候检查当前线程是否持有锁,只有持有锁的线程才能释放,避免误删其他线程的锁。 另外,Redission可能还实现了看门狗机制,也就是在获取锁之后,启动一个后台线程,定期检查锁的状态,并在锁快过期时自动续期。这样可以避免业务逻辑执行时间超过锁的过期时间导致的问题。不过,这个机制的具体实现细节需要再想想,比如续期的频率、如何停止续期等。 还有,Redission的锁是否可重入?应该是的,因为它内部可能维护了一个计数器,记录同一个线程获取锁的次数,每次释放锁时计数器减一,直到计数器为零时才真正释放锁。这样同一个线程可以多次获取锁,避免死锁。 不过,用户可能关心的是具体的实现步骤,比如加锁、解锁、续期的流程。需要把这些步骤详细地列出来,并解释每一步的作用。比如,加锁时使用Lua脚本设置键,同时存储线程标识和计数器;解锁时同样用Lua脚本来判断线程标识并减少计数器,如果计数器为零则删除键。 可能还需要提到Redission如何处理节点故障的情况,比如Redis集群中的主从切换是否会导致锁失效,这时候Redission是否有相应的容错机制,比如RedLock算法,但RedLock本身也存在一些争议,需要说明其优缺点。 另外,需要比较一下Redission的分布式锁和其他实现方式,比如基于Zookeeper的临时顺序节点,各自的优缺点。比如Redis实现的锁性能更高,但可能在极端情况下(如主从切换)存在安全性问题;而Zookeeper的锁更可靠,但性能稍差。 不过用户的问题主要集中在Redission的实现原理,所以应该重点放在Redission如何利用Redis的特性,以及它的机制设计上。比如如何保证原子性、如何处理锁过期、如何实现可重入和自动续期,以及如何确保释放锁的安全性。 可能还需要提到Redission的一些高级功能,比如公平锁、联锁(MultiLock)、红锁(RedLock)等,但用户问的是基本实现原理,所以这些高级特性可以简要带过,或者不提及,视情况而定。 总结一下,回答的结构应该是先介绍分布式锁的必要性,然后详细说明Redission的实现步骤,包括加锁、续期、解锁的过程,强调其如何利用Redis的原子操作和Lua脚本,以及看门狗机制和可重入设计。最后可以简要提及优缺点或与其他方案的对比,但以用户的问题为导向,重点在原理部分。</think>Redission实现分布式锁的核心原理基于Redis的原子性操作和Lua脚本,以下是分步解析: --- ### **1. 加锁过程** - **核心命令**:使用`SET key value NX PX <expireTime>`实现原子性加锁: - `NX`:仅当key不存在时才设置值(互斥性) - `PX`:设置锁的过期时间(避免死锁) - **唯一标识**:锁的`value`包含客户端唯一标识(如UUID+线程ID),确保只有加锁者能解锁。 - **可重入性**:通过计数器记录锁的重入次数(Hash结构存储`<客户端ID, 重入次数>`)。 ```lua -- Lua脚本实现原子性加锁 if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1) redis.call('pexpire', KEYS[1], ARGV[1]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1) redis.call('pexpire', KEYS[1], ARGV[1]) return 1 end return 0 ``` --- ### **2. 锁续期(看门狗机制)** - **后台线程**:加锁成功后启动守护线程(Watchdog),定期(默认10秒)检查锁是否仍持有。 - **自动续期**:若锁未被释放,重置过期时间为初始值(默认30秒),避免业务未完成锁已过期。 --- ### **3. 解锁过程** - **原子性释放**:通过Lua脚本验证客户端身份并减少重入次数,计数器归零时删除锁。 ```lua -- Lua脚本实现原子性解锁 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil end local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1) if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]) return 0 else redis.call('del', KEYS[1]) return 1 end ``` --- ### **4. 容错机制** - **Redis集群支持**:通过`MultiLock`关联多个主节点,半数以上节点加锁成功视为成功(类似RedLock算法)。 - **失败重试**:提供`tryLock()`支持超时等待和重试机制。 --- ### **优缺点分析** | **优点** | **缺点** | |-----------------------------------|-----------------------------------| | 高可用(Redis集群) | 依赖Redis持久化,极端情况可能丢锁 | | 自动续期避免业务中断 | 网络延迟可能导致锁竞争 | | 可重入设计适配复杂业务 | 需要合理设置超时时间 | --- ### **对比其他方案** - **ZooKeeper锁**:基于临时顺序节点,可靠性更高,但性能低于Redis。 - **数据库锁**:通过唯一索引实现,简单但性能差,不适合高并发。 --- 通过上述机制,Redission在分布式场景下实现了高效、可靠的锁管理,平衡了性能与安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值