一、什么是幂等键?
幂等键是用于识别同一笔业务请求的唯一标识。
它主要解决网络超时、重复提交、自动重试导致的重复执行问题。
例如:用户扣款 100 元,第一次请求可能已经扣款成功,但由于网络超时,调用方没有收到结果。如果直接重试,就可能重复扣款。
因此,系统为这笔业务生成幂等键 PAY001。重试时继续使用 PAY001,服务端识别出这是同一笔业务,就不会再次执行扣款。
核心记忆:同一笔业务,重试时使用同一个幂等键。
二、为什么需要幂等键?
以支付扣款为例:
- 用户发起扣款 100 元。
- 服务端调用支付平台,扣款成功。
- 返回结果时发生网络超时。
- 调用方没有收到结果,发起重试。
- 如果服务端再次扣款,就可能造成重复扣款。
注意:网络超时不代表业务执行失败,只代表调用方没有及时收到结果。
幂等键可以帮助服务端识别重复请求,避免重复执行。
三、服务端如何实现幂等?
1. 数据库唯一约束
服务端将幂等键保存到数据库,并对幂等键设置唯一约束。
具体体现:建表时给 idempotency_key 字段添加 UNIQUE。
CREATE TABLE payment_requests ( id BIGSERIAL PRIMARY KEY, idempotency_key VARCHAR(100) NOT NULL UNIQUE, amount DECIMAL(10, 2) NOT NULL, status VARCHAR(20) NOT NULL, result JSONB);例如,数据库中已经存在:
| 幂等键 | 状态 | 金额 |
|---|---|---|
| PAY001 | SUCCESS | 100 |
如果再次插入 PAY001,数据库就会阻止重复插入。
作用:保证同一个幂等键不能重复登记。
注意:唯一约束只能防止重复登记,不能单独阻止重复扣款,还需要配合业务执行逻辑。
2. 记录业务处理状态
服务端记录每笔请求的处理状态:
PROCESSING:正在处理。SUCCESS:处理成功。FAILED:处理失败。
收到重复请求时,根据状态决定如何处理:
- 已成功:不再执行扣款,返回已有结果。
- 正在处理:返回处理中,或查询原请求的进度。
- 处理失败:根据失败原因判断是否可以安全重试。
作用:让服务端知道这笔业务处理到了哪一步。
3. 处理并发请求
两个相同请求可能同时到达服务端。
如果两个请求都先检查数据库,发现幂等键不存在,然后同时执行扣款,就可能重复执行。
因此,需要利用数据库唯一约束和原子操作等手段,确保只有一个请求能够取得处理资格。
(1)通过插入记录竞争处理资格
不是让两个请求先查询、再决定谁执行,而是让它们直接尝试插入同一个幂等键。
INSERT INTO payment_requests (idempotency_key, amount, status)VALUES ('PAY001', 100.00, 'PROCESSING')ON CONFLICT (idempotency_key) DO NOTHINGRETURNING id;这条 SQL 的含义:
INSERT:尝试创建请求记录。ON CONFLICT (idempotency_key) DO NOTHING:如果幂等键已经存在,就不重复插入。RETURNING id:插入成功时返回新记录的 ID;没有插入时不返回记录。
程序根据返回值判断是否取得处理资格:
- 返回了 ID:本次插入成功,可以继续处理。
- 没有返回 ID:说明已有相同幂等键的记录,应查询已有状态,不能再次执行扣款。
(2)并发时数据库如何处理?
假设请求 A 和请求 B 同时执行上述 SQL:
- A 尝试插入
PAY001,插入成功。 - B 也尝试插入
PAY001。 - 如果 A 的事务尚未结束,B 可能等待 A 的结果。
- A 提交成功后,B 发现唯一键冲突,不会插入。
- 如果 A 回滚,B 则有机会插入成功。
这就是数据库利用唯一约束,在并发情况下决定谁成功创建记录。
(3)事务、原子操作和锁分别是什么?
| 机制 | 具体作用 |
|---|---|
| 唯一约束 | 防止相同幂等键重复登记 |
| 事务 | 保证一组数据库操作要么一起提交,要么一起回滚 |
| 原子操作 | 将关键判断和修改合并为一个不可分割的操作 |
| 数据库锁 | 控制多个事务对同一条记录的并发修改 |
例如,使用事务:
BEGIN;
INSERT INTO payment_requests (idempotency_key, amount, status)VALUES ('PAY002', 100.00, 'PROCESSING');
-- 在这里执行其他需要一起提交的数据库操作
COMMIT;如果事务失败并回滚,事务内尚未提交的数据库修改也会回滚。
注意:事务只能保证数据库内部操作的一致性,不能直接保证第三方支付平台也一起成功或回滚。
对于首次创建请求,通常使用“唯一约束 + 原子插入”来竞争处理资格;对于已有记录的状态修改,可以根据业务需要使用原子更新或行级锁。
四、为什么只有幂等键还不够?
假设服务端执行以下操作:
- 调用支付平台。
- 支付平台扣款成功。
- 服务端准备把数据库状态更新为
SUCCESS。 - 服务端突然崩溃。
此时,支付平台已经扣款,但本地数据库可能仍然显示 PROCESSING。
如果服务端重启后直接再次扣款,就可能发生重复扣款。
因此,幂等键必须配合状态管理和业务一致性控制。
第三方支付的幂等与异常处理
核心问题:
调用第三方支付平台时,可能出现网络超时。超时不代表支付失败,因为平台可能已经扣款成功,只是返回结果时网络中断。
核心原则:支付结果不明确时,先确认真实状态,不要盲目重新扣款。
1. 本地记录请求和处理状态
调用支付平台之前,先在本地数据库创建支付记录。
例如:
| 订单号 | 金额 | 状态 |
|---|---|---|
| ORDER1001 | 100 元 | PROCESSING |
PROCESSING 表示支付流程正在处理,不代表已经扣款成功。
作用: 记录这笔业务的处理进度,方便后续查询、重试和恢复。
2. 调用支付平台时使用其支持的幂等机制
调用支付平台时,携带同一笔业务的稳定标识,例如商户订单号 ORDER1001。
如果需要重试同一笔支付,就继续使用原来的业务标识,并遵循支付平台的幂等规则。
作用: 让支付平台识别同一笔业务的重复请求,避免重复扣款。
注意:不同支付平台的幂等机制不同,要以实际接口文档为准。有些平台通过商户订单号识别重复业务,不一定提供名为 Idempotency-Key 的字段。
3. 网络超时后,不直接认定失败
例如,系统请求扣款 100 元,但等待响应时发生超时。
此时可能有两种情况:
- 平台已经扣款成功,但成功响应没有返回。
- 平台没有完成扣款,或者仍在处理中。
因此,不能仅凭超时就把订单标记为失败,也不能直接创建新的支付请求。
作用: 避免把“没有收到结果”误判成“业务执行失败”。
4. 查询支付订单状态,确认是否已经扣款
调用支付平台提供的订单查询接口,查询原来的商户订单号或支付单号。
根据查询结果处理:
- 支付成功: 准备更新本地状态。
- 处理中: 稍后再次查询。
- 明确未支付: 根据平台规则判断是否可以安全重试。
- 结果仍不明确: 保留待确认状态,继续查询或进入异常处理。
作用: 以支付平台的实际订单状态为依据,而不是猜测支付是否成功。
5. 根据确认结果更新本地状态
如果确认支付成功,就把本地记录更新为 SUCCESS,并保存支付平台的交易号等信息。
例如:
ORDER1001:PROCESSING → SUCCESS如果确认失败,也要按照支付平台的状态定义和业务规则更新。
如果本地更新失败,不要重新扣款,而应重新查询平台结果,并安全地重试本地状态更新。
作用: 让本地订单状态尽可能与支付平台的真实结果保持一致。
6. 必要时通过回调、重试和对账修复状态不一致的问题
支付回调
支付平台主动通知商户支付结果。商户需要验证签名、订单号和金额,再更新本地状态。
回调可能重复发送,因此回调处理也必须具备幂等性。
重试
对暂时失败的查询、通知处理或本地数据库更新等操作,可以按规则重试。
重试时要区分“查询或保存结果”和“重新发起扣款”,不能把所有失败都当成可以重新支付。
对账
定期比对支付平台的交易记录与本地订单。
例如,平台显示支付成功,但本地订单仍然是 PROCESSING,就需要进一步核实并修复本地状态。
作用: 处理回调丢失、服务崩溃、状态更新失败等情况留下的问题。
完整流程
- 本地创建支付记录,状态设为
PROCESSING。 - 调用支付平台,使用平台支持的幂等机制。
- 如果网络超时,不直接认定失败。
- 查询支付订单状态,必要时结合支付回调确认。
- 根据确认结果更新本地状态。
- 通过重试和定期对账,发现并修复状态不一致的问题。
注意:本地数据库和第三方支付平台通常是两个独立系统,仅靠本地事务不能保证两边同时成功或失败。
第三方支付面试口播
对于第三方支付,我会先在本地记录订单和处理状态,再调用支付平台,并使用平台支持的幂等机制,避免同一笔业务重复扣款。
如果请求超时,我不会直接认定支付失败,因为平台可能已经扣款成功,只是响应没有返回。我会通过订单查询接口确认结果,同时处理支付平台的回调通知。
确认支付成功后,再更新本地订单状态。如果结果暂时不明确,就保留待确认状态,按规则继续查询,而不是盲目重新扣款。
此外,我会通过重试处理暂时失败的操作,并定期对账,发现和修复本地记录与支付平台实际结果不一致的问题。
记忆口诀:先记录、再支付;超时不判败,查单确认;更新本地,回调重试,对账兜底。
五、幂等键、重试、事务的区别
| 概念 | 主要作用 |
|---|---|
| 幂等键 | 识别是不是同一笔业务请求 |
| 重试机制 | 请求超时或失败时,再次尝试 |
| 数据库唯一约束 | 防止同一个键被重复登记 |
| 状态管理 | 记录业务当前处理到哪一步 |
| 事务 | 保证一组数据库操作的一致性 |
| 业务一致性控制 | 处理本地状态与外部业务结果不一致的问题 |
六、幂等键面试口播答案
幂等键是用来识别同一笔业务请求的唯一标识,主要解决网络超时、重复提交和重试导致的重复执行问题。
例如,支付扣款时,第一次请求可能已经扣款成功,但由于网络超时,调用方没有收到结果。如果直接重试,就可能重复扣款。
因此,我会为每笔业务生成一个幂等键,重试时保持这个键不变。服务端通过数据库唯一约束、状态记录和并发控制识别重复请求,已经成功的请求直接返回之前的结果。
但仅有本地幂等记录还不够。如果支付平台已经扣款成功,而服务端在保存成功状态之前崩溃,本地状态就可能与真实支付结果不一致。因此,还需要结合支付平台的幂等机制、订单状态查询、回调和对账,保证业务结果正确。
七、记忆口诀
同键识别、唯一防重、状态判断、并发控制、超时查单。
- 同键识别:重试时使用同一个幂等键。
- 唯一防重:数据库唯一约束防止重复登记。
- 状态判断:区分处理中、成功和失败。
- 并发控制:避免多个请求同时执行同一笔业务。
- 超时查单:结果不明确时先确认真实业务状态,不盲目重试。
一句话总结:重试解决“没收到结果怎么办”,幂等解决“重试后会不会重复执行”。
相关文章
- 场景题:扣费请求发生网络超时,如何设计?:幂等键在支付超时场景中的完整落地。
- 图生图场景:数据库表设计与异步生成流程:涉及扣费时如何用幂等键避免重复扣款。