别再背幂等三板斧了:分布式系统真正危险的是这些边界条件
学习资料
--------------------------------------------------
一、请求还没处理完,重复请求撞进来怎么办?
核心冲突:处理中的状态与 Lease 机制
很多系统把幂等简化为“数据库唯一键 + 插入”,默认重复请求在前一个请求完全结束后到达。但实际中第二个请求常在第一个请求处于 In-Flight(处理中)时到达(例如等待第三方网关、慢 SQL)。
如果直接抛 Duplicate Key 并返回失败,客户端可能误认为没执行而重试;如果直接返回成功,而后端事务回滚,会造成“显示成功但未扣款”的资损。
常见工程范式
二、系统怎么知道这是同一个请求?
核心冲突:请求身份(Identity)与 Token 误区
Token 是 Identity 的一种载体,但不能单独作为唯一状态源。
状态机的收敛链路
引入 Identity 后,必须用状态机管理其生命周期。UNKNOWN 不是终态,而是需要继续收敛的中间态。
INIT (原子性占位)
|
v
PROCESSING
/ | \
SUCCESS UNKNOWN FAILED
\ | /
(异步对账/查询收敛)
--------------------------------------------------
三、本地 DB 事务保不了远端 RPC
核心冲突:复杂网络下的状态收敛(Consistency)
外部 RPC 超时导致的状态倒置
常见错误做法:在本地事务内调用外部 RPC,并把超时当失败:
推荐实践:优先持久化本地状态,异步对账收敛
条件状态更新(CAS)
多线程竞争同一状态机时,必须使用带版本号或租约的条件更新,防止旧状态覆盖新状态。例如:
UPDATE idempotency_record
SET state = 'SUCCESS', response_body = '...', version = version + 1, updated_at = NOW()
WHERE identity_id = 'ORD_123456'
AND state = 'PROCESSING'
AND (lease_expire_at > NOW() OR owner_id = 'worker-node-01');
--------------------------------------------------
终局思考:Exactly-Once 究竟意味着什么?
物理层面纯粹的 Exactly-Once Delivery 在存在网络分区、重试和节点故障时不可实现。
Exactly Once Effect 依赖多要素收敛:
一、请求还没处理完,重复请求撞进来怎么办?
核心冲突:处理中的状态与 Lease 机制
很多系统把幂等简化为“数据库唯一键 + 插入”,默认重复请求在前一个请求完全结束后到达。但实际中第二个请求常在第一个请求处于 In-Flight(处理中)时到达(例如等待第三方网关、慢 SQL)。
如果直接抛 Duplicate Key 并返回失败,客户端可能误认为没执行而重试;如果直接返回成功,而后端事务回滚,会造成“显示成功但未扣款”的资损。
常见工程范式
- 服务端挂起/短轮询(Server Waiting/Polling):服务端内存或 Redis 等待前一个请求完成并复用结果(有 TTL)。
- 明确告知客户端等候(Still Processing):返回特定状态码(如 202 或业务状态 STILL_PROCESSING),由客户端按指数退避查询。
- 租约占用:把 PROCESSING 视为对该 Identity 的临时占用,设置合理 lease(秒级或分钟级)。
- 超时抢占:超时后,后续请求可通过带 lease 条件的 CAS(比较 lease_expire_at 或 version)获得处理权,接管执行。
二、系统怎么知道这是同一个请求?
核心冲突:请求身份(Identity)与 Token 误区
Token 是 Identity 的一种载体,但不能单独作为唯一状态源。
- 先删 Token 再做业务:若 RPC 超时,用户重试时 Token 已删,导致误拦截或无法支付。
- 先做业务再删 Token:并发请求在业务执行窗口内可能穿透,造成重复扣款。
状态机的收敛链路
引入 Identity 后,必须用状态机管理其生命周期。UNKNOWN 不是终态,而是需要继续收敛的中间态。
INIT (原子性占位)
|
v
PROCESSING
/ | \
SUCCESS UNKNOWN FAILED
\ | /
(异步对账/查询收敛)
--------------------------------------------------
三、本地 DB 事务保不了远端 RPC
核心冲突:复杂网络下的状态收敛(Consistency)
外部 RPC 超时导致的状态倒置
常见错误做法:在本地事务内调用外部 RPC,并把超时当失败:
- 开启本地事务,更新幂等表为 PROCESSING
- 调用外部 RPC(网络 ACK 超时)
- 抛异常,触发本地事务回滚
推荐实践:优先持久化本地状态,异步对账收敛
- 先落盘 PROCESSING:在发起 RPC 前,用独立事务将状态置为 PROCESSING 并提交,锁住并发入口。
- RPC 超时时更新为 UNKNOWN,由后台对账或主动 Query 收敛为 SUCCESS 或 FAILED。
条件状态更新(CAS)
多线程竞争同一状态机时,必须使用带版本号或租约的条件更新,防止旧状态覆盖新状态。例如:
UPDATE idempotency_record
SET state = 'SUCCESS', response_body = '...', version = version + 1, updated_at = NOW()
WHERE identity_id = 'ORD_123456'
AND state = 'PROCESSING'
AND (lease_expire_at > NOW() OR owner_id = 'worker-node-01');
--------------------------------------------------
终局思考:Exactly-Once 究竟意味着什么?
物理层面纯粹的 Exactly-Once Delivery 在存在网络分区、重试和节点故障时不可实现。
Exactly Once Effect 依赖多要素收敛:
- At-Least-Once Delivery(网络重试)
- Unique Identity(精确识别)
- Lease Ownership & State Machine(锁住并发与中间态)
- Conditional Transition(防止状态倒置)
已获得 15 大米


+2
8
共0条回复
✨ 您正在体验新版论坛UI

