← [ BACK_TO_NOTES ]

BodySense 数据库第 15 课:并发控制模式——Pessimistic、Optimistic、Constraint 与 Idempotency

把锁、revision/CAS、UNIQUE constraint、idempotency key 与 lease 作为不同并发控制工具,映射 BodySense BodyState revision、Job claim 与 durable workflow。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

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 / 场景首先考虑
不允许重复 keyUNIQUE / EXCLUDE
热点 row 短临界区FOR UPDATE
冲突低、计算可重试version / CAS
复杂 predicate serial semanticsSerializable + retry
网络重复请求Idempotency Key
长时间后台 ownershipLease / 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]

  1. “邮箱唯一”更适合 service 先 SELECT 还是数据库 UNIQUE 做最终裁决?
  2. 冲突很少但业务计算耗时较长,长时间 FOR UPDATE 是否一定最好?
  3. HTTP timeout 后客户端重试,为什么 transaction 本身不能防重复请求?
  4. Job 执行 10 分钟,为什么不应该持有 10 分钟数据库 transaction + row lock?
  5. 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 场景分别选机制并说明理由:

  1. BodyState.current_revision;
  2. 同一 source evidence 不得重复;
  3. Worker claim 一个 pending Job。

L4 Gate

面对新并发需求,能比较 constraint / pessimistic lock / optimistic version / idempotency / lease,而不是默认选“加一个 mutex/数据库锁”。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本