CONCURRENCY CONTROL · SYSTEM MAP

并发控制体系总览

不止有锁——从进程内互斥、CAS 无锁、Go Channel/select、Actor,到数据库 MVCC、分布式锁与分布式事务,再到死锁/ABA/锁粒度。一张图看清「如何安全地让多件事同时发生」。

3大层级
2条顶层思想
5+控制范式
20+张示意图
THE BIG PICTURE
一图总览:并发控制的分类树

无论哪种机制,都逃不出两条顶层思想——悲观(PCC)乐观(OCC);再往下按「作用范围」分三层;此外还有「不共享状态」的通信/串行范式(CSP、Actor、单线程协程)。

🧩 并发控制体系 Concurrency Control
🛡️ 悲观 PCC:先加锁,假设冲突频繁
⚡ 乐观 OCC:先操作+校验,假设冲突少
↑ 横切全部三层;另有「避免共享」范式

① 进程内(线程/Goroutine)

1.1 悲观阻塞锁
  • Java JUC:synchronized / ReentrantLock / 读写锁
  • Semaphore / CountDownLatch / CyclicBarrier / Condition
  • Go sync:Mutex / RWMutex / SpinLock
  • WaitGroup / sync.Cond / sync.Once
1.2 乐观无锁 CAS
  • Java atomic 包 / Go sync/atomic
1.3 CSP 通信(Go)
  • channel + select(用通信替代共享内存)
1.4 其他模型
  • Actor 模型 / 单线程协程 asyncio

② 数据库事务并发控制

2.1 悲观路线(InnoDB)
  • S/X 共享排他锁 · 意向锁
  • 表锁 · 行锁 · 间隙锁 · 临键锁
  • MDL 锁 · 自增锁
2.2 乐观路线
  • MVCC 多版本并发 · 业务 version 乐观锁

③ 分布式并发控制

3.1 分布式悲观锁
  • Redis 锁 / ZooKeeper 锁 / etcd 锁
3.2 分布式乐观
  • 全局版本号 · CAS 条件更新
3.3 分布式事务
  • 2PC · TCC · SAGA · 可靠消息
TOP-LEVEL MINDSET
顶层思想 + 控制策略光谱

所有并发控制手段,可以按「对冲突的态度」排成一条光谱:从「最悲观、靠锁挡人」,过渡到「乐观、靠版本校验」,再到「干脆不共享、用通信/串行规避竞争」。

最悲观 避免共享 阻塞锁 读写/分段锁 CAS / 版本号 MVCC 快照 Channel / 消息 单线程串行 冲突假设由「强」到「弱」;共享程度由「强共享+锁」到「不共享」
并发控制光谱:从加锁防御 → 乐观校验 → 不共享状态
悲观 PCC

访问前先拿锁,别人被挡。安全但吞吐低、可能死锁。代表:互斥锁、行锁、分布式锁。

乐观 OCC

先操作,提交时校验版本/CAS。高吞吐,但高冲突会重试。代表:atomic、version、MVCC。

不共享(规避)

用消息传递或单线程串行,从源头消除竞争。代表:Channel、Actor、Event Loop。

LAYER 1 · IN-PROCESS
一、进程内并发控制(线程 / Goroutine)

同一进程内多线程/协程争用共享状态。Java 走 JUC 工具箱,Go 走 sync 包;Go 还额外提供 Channel 这种「用通信代替共享内存」的范式。

1.1 悲观阻塞锁:Java JUC vs Go sync

两套生态的「互斥 + 同步协调」原语对照。

☕ Java JUC

synchronizedReentrantLockReentrantReadWriteLock SemaphoreCountDownLatchCyclicBarrierCondition

🐹 Go sync

sync.Mutexsync.RWMutexSpinLock(原子自旋) sync.WaitGroupsync.Condsync.Once
互斥类

synchronized / ReentrantLock / Mutex / RWMutex 保证临界区独占;读写锁让读并发、写独占;自旋锁在极短临界区忙等省切换。这类是「共享内存 + 锁」的典型悲观方案。

同步协调原语

等待一批任务完成 / 全部到齐再继续 / 只执行一次。
WaitGroup / CountDownLatch 计数器 = 3 G1 G2 G3 Wait 每个 Done() 减 1,归零后 Wait 放行 sync.Once:首调执行,其余直接跳过
计数到零放行 · Once 保证单次

CyclicBarrier / Condition

多线程在「栅栏」处互相等齐;或条件满足才唤醒。
CyclicBarrier:到齐才冲 T1 T2 T3 Barrier 三者到齐才放行 Condition:await 等 signal
Barrier 集结 · Condition 等待/通知

1.2 乐观无锁:CAS 原子操作

Java atomic 包 / Go sync/atomic:靠 CPU 原子指令无锁更新。
定义

核心指令 cas(addr, expect, new):仅当内存值 == expect 才写入 new。失败则循环重试。无阻塞、无死锁,但高冲突会空转。

无锁CPU 原子指令ABA 风险
内存值 = 5 expect=5 → new=6 T1 成功 T2 重试
CAS:期望值匹配才更新,否则重试

★ ABA 问题(CAS 的坑)

值从 A→B→A,CAS 看到仍是 A 就误以为「没变过」。
定义

线程1读得 A,被切走;线程2把 A 改成 B 又改回 A(如指针释放后重用)。线程1的 CAS(A→C) 仍「成功」,但中间状态已变,语义可能出错。

值回退≠状态未变Java 用 AtomicStampedReference 带版本戳
共享变量 T1 读 A T2 A→B→A T1 CAS 看到A→成功! 带版本戳/标记可识别「曾变过」
ABA:值相同,过程已变
DON'T SHARE · COMMUNICATE
不共享状态:CSP / Actor / 单线程协程

「共享内存 + 锁」不是唯一答案。另一条路是:让状态不共享,靠消息传递或单线程串行来天然规避竞争。

1.3 CSP:Go channel + select

「不要通过共享内存来通信,而要通过通信来共享内存。」
定义

Channel 是 Goroutine 间的安全管道:发送方写入、接收方读出,由运行时保证同步,无需手动加锁。select 同时监听多个 channel,谁先就绪就处理谁,实现多路复用。

线程安全管道select 多路复用替代锁
G1 G2 G3 chan select 主协程 数据随消息流动,状态不共享
CSP:channel 传递数据,select 监听多路

1.4 Actor 模型

每个 Actor 私有状态 + 邮箱,只通过消息改变自己,串行处理无锁。
定义

Actor 是计算单元:内部状态私有,外部只能通过「发消息」请求它;Actor 从邮箱逐个取消息串行处理,因此自身无需锁。代表:Erlang、Akka(JVM)、Rust actix。

私有状态邮箱串行无内部锁
Actor 私有状态 A B C 消息进邮箱,Actor 串行消费 → 无锁
Actor:消息驱动、状态私有、串行处理

单线程协程 / Event Loop(如 Python asyncio)

一个线程 + 事件循环,协程主动让出(await),宏观并发、微观串行 → 根本不需要锁。
定义

asyncio 在单线程内跑多个协程:遇到 I/O 时 await 让出控制权,事件循环切到别的协程。因为没有并行执行,共享变量不会被同时改写,所以「无锁即可安全」。代价:不能真正并行(受 GIL/单线程限制),CPU 密集任务仍需多进程。

单线程协作式调度无锁非真正并行
Event Loop 协程 A 协程 B 协程 C 同一时刻只有一个协程在跑(串行)→ 共享态安全
asyncio:单线程内协程交替执行,无需锁
LAYER 2 · DATABASE (InnoDB)
二、数据库事务并发控制

在事务边界内控制并发。InnoDB 用「悲观锁 + MVCC」双管齐下:写用锁、读用快照,既安全又高并发。

2.1 悲观路线:InnoDB 锁系统

S/X + 意向锁

S 共享锁(读,可并发)、X 排他锁(写,独占);加行锁前先加意向锁 IS/IX,让表级锁快速判断冲突。

请求\已持有SX
S兼容冲突
X冲突冲突
锁粒度(含分段思想)

表锁整表;行锁索引行;间隙锁锁记录间空隙防插入;临键锁 Next-Key=行锁+前间隙(左开右闭),本质是对索引区间「分段加锁」防幻读。还有 MDL 锁(保护表结构)、自增锁(AUTO_INCREMENT 连续)。

临键锁 = 记录 + 左侧间隙 = 分段锁定索引区间
InnoDB:行/间隙/临键锁 = 索引分段加锁

2.2 乐观路线:MVCC + version

MVCC 多版本并发控制

每次更新生成新版本(带事务ID/时间戳),读操作看「快照」——读不加锁,与写互不阻塞。读已提交 / 可重复读靠快照实现。写仍用行锁保证不冲突。

业务 version 乐观锁

表里加 versionUPDATE ... SET val=?, version=version+1 WHERE id=? AND version=#{old}。返回 0 行即被改,重试。

读不加锁快照读读写不阻塞version 防丢更新
MVCC:一行数据的版本链 v1 (T1) v2 (T3) v3 (T5) 最新 快照读 T4 读 T4 看到它开始时的快照(v2),不受 v3 写影响
MVCC:快照读不被写阻塞
LAYER 3 · DISTRIBUTED
三、分布式并发控制(跨机器 / 跨服务)

资源被多个节点共享时,进程内锁失效,需借助外部协调服务或事务协议保证一致。

3.1 分布式悲观锁

Redis / ZooKeeper / etcd 实现跨节点互斥。
Redis

SET NX EX + 看门狗续期 + Lua 释放;高吞吐、AP。

ZooKeeper

临时顺序节点 + 监听前驱;CP、强一致、自动释放。

etcd

Lease + 租约 + 前缀监听;类 ZK,云原生常用。

互斥CP(强一致)需防死锁/误删

3.2 分布式乐观控制

全局版本号 / CAS 条件更新,跨服务不持锁。
定义

多个服务更新同一资源时带版本条件:UPDATE ... WHERE id=? AND version=?;或借助全局版本服务/向量时钟检测冲突。失败由业务层重试,不占用连接锁,扩展性更好。

全局版本号CAS 条件更新无长锁

3.3 分布式事务:2PC / TCC / SAGA / 可靠消息

跨多个服务要保证「要么都成功,要么都回滚」,四种主流方案各有取舍。
2PC 两阶段提交

协调者先问各参与者「能否提交?」(Vote),全 OK 再发「提交」(Commit),否则回滚。强一致,但协调者单点、同步阻塞、锁资源久。

协调者 P1 P2 阶段1 询问 YES
2PC:投票 → 提交
TCC(Try-Confirm-Cancel)

业务层三阶段:Try 预留资源、Confirm 确认提交、Cancel 释放预留。无长锁、性能好,但每个服务都要写三套逻辑,侵入性强。

Try 预留 Confirm Cancel 成功 → Confirm;任一失败 → 全部 Cancel 每个服务实现 Try/Confirm/Cancel
TCC:预留 → 确认/补偿
SAGA 长事务

把大事务拆成多个本地小事务,串执行;某步失败则反向补偿前面已完成的步骤(C1←C2←…)。适合长流程、高可用,最终一致。

T1 T2 T3✗ C2 T3 失败 → 反向补偿 C2、C1
SAGA:本地事务链 + 反向补偿
可靠消息(最终一致)

本地事务与发消息放在同一原子步骤(事务消息 / 出库表 Outbox),消息队列保证下游最终消费。旁路解耦,不阻塞主流程。

本地事务+消息 MQ 消费者 事务消息/Outbox 保证「本地成功才发得出」
可靠消息:事务消息 + MQ 最终一致
CROSS-CUTTING ISSUES
通用问题:死锁 / ABA / 锁粒度优化

无论哪一层,并发控制都要面对这几个「老大难」。理解它们才能避免踩坑。

死锁(进程内 / 数据库 / 分布式)

多个执行体互相等待对方持有的资源,形成环,谁都动不了。
四个必要条件

互斥:资源不可共享;② 占有且等待:拿着一个等另一个;③ 不可剥夺:资源不能被强行抢;④ 循环等待:T1→T2→…→T1 成环。四者同时满足才死锁。

打破任一条件即可避免加锁顺序/超时/死锁检测
T1 T2 T3 环路等待 → 全部卡死
死锁:循环等待资源

锁粒度优化:分段 / 分片 / 串行化

核心思路只有一个:让更少的请求争同一把锁。
分段锁

把结构分 N 段,每段独立锁(如 JDK7 ConcurrentHashMap 16 段)。并发度≈段数。

分片 Sharding

按 key 哈希路由到不同分片/库表,各分片锁互不干扰,水平扩展。

串行化

把竞争操作丢进单队列/Actor/单线程,强制串行 → 无需锁(回到光谱最右端)。

一把大锁全阻塞 段0🔒 段1 段2🔒 段3 单队列串行处理无锁 大锁 → 分段/分片 → 串行化(争用越来越少)
锁粒度优化:缩小争用范围
⚠️
避坑口诀:① 加锁务必定统一顺序防死锁,并加超时/看门狗;② CAS 用带版本戳防 ABA;③ 能用分段/行锁就别用表锁;④ 分布式锁释放必须 Lua 原子;⑤ 跨服务一致性优先选最终一致(SAGA/消息)而非强一致 2PC,除非真需要。