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(用通信替代共享内存)
② 数据库事务并发控制
2.1 悲观路线(InnoDB)
- S/X 共享排他锁 · 意向锁
- 表锁 · 行锁 · 间隙锁 · 临键锁
- MDL 锁 · 自增锁
2.2 乐观路线
- MVCC 多版本并发 · 业务 version 乐观锁
③ 分布式并发控制
3.1 分布式悲观锁
- Redis 锁 / ZooKeeper 锁 / etcd 锁
TOP-LEVEL MINDSET
顶层思想 + 控制策略光谱
所有并发控制手段,可以按「对冲突的态度」排成一条光谱:从「最悲观、靠锁挡人」,过渡到「乐观、靠版本校验」,再到「干脆不共享、用通信/串行规避竞争」。
并发控制光谱:从加锁防御 → 乐观校验 → 不共享状态
悲观 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 保证临界区独占;读写锁让读并发、写独占;自旋锁在极短临界区忙等省切换。这类是「共享内存 + 锁」的典型悲观方案。
同步协调原语
等待一批任务完成 / 全部到齐再继续 / 只执行一次。
计数到零放行 · Once 保证单次
CyclicBarrier / Condition
多线程在「栅栏」处互相等齐;或条件满足才唤醒。
Barrier 集结 · Condition 等待/通知
1.2 乐观无锁:CAS 原子操作
Java atomic 包 / Go sync/atomic:靠 CPU 原子指令无锁更新。
定义
核心指令 cas(addr, expect, new):仅当内存值 == expect 才写入 new。失败则循环重试。无阻塞、无死锁,但高冲突会空转。
无锁CPU 原子指令ABA 风险
CAS:期望值匹配才更新,否则重试
★ ABA 问题(CAS 的坑)
值从 A→B→A,CAS 看到仍是 A 就误以为「没变过」。
定义
线程1读得 A,被切走;线程2把 A 改成 B 又改回 A(如指针释放后重用)。线程1的 CAS(A→C) 仍「成功」,但中间状态已变,语义可能出错。
值回退≠状态未变Java 用 AtomicStampedReference 带版本戳
ABA:值相同,过程已变
DON'T SHARE · COMMUNICATE
不共享状态:CSP / Actor / 单线程协程
「共享内存 + 锁」不是唯一答案。另一条路是:让状态不共享,靠消息传递或单线程串行来天然规避竞争。
1.3 CSP:Go channel + select
「不要通过共享内存来通信,而要通过通信来共享内存。」
定义
Channel 是 Goroutine 间的安全管道:发送方写入、接收方读出,由运行时保证同步,无需手动加锁。select 同时监听多个 channel,谁先就绪就处理谁,实现多路复用。
线程安全管道select 多路复用替代锁
CSP:channel 传递数据,select 监听多路
1.4 Actor 模型
每个 Actor 私有状态 + 邮箱,只通过消息改变自己,串行处理无锁。
定义
Actor 是计算单元:内部状态私有,外部只能通过「发消息」请求它;Actor 从邮箱逐个取消息串行处理,因此自身无需锁。代表:Erlang、Akka(JVM)、Rust actix。
私有状态邮箱串行无内部锁
Actor:消息驱动、状态私有、串行处理
单线程协程 / Event Loop(如 Python asyncio)
一个线程 + 事件循环,协程主动让出(await),宏观并发、微观串行 → 根本不需要锁。
定义
asyncio 在单线程内跑多个协程:遇到 I/O 时 await 让出控制权,事件循环切到别的协程。因为没有并行执行,共享变量不会被同时改写,所以「无锁即可安全」。代价:不能真正并行(受 GIL/单线程限制),CPU 密集任务仍需多进程。
单线程协作式调度无锁非真正并行
asyncio:单线程内协程交替执行,无需锁
LAYER 2 · DATABASE (InnoDB)
二、数据库事务并发控制
在事务边界内控制并发。InnoDB 用「悲观锁 + MVCC」双管齐下:写用锁、读用快照,既安全又高并发。
2.1 悲观路线:InnoDB 锁系统
S/X + 意向锁
S 共享锁(读,可并发)、X 排他锁(写,独占);加行锁前先加意向锁 IS/IX,让表级锁快速判断冲突。
锁粒度(含分段思想)
表锁整表;行锁索引行;间隙锁锁记录间空隙防插入;临键锁 Next-Key=行锁+前间隙(左开右闭),本质是对索引区间「分段加锁」防幻读。还有 MDL 锁(保护表结构)、自增锁(AUTO_INCREMENT 连续)。
InnoDB:行/间隙/临键锁 = 索引分段加锁
2.2 乐观路线:MVCC + version
MVCC 多版本并发控制
每次更新生成新版本(带事务ID/时间戳),读操作看「快照」——读不加锁,与写互不阻塞。读已提交 / 可重复读靠快照实现。写仍用行锁保证不冲突。
业务 version 乐观锁
表里加 version:UPDATE ... SET val=?, version=version+1 WHERE id=? AND version=#{old}。返回 0 行即被改,重试。
读不加锁快照读读写不阻塞version 防丢更新
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),否则回滚。强一致,但协调者单点、同步阻塞、锁资源久。
2PC:投票 → 提交
TCC(Try-Confirm-Cancel)
业务层三阶段:Try 预留资源、Confirm 确认提交、Cancel 释放预留。无长锁、性能好,但每个服务都要写三套逻辑,侵入性强。
TCC:预留 → 确认/补偿
SAGA 长事务
把大事务拆成多个本地小事务,串执行;某步失败则反向补偿前面已完成的步骤(C1←C2←…)。适合长流程、高可用,最终一致。
SAGA:本地事务链 + 反向补偿
可靠消息(最终一致)
本地事务与发消息放在同一原子步骤(事务消息 / 出库表 Outbox),消息队列保证下游最终消费。旁路解耦,不阻塞主流程。
可靠消息:事务消息 + MQ 最终一致
CROSS-CUTTING ISSUES
通用问题:死锁 / ABA / 锁粒度优化
无论哪一层,并发控制都要面对这几个「老大难」。理解它们才能避免踩坑。
死锁(进程内 / 数据库 / 分布式)
多个执行体互相等待对方持有的资源,形成环,谁都动不了。
四个必要条件
① 互斥:资源不可共享;② 占有且等待:拿着一个等另一个;③ 不可剥夺:资源不能被强行抢;④ 循环等待:T1→T2→…→T1 成环。四者同时满足才死锁。
打破任一条件即可避免加锁顺序/超时/死锁检测
死锁:循环等待资源
锁粒度优化:分段 / 分片 / 串行化
核心思路只有一个:让更少的请求争同一把锁。
分段锁
把结构分 N 段,每段独立锁(如 JDK7 ConcurrentHashMap 16 段)。并发度≈段数。
分片 Sharding
按 key 哈希路由到不同分片/库表,各分片锁互不干扰,水平扩展。
串行化
把竞争操作丢进单队列/Actor/单线程,强制串行 → 无需锁(回到光谱最右端)。
锁粒度优化:缩小争用范围
⚠️
避坑口诀:① 加锁务必定统一顺序防死锁,并加超时/看门狗;② CAS 用带版本戳防 ABA;③ 能用分段/行锁就别用表锁;④ 分布式锁释放必须 Lua 原子;⑤ 跨服务一致性优先选最终一致(SAGA/消息)而非强一致 2PC,除非真需要。