BodySense 数据库第 15 课:并发控制模式——Pessimistic、Optimistic、Constraint 与 Idempotency
把锁、revision/CAS、UNIQUE constraint、idempotency key 与 lease 作为不同并发控制工具,映射 BodySense BodyState revision、Job claim 与 durable workflow。
[!info] related notes
- 课程 MOC:BodySense 数据库工程深化课程 MOC
- 前置:第 14 课
- 知识:幂等性、分布式事务
BodySense 数据库第 15 课:并发控制模式——Pessimistic、Optimistic、Constraint 与 Idempotency
[!abstract] 本课目标 并发控制不是“统一加锁”。不同冲突形态需要不同工具:谁是最终裁决者?失败时是等待、冲突还是重试?
从零建立概念:并发控制是在选择“冲突发生时谁来裁决”
上一课学的是 Lock 机制。
这一课把可用手段放到一张决策图里:
Database Constraint
Pessimistic Lock
Optimistic Version / CAS
Serializable Transaction
Idempotency
Lease / Claim
它们不是同一种技术的不同品牌,而是解决不同冲突形状。
先定义 Business Invariant
不要先问:
用乐观锁还是悲观锁?
先写一句:
系统永远不允许出现什么状态?
例如:
同一 email 只能一个 user
同一 user 的 BodyState revision 不能倒退
一个 pending Job 同时只能被一个 Worker claim
同一个 Idempotency-Key 只能产生一个业务订单
机制只是为了保护 invariant。
Pessimistic Concurrency:先假设会冲突
典型:
lock resource
→ read current state
→ decide
→ update
→ commit / unlock
“Pessimistic” 的意思是:
我不相信别人不会同时改,所以先把冲突资源锁住。
优点:
critical section 直观
single winner 清晰
不需要大量 retry
代价:
blocking
deadlock
connection/lock hold time
吞吐受热点资源影响
适合:
高冲突
临界区短
资源可明确锁定
Optimistic Concurrency:先做,提交时检查有没有变
典型 Version/CAS:
read revision = 7
↓
compute changes
↓
UPDATE ...
WHERE id=? AND revision=7
SET revision=8
如果:
affected rows = 1
→ success
affected rows = 0
→ someone changed it
→ conflict
CAS = Compare-And-Swap / Compare-And-Set 的思想就是:
只有当前值仍等于我之前观察到的 expected value,才允许写新值。
优点:
不需要长时间持锁
冲突少时并发好
代价:
冲突后要重算/重试
可能导致用户 conflict
高冲突时 retry 浪费大
Constraint as Concurrency Control:让数据库直接裁决“不允许共存”
如果 invariant 是:
这一组 key 只能有一行
最自然的工具常常不是锁,而是:
UNIQUE (...)
为什么?
因为两个 transaction 即使同时:
SELECT → missing
最终只有数据库能在并发提交时原子判断:
two rows cannot coexist
应用层 if not exists 可以改善错误提示,但不能替代 UNIQUE。
Upsert:冲突本身也可以成为业务分支
PostgreSQL:
INSERT ...
ON CONFLICT (...) DO UPDATE ...;
把:
try insert
→ uniqueness conflict
→ alternate action
收敛为数据库原子语义。
但仍要明确:
哪个 UNIQUE constraint 定义 conflict identity
update 是否幂等
并发下最终希望哪个值获胜
Idempotency:解决“同一意图被执行多次”
并发和网络重试经常制造:
same business request
arrives more than once
例如用户点击一次,但 response 丢失:
POST create
→ server commits
→ response lost
→ client retries
没有 idempotency:
two rows / two charges / two jobs
有 Idempotency-Key:
request identity = abc123
服务端可以:
first time
→ execute + save result
retry same key
→ return same result
→ no duplicate effect
数据库常用:
UNIQUE(idempotency_key)
+ transaction
作为最终保护。
Lease:为什么后台任务不能靠长 Transaction Lock 执行十分钟
Job Worker:
claim task
→ perform external work 10 min
→ complete
如果一直:
BEGIN
FOR UPDATE
hold row lock 10min
会:
hold DB connection
hold lock
hold snapshot
hurt failover/recovery
更常见:
short transaction
→ update job owner/lease_until/status
→ COMMIT
worker executes outside transaction
heartbeat / extend lease
finish in another short transaction
Lease 的核心语义
owner = worker-7
lease_until = T
如果 Worker 死亡:
now > lease_until
→ another worker may reclaim
于是系统不会永久卡死。
Lease 立即带来 Duplicate Execution 问题
场景:
worker A lease expires
但 A 其实还在慢慢执行
worker B reclaims
and starts same job
现在 A/B 都可能产生外部副作用。
所以 Job system 还需要:
idempotent side effect
fencing token / generation
ownership re-check
unique business effect
不能因为“有 lease”就声称 exactly-once。
BodyState 为什么同时出现 Lock + Revision
当前 BodySense:
SELECT state row FOR UPDATE
+
expectedRevision check
+
persist next revision
看似重复,其实语义不同:
FOR UPDATE
→ serialize concurrent mutations now
expectedRevision
→ detect caller based decision on stale domain revision
revision history
→ durable business audit
这就是一个机制组合案例。
最后形成选择表
| Invariant / 场景 | 首先考虑 |
|---|---|
| 不允许重复 key | UNIQUE / EXCLUDE |
| 热点 row 短临界区 | FOR UPDATE |
| 冲突低、计算可重试 | version / CAS |
| 复杂 predicate serial semantics | Serializable + retry |
| 网络重复请求 | Idempotency Key |
| 长时间后台 ownership | Lease / Claim |
实际系统可以组合,但每多一层都要解释它独立保护什么。
1. Pessimistic Lock
先锁住资源
→ 再读取/决定/写入
适合冲突概率高、临界区短、必须单赢家的状态转换。
2. Optimistic Concurrency
read version = 7
→ compute
→ UPDATE ... WHERE version = 7
→ rows affected = 0 => conflict
适合冲突相对少、计算可重试,避免长时间持锁。
BodySense 的 expectedRevision + locked current revision 同时体现了显式版本语义和数据库互斥。
3. Constraint as Concurrency Control
很多“不允许重复”的规则最优雅的裁决者是数据库:
UNIQUE(user_id, source_key)
两个并发 transaction 可以竞争,最终数据库保证只有合法状态 commit。应用负责把 unique violation 翻译为业务结果。
4. Idempotency
网络重试场景:
客户端不知道第一次请求是否成功
→ 重试
如果服务端用 idempotency key / unique identity 识别同一业务意图,就能避免重复 side effect。
5. Lease / Claim
Job worker 常见:
claim pending job
→ owner/lease_until
→ execute
→ heartbeat/complete
它解决的是长期异步 ownership,不应简单等同数据库 row lock,因为任务执行可能远长于适合持 transaction 的时间。
Prediction
[!question]
- “邮箱唯一”更适合 service 先 SELECT 还是数据库 UNIQUE 做最终裁决?
- 冲突很少但业务计算耗时较长,长时间 FOR UPDATE 是否一定最好?
- HTTP timeout 后客户端重试,为什么 transaction 本身不能防重复请求?
- Job 执行 10 分钟,为什么不应该持有 10 分钟数据库 transaction + row lock?
- Optimistic conflict 是系统 bug 吗?
[!success]- Answer / 解析 1. UNIQUE。 SELECT 可用于友好提示,但并发最终裁决必须由数据库 constraint。
2. 不一定。 Optimistic version/CAS 可以缩短持锁时间,让冲突者在 commit/update 时发现版本变化。
3. 重试是新的 transaction;第一次可能已经 commit。需要跨请求 identity/idempotency 识别同一意图。
4. 会长期占连接、snapshot、锁,影响并发和 VACUUM;应使用 durable lease/claim 状态。
5. 不一定。 它可能是并发协议的正常结果,应用应该清晰返回冲突或安全重试。
Lab
为三个 BodySense 场景分别选机制并说明理由:
BodyState.current_revision;- 同一 source evidence 不得重复;
- Worker claim 一个 pending Job。
L4 Gate
面对新并发需求,能比较 constraint / pessimistic lock / optimistic version / idempotency / lease,而不是默认选“加一个 mutex/数据库锁”。