THE BIG PICTURE
一图总览:锁的分类树
无论哪种锁,底层只有两条并发控制思想——悲观(PCC) 与 乐观(OCC),它们横切下面三个层级。先看整体结构,后面逐层展开。
🔒 锁体系 Lock System
🛡️ 悲观并发控制 PCC
先加锁,假设冲突频繁
⚡ 乐观并发控制 OCC
先操作,提交时校验,假设冲突少
↑ 两条思想横切以下全部三层
① 进程内锁
悲观锁(阻塞)
- 互斥锁 Mutex
- 可重入锁 ReentrantLock
- 读写锁 RWMutex
- 自旋锁 SpinLock
- 信号量 Semaphore
- synchronized(JVM 锁升级)
乐观锁(无锁 CAS)
- 原子操作类 Atomic / sync.atomic
② 数据库锁(InnoDB)
悲观锁(物理锁)
- 共享锁 S / 排他锁 X
- 粒度:表锁·行锁·间隙锁·临键锁
- 辅助:意向锁·MDL·自增锁
③ 分布式锁
悲观锁(跨进程)
- Redis:NX+EX / 看门狗 / Redlock
- ZooKeeper / etcd
- 数据库实现
TOP-LEVEL MINDSET
顶层思想:悲观并发控制 vs 乐观并发控制
所有具体锁都能归到这两条思想之下。区别在于「何时处理冲突」——悲观者在进入临界区前就锁住,乐观者先放手去做、提交时再验证。
🛡️ 悲观并发控制 PCC
Pessimistic Concurrency Control —— 假设冲突一定会发生,先加锁把别人挡在门外再操作。
定义
访问数据前先获取锁;持有锁期间其他线程/事务被阻塞,直到释放。最常见的「互斥」「行锁」「分布式锁」都属此类。
先加锁后访问
冲突高时安全
吞吐偏低
可能死锁
悲观路径:进入临界区前必须先拿到锁
⚡ 乐观并发控制 OCC
Optimistic Concurrency Control —— 假设冲突很少,先直接操作,提交时再校验版本/条件。
定义
不加物理锁,而是读取时记录版本,写回时用「版本号/CAS」判断期间是否被改过;若冲突则重试或放弃。CAS、version 乐观锁、分布式乐观锁都属此类。
先操作后校验
冲突少时高吞吐
无死锁
高冲突会重试
乐观路径:提交时才验证版本是否被改
💡
一句话区分:悲观锁「怕别人抢,所以先锁门」;乐观锁「觉得没人抢,进门干完活再确认门没被换」。前者靠锁保证安全,后者靠版本/条件保证正确。
LAYER 1 · IN-PROCESS
一、进程内锁(单进程 · 多线程)
同一进程内、多个线程争用共享资源时使用。语言/运行时原生提供(Java/Go/Python/PHP 均有对应实现),不跨进程、不跨机器,性能最高。
互斥锁 Mutex
最基础的悲观锁:同一时刻只允许一个线程进入临界区。
定义
Mutex = Mutual Exclusion。拿不到锁的线程会被挂起排队(阻塞),让出 CPU 直到锁释放。Go 的 sync.Mutex、Java 的 Lock、pthread 的 pthread_mutex 都是它。
独占阻塞排队保护共享变量
Mutex:独占 + 阻塞排队
读写锁 RWMutex
把「读」和「写」分开:读共享、写独占,专治「读多写少」。
定义
Read-Write Mutex。多个读线程可同时持有读锁;一旦有写线程,则读写都互斥。Go 的 sync.RWMutex、Java 的 ReentrantReadWriteLock。
读读并行读写互斥写写互斥
RWMutex:读可并行,写独占
自旋锁 SpinLock
拿不到锁时不睡觉,而是「空转」循环重试——换 CPU 时间换低延迟。
定义
Spin Lock。线程在 while(!cas(lock)) 上空转,不触发上下文切换。适合临界区极短、多核场景;否则空转浪费 CPU。内核、JVM 轻量级锁底层常用。
忙等无上下文切换占 CPU临界区要极短
自旋 vs 阻塞:延迟与 CPU 的取舍
信号量 Semaphore
不是「锁一把」,而是「限 N 个」——控制同时访问的线程数量。
定义
维护一个许可计数器 permit。acquire 减 1、release 加 1;为 0 时阻塞。当 N=1 时退化为互斥锁。常用于连接池、限流。Java Semaphore、Go chan 限流。
许可计数可设 N 个可跨线程释放
Semaphore:计数许可,N 个并发
可重入锁 ReentrantLock
同一线程可多次加同一把锁而不死锁——靠「持有者+计数」实现。
定义
Reentrant = 可重入。内部记录 owner 线程与 holdCount,同一线程重复 lock 只增计数,解锁到 0 才真正释放。避免递归/嵌套调用自锁。比 synchronized 更灵活:可公平、可中断、可超时、可绑定多个 Condition。
防自死锁可公平/中断多条件变量
synchronized(JVM 锁升级)
Java 关键字级互斥,锁会按竞争强度「自动升级」以省开销。
定义
JVM 对 synchronized 的优化:无竞争时用偏向锁(记录线程 ID),轻度竞争升级为轻量级锁(CAS 自旋),竞争激烈才膨胀为重量级锁(OS 互斥、阻塞)。即「无锁→偏向→轻量→重量」四态。
自动升级无锁→偏向→轻量→重量
synchronized 锁升级路径
乐观锁:原子操作类(CAS)
无锁(lock-free)方案:靠 CPU 原子指令 Compare-And-Swap 实现线程安全。
定义
AtomicInteger / sync/atomic 等。核心是一条硬件指令:cas(addr, expect, new) —— 仅当内存值等于 expect 才写入 new,返回是否成功。失败则循环重试(自旋)。无阻塞、无死锁。
无锁CPU 原子指令ABA 问题高冲突自旋
CAS:期望值匹配才更新,否则重试
并发工具:条件变量 Condition
和互斥锁配合,实现「条件不满足就等,满足了再醒」的精确等待。
定义
Condition = 等待队列。线程先持有互斥锁,条件不满足时 await() 释放锁并挂起;另一线程 signal() 唤醒它,它重新抢锁。典型用于生产者-消费者、阻塞队列。是「锁 + 等待」的组合件。
配合 Mutex等待/通知生产者消费者
Condition:wait / signal 的协作
LAYER 2 · DATABASE (InnoDB)
二、数据库锁(以 InnoDB 为例)
在数据库引擎层实现的锁,作用于「行/表」,由事务持有。悲观锁是引擎的物理锁,乐观锁是业务层用版本号模拟。
共享锁 S / 排他锁 X
读锁与写锁的兼容性,决定事务能否并发。
定义
S 共享锁(读):SELECT ... LOCK IN SHARE MODE,大家都能读、都不能写。X 排他锁(写):SELECT ... FOR UPDATE / UPDATE/DELETE/INSERT,独占,别人既不能读锁也不能写。
| 请求 \ 已持有 | S | X |
| S 共享 | 兼容 ✓ | 冲突 ✗ |
| X 排他 | 冲突 ✗ | 冲突 ✗ |
S-S 可读并发,其余相互阻塞
锁粒度:表 / 行 / 间隙 / 临键
InnoDB 在「索引」上加锁,粒度从小到大,临键锁本质是「分段」思想的体现。
定义
• 表锁:锁整张表,开销小但并发差。
• 行锁:锁索引上的某行(Record Lock)。
• 间隙锁 Gap:锁两条记录之间的「空隙」,防止插入——解决幻读。
• 临键锁 Next-Key = 行锁 + 向前间隙锁(左开右闭),是 InnoDB 默认行锁算法,可看作对索引区间的「分段加锁」。
临键锁:记录 + 间隙 = 分段锁定索引区间
辅助锁:意向 / MDL / 自增
为了「快速判断要不要加表锁」「保护元数据」「高效自增」而存在的配套锁。
定义
• 意向锁 IS/IX:事务给行加 S/X 前,先给表加意向锁,让表级锁能快速判断是否有行级冲突,不必逐行扫。
• MDL 元数据锁:对表结构(DDL)加锁,防止读写时表结构被改。
• 自增锁 AUTO-INC:保证 AUTO_INCREMENT 插入时序号连续不冲突。
| 请求 \ 已持有 | IS | IX | S | X |
| IS | ✓ | ✓ | ✓ | ✗ |
| IX | ✓ | ✓ | ✗ | ✗ |
意向锁之间兼容,与表级 S/X 才冲突
乐观锁:version 版本号
不加物理锁,用「版本号/时间戳」在 UPDATE 时做条件校验。
定义
表里加 version 字段。更新时:UPDATE t SET val=?, version=version+1 WHERE id=? AND version=#{old}。若返回 0 行说明期间被改,需重试。避免长事务长时间持有行锁。
无物理锁版本号/时间戳防丢失更新适合读多写少
version 乐观锁:后提交者因版本不符而失败重试
LAYER 3 · DISTRIBUTED
三、分布式锁(跨机器 · 跨服务)
当资源被多个进程/节点共享,进程内锁失效,需要借助外部协调服务(Redis / ZK / etcd / DB)来实现互斥。
Redis 分布式锁:NX + EX / 看门狗 / Redlock
最常用方案:用 SET key val NX EX 原子加锁,靠过期时间防死锁,靠「看门狗」自动续期。
基础加锁
SET lock:order uuid NX EX 30。仅当 key 不存在才设置成功 → 拿到锁;带过期时间避免客户端宕机后锁永不释放。释放须用 Lua 脚本保证「判断 uuid + 删除」原子,防止误删别人的锁。
看门狗 Watchdog
业务没执行完锁快过期时,后台定时线程自动续期(如 Redisson 每 1/3 TTL 续一次),直到业务释放。解决「锁提前过期」问题。
Redis 单节点加锁 / 释放
Redlock(多节点容错)
为降低单点 Redis 宕机丢锁的风险:向 N 个独立 Redis 主节点依次加锁,当在 多数(≥ N/2+1) 节点上成功且总耗时小于锁有效期,才算真正拿到锁。属于 AP 系统的折中强一致方案,争议较大、需谨慎评估时钟与运维成本。
Redlock:多数派原则
ZooKeeper / etcd 分布式锁
基于「临时顺序节点 + 监听前驱」实现,CP 系统,天然防羊群效应。
定义
在锁目录下创建临时顺序(ephemeral_sequential)节点;序号最小的节点获得锁。其他节点只监听「比自己小一号」的前驱,前驱释放(会话断开/删除)才被唤醒去抢。会话断开临时节点自动消失 → 不会死锁。
CP 强一致无羊群效应自动释放性能略低于 Redis
ZK/etcd:临时顺序节点 + 监听前驱
数据库 / 分布式乐观锁
兜底方案:用 DB 唯一键或版本号实现跨服务互斥。
DB 实现(悲观)
建一张锁表,INSERT 唯一键(如 method_name)抢锁,释放即 DELETE;或用 SELECT ... FOR UPDATE 行锁。简单但强依赖 DB,并发与可用受限,仅适合低频场景。
分布式乐观锁
与单库 version 思路一致,但跨多个服务/节点更新同一资源时带版本条件:UPDATE ... WHERE id=? AND version=?。失败时由业务层重试,不持有连接锁,扩展性更好。
DB 唯一键抢锁跨服务 version CAS
KEY IDEA · SEGMENTED LOCK
分段锁:把「一把大锁」拆成「多把小锁」
分段锁本身是一种降低锁粒度的设计思想,不是某种具体锁,而是悲观锁在高并发容器上的工程化运用。核心:把数据分桶,每个桶一把独立锁,线程只锁自己要用的那段。
原理:分而治之
定义
把共享数据结构(如哈希表)分成 N 个段(Segment),每段持有一把独立锁。线程操作 key 时,只锁该 key 所属的分段,其他分段仍可并发。JDK 7 的 ConcurrentHashMap 就是 16 个 Segment;JDK 8 后改为更细的「数组 + CAS + 节点锁(synchronized)」。
降低锁粒度提升并发度JDK7 ConcurrentHashMap
与临键锁的呼应
思想同源
分段思想一脉相承:InnoDB 的临键锁把索引「分段」加锁防幻读;分布式用分片锁 / 分库分表锁降低冲突;容器用分段锁提升并发。本质都是「缩小锁保护范围,让更多操作并行」。
分段锁:锁范围从「整表」缩小到「单段」
HOW TO CHOOSE
如何选型:一张决策图
先判断「冲突范围」在哪一層,再决定用悲观还是乐观,最后挑具体实现。
⚠️
常见坑:① 乐观锁务必用「版本号/条件」而非常量,否则失效;② Redis 锁释放必须用 Lua,否则可能删掉别人的锁;③ 锁的粒度越粗并发越低,能用分段/行锁就别用表锁;④ 悲观锁要记得加超时与重试,避免死锁拖垮系统。