架构设计
幂等方案怎么选,先看业务原子性边界,不是先看技术栈
request_id、唯一键、本地事务、状态机幂等表和 Redis 过程控制各有自己的成立边界。真正先要看清的,不是团队会什么,而是这次业务到底在哪一刻才算真正成立。
前面我已经分别拆过两层问题:
写到这里以后,我又碰到一个更前面的分叉:
为什么有些业务用 request_id + 唯一键 + 事务 就够了,有些业务却非得上状态机幂等表,甚至最后还会继续长到 Redis 票据、分布式锁和 fencing token 那一层?
我不想把这个问题理解成“哪个方案更高级”。分水岭不在技术栈,在另一件事:
这次业务成立的边界,到底落在哪。
我现在区分幂等方案,先看业务原子性边界
如果把不同方案放在一起看,我后来更愿意用一种很朴素的方式去区分它们。
边界还在本地事务里
最优方案通常就是:
request_id- 唯一键
- 本地事务捆绑
边界已经跨出本地事务
就必须开始显式记录过程状态,比如:
PROCESSINGCOMPLETEDFAILED_RETRYABLEFAILED_PERMANENT
也就是所谓状态机幂等表。
如果流程再复杂
比如:
- 步骤长
- 外部调用多
- 需要接管恢复
- 并发重试频繁
- 还要防旧执行流覆盖新结果
那系统就会继续进入分布式过程控制的世界,用 Redis、锁、状态机、fencing 这类机制去接管 correctness。
所以这一整套东西并不是谁替代谁,而更像是沿着业务边界不断演进的一条梯度。
这条演进路径,画出来大概长这样:
flowchart TD
Start["新业务需要幂等"] --> D1{"业务效果能被\n本地事务包住?"}
D1 -->|"能"| L1["唯一键 + request_id\n+ 本地事务"]
D1 -->|"不能:涉及 RPC / MQ / 跨库"| L2["状态机幂等表\n显式记录过程状态"]
L2 --> D2{"PROCESSING 卡死\n需要恢复机制?"}
D2 -->|"引入清道夫 / 补偿任务"| L3["后台扫描超时记录\n重试或修正状态"]
L3 --> D3{"清道夫与正常请求\n并发冲突?"}
D3 -->|"需要互斥与正确性保护"| L4["分布式过程控制\nRedis 票据 + 锁 + fencing"]
如果业务效果能被一个本地事务包住,纯幂等表依然是最优解
谈架构时容易把简单方案说得像“只适合初级场景”。不过纯幂等表方案其实没那么简陋——它有一个非常明确的成立条件:
业务效果和幂等记录,能不能在同一个事务里一起生效。
典型写法很简单:
- 调用方生成唯一
request_id - 在事务里先插入幂等表
- 再执行核心业务 SQL
- 一起提交
如果 request_id 已存在,插入失败,整个事务回滚。应用层捕获唯一键冲突,就知道这是一笔重复请求。
像这种场景,幂等表大概会长成这样:
CREATE TABLE idempotent_request (
request_id VARCHAR(64) PRIMARY KEY,
biz_type VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL
);
然后在同一个事务里做这两件事:
INSERT INTO idempotent_request (request_id, biz_type, created_at)
VALUES (?, 'CREATE_ORDER', NOW());
INSERT INTO orders (order_no, status, amount)
VALUES (?, 'INIT', ?);
它之所以漂亮,是因为它在一个原子事务里,同时保证了两件事:
- 这次请求只会成功“记账”一次
- 这次业务也只会生效一次
只要这个前提成立,纯幂等表方案会有几个非常难得的优点:
- 逻辑简单
- 容易审计
- 不需要额外恢复机制
- 没有
PROCESSING这种中间悬挂状态 - 新请求路径也足够快
我现在更愿意把它看成:当原子性边界足够小的时候,数据库事务就是最强的过程控制器。
这套东西什么时候开始不够用
分水岭跟并发高不高关系不大,关键在于你的业务是否已经跑出了本地事务的边界。
比如下面这类流程:
- 先写数据库
- 再调一个外部 RPC
- 再发一条 MQ
- 最后再更新另一张表
这时候,纯幂等表方案最依赖的那个前提已经没了:你再也没法把“业务确实完成”这件事,包进一个本地事务瞬间里。
然后你会发现,两个很经典的坏选择都会出现。
先插幂等记录,再调外部 RPC
这样做的风险是:
- 幂等记录成功了
- 但 RPC 失败了
结果是,这次请求在幂等层面已经被认为“做过了”,可实际业务并没有完成。后面再来重试时,系统可能直接把它挡掉,造成一种假成功。
这种问题排查起来还特别慢。
先调外部 RPC,再插幂等记录
这样做的问题正好反过来:
- RPC 真的执行成功了
- 但服务在写幂等记录前挂了
下一次请求再进来,因为还看不到幂等记录,它会以为这次业务没做过,于是把 RPC 又调一遍。
这时候你就会意识到:唯一键仍然有用,但它已经不够证明“这次请求真的完成了”。
当业务的生效边界跑到事务之外时,纯幂等表方案的问题不在于“写得不够细”——它依赖的基础条件本身已经消失了。
状态机幂等表,本质上是在记录“现在做到哪了”
当“完成”不再是一个事务瞬间,而变成一个跨步骤过程时,系统就必须开始回答另一个问题:
如果这次业务中途停住了,我们现在到底做到哪一步了?
这就是状态机幂等表出现的原因。
和纯幂等表相比,它最关键的区别在于思路变了,多几个字段只是表面:
- 纯幂等表关心的是:这次请求有没有成功成立过
- 状态机幂等表关心的是:这次请求现在处于什么生命周期阶段
典型状态可能会包括:
PROCESSINGCOMPLETEDFAILED_RETRYABLEFAILED_PERMANENT
这套设计的价值在于它终于承认现实:
- 请求可能处理中挂掉
- 外部调用可能半成功半失败
- 一个流程的完成,本来就是逐步推进的
- 幂等不再只是防重,而开始承担恢复和可观测职责
当请求到来时,系统不再只是“查一下有没有这个 request_id”,而会变成:
- 如果已经
COMPLETED,直接返回结果 - 如果还在
PROCESSING,判断是否超时 - 如果
FAILED_RETRYABLE,决定能不能继续推进 - 如果不存在,插入一条新的
PROCESSING
也就是说,幂等表不再只是一个“防重名单”,而更像是一张小型过程票据。
状态机方案真正麻烦的,不是多了一个状态字段
第一次看到状态机幂等表,容易觉得无非就是:
- 表里加个
status - 成功改成
COMPLETED - 失败改成
FAILED
但难的地方不在状态字段本身,而是下面这些问题。
PROCESSING 卡死怎么办
这几乎是状态机方案绕不开的第一个坑。
如果服务在调用完下游以后、更新最终状态之前崩了,记录就会一直停在 PROCESSING。这时候你既不能轻易说它成功了,也不能轻易说它失败了。
就悬在那儿。
系统得再引入一个恢复机制,去处理那些“处理中超时”的任务。
为什么通常需要清道夫或补偿任务
因为总会有一些请求,无法在当次调用链里自然收尾。
你需要一个后台任务去扫描超时的 PROCESSING 记录,然后决定:
- 是重试
- 是查下游状态后修正
- 还是标记成失败并等待人工介入
到这里,幂等设计已经明显从“防重技巧”演进成“过程管控”。
清道夫和正常请求可能会打架
这时候新的问题又会冒出来:
- 正常请求正在处理这笔任务
- 清道夫也来扫描并修它
- 两边可能同时操作同一条状态记录
所以很多系统走到这一步时,会开始给 request_id 级别的处理再加一层分布式互斥。也就是说,状态机方案最后往往会自然长出锁。
下游能力开始反过来决定你能恢复到什么程度
这是很多文章讲得不够的一点。
你的状态机表能不能顺利恢复,不只取决于你自己写得多好,还取决于下游到底给不给你可核实的事实。
如果下游能提供:
- 幂等查询接口
- 外部请求号查询能力
- 明确的最终状态语义
那你的补偿逻辑就能比较稳地把本地状态修回来。
如果下游完全是黑盒,不支持查询,也不支持幂等键,那很多卡在中间的请求,最后就很难靠系统自动恢复,只能靠人工补偿或者对冲操作。
走到这里,你已经在处理一个跨系统过程了。
再往前一步:什么时候要从数据库状态机继续演进到 Redis 过程控制
并不是所有事务外业务,都要立刻上 Redis 过程控制。
但有些场景,数据库状态机会逐渐显出吃力:
- 同一个流程步骤很多
- 外部调用耗时长且不稳定
- 重试和接管频繁
- 高并发下有大量重复请求
- 系统很在意恢复速度
- 最终状态正确性不能被过期执行流污染
这时候,光靠数据库表里的 status 往往已经不够优雅了。系统会进一步演进出这些东西:
- Redis 票据,记录过程态
- 分布式锁,控制接管互斥
- 看门狗,处理长任务持锁
- fencing token,阻止旧执行流回来覆盖新结果
也就是说,状态机方案并非终点,它只是你开始承认“过程本身需要被管起来”的起点。如果问题继续往 correctness 那一侧长,最后就会走到我在前一篇里拆的那层。
我现在自己的选择顺序
所以如果一定要把这件事压缩成一句决策建议,我现在会这样判断。
能放进一个本地事务里,就尽量别把问题升级
如果核心业务能完全被数据库事务包起来,那唯一键 + 本地事务,通常就是最简单也最强的方案。
只要边界跨出事务,就别再假装唯一键足够了
一旦流程里出现 RPC、MQ 或其他事务外动作,幂等问题就已经变了。这时候继续只靠“插一条防重记录”是不够的,系统必须开始显式记录过程状态。
如果过程已经足够复杂,就进入真正的流程控制
当你开始在乎:
- 断点续作
- 自动接管
- 并发恢复
- 正确性保护
- 高危状态不被覆盖
那系统最终大概率会演进到 Redis 状态票据、分布式锁和 fencing 那一层。
结语
回头看,幂等设计其实很少是“哪个好,哪个差”的问题。
它更像是在不断回答一个更根本的问题:
这次业务成立的边界,到底落在哪。
如果边界在本地事务里,那数据库唯一键和事务就是最干净的解法。
如果边界已经跨出事务,那系统就必须开始面对现实:它已经超出了“防重复插入”的范畴,变成一段需要追踪、恢复和接管的过程。
所以我不把幂等理解成一个单点技巧。它更像是一种建模能力:
你能不能准确地看见一段业务的原子性边界,并为这个边界之外的不确定性,准备对应层级的过程控制。