如何保证接口幂等性,重试时如何避免重复扣减?

2 个回答

  • 比比东
    我一路种下蘑菇,只为让你知道回家的路。
    ## 作用 - 保证同一业务请求在网络重试、消息重复投递、客户端重复提交时,只会产生一次业务效果。 - 对“扣减库存、扣余额、扣款、计费、发券”等操作,幂等性可以避免重试导致重复扣减或重复入账。 - 在 MQTT 等至少一次投递的场景中,QoS 1 可能重复发送消息,因此不能只依赖协议层,业务侧必须具备幂等处理能力。 ## 原理 ### 1. 用唯一业务单号标识一次操作 - 为每次业务操作生成全局唯一 ID,例如订单号、支付流水号、扣减流水号或请求 ID。 - 服务端把“该业务单号是否已处理”作为判断依据,而不是根据接口被调用次数判断。 - 同一个单号重复请求时,直接返回首次处理结果,不能再次执行扣减。 ### 2. 服务端去重 常见做法: - **数据库唯一索引**:给业务流水号建唯一约束,重复插入时会失败。 - **幂等表 / 去重表**:先记录请求单号,再执行业务;处理完成后更新状态。 - **状态机控制**:订单只能从“待支付”变为“已支付”,已经是终态的请求不能再次扣减。 - **Redis 去重标记**:用请求 ID 加锁或设置已处理标记,适合高并发场景,但要考虑过期时间、异常回滚和持久化。 ### 3. 扣减操作要具备原子性 避免“先查余额再扣减”的非原子流程,可以使用条件更新: ```sql UPDATE account SET balance = balance - #{amount} WHERE user_id = #{userId} AND balance >= #{amount} AND NOT EXISTS ( SELECT 1 FROM idempotent_record WHERE request_id = #{requestId} ); ``` 也可以在一个本地事务中完成: 1. 插入业务流水号; 2. 判断是否重复; 3. 执行扣减; 4. 提交事务。 靠数据库唯一约束和本地事务保证“同一条流水只能扣一次”。 ### 4. 先完成业务,再确认消息 在 MQTT 等消息场景中,需要注意 QoS 的边界: - QoS 1 只保证至少一次投递,可能重复。 - QoS 的可靠性主要覆盖客户端与 Broker 这一跳,不等于端到端业务恰好一次。 - 如果消费端还没完成数据库扣减就提前 ACK,进程崩溃时可能造成业务丢失。 - 应在业务处理成功并提交后,再向 Broker 确认消息。 - 即使消息重复到达,也通过业务流水号去重,保证不会重复扣减。 ### 5. 结合端到端对账 对关键资金或库存链路,还应增加: - 每条消息携带全局唯一 ID 和业务序列号; - 消费端记录已处理的序列号; - 定时核对消息流水与业务扣减记录; - 对未知状态的请求提供查询接口,而不是盲目再次扣减。 核心原则是:**协议层负责尽量送达,业务层用唯一流水、去重记录、条件更新和事务保证一次生效。**
    发布于 2026-10-29 00:50 •IP属地 内蒙古
    举报
    评论
    收藏

  • 菜菜
    世界上有两个我,一个想去远方,一个身不由己。
    ## 作用 接口幂等用于保证同一业务操作被重复提交、网络超时重试、消息重复投递或支付回调重复通知时,业务结果只生效一次,避免重复扣款、重复记账、重复发货等问题。 在支付场景中,需要区分: - **重复回调**:同一笔支付成功事件被多次推送,不代表实际扣了两次钱。 - **多个支付会话**:同一订单被重复发起支付,可能生成多笔渠道交易。 - **多笔实际扣款**:不同交易号分别扣款成功,属于真实超额收款。 - **预授权和正式扣款**:银行卡可能先冻结资金,之后再正式扣款,不能仅凭账单截图判断重复支付。 ## 实现原理 ### 1. 使用稳定的业务幂等键 - 同一次业务操作必须绑定稳定的幂等键,重试、刷新或任务补偿时复用该键。 - 不能每次重试都生成新的请求标识,否则支付网关可能认为这是一笔新交易。 - 幂等键应按创建支付、扣款、退款等操作分别设计,不同操作不能混用。 - 请求金额、币种、订单等参数必须保持一致。 - 不同网关对幂等键的作用范围、保留时间和参数冲突处理不同,需要按接口文档确认。 ### 2. 拆分订单、支付尝试和幂等记录 建议维护三层标识: - **订单编号**:标识一笔业务订单,不等同于某一次支付请求。 - **支付尝试编号**:记录一次支付过程,关联渠道、金额、币种和网关交易号。 - **操作幂等键**:标识创建、扣款、退款等具体操作。 支付接口超时时,只能说明商城没有及时拿到结果,不能证明网关没有受理。此时应先查询原交易,不能直接换键重新扣款。只有确认原交易失败、撤销或会话失效后,才能开放新的支付尝试。 ### 3. 数据库唯一约束兜底 后端必须通过持久化约束防重,不能只依赖前端按钮置灰: - 支付尝试编号建立唯一约束。 - 渠道交易号在对应商户账户内唯一。 - 渠道事件编号建立唯一约束。 - 限制同一订单不能无规则地创建多个活跃支付尝试。 - 订单状态更新使用条件更新、行锁或版本号控制并发。 分布式锁可以用于限流和控制并发,但不能替代数据库唯一约束;锁过期或进程崩溃后,仍要依靠持久化记录保证正确性。 ### 4. 先记录支付尝试,再调用支付网关 推荐流程: 1. 校验订单状态、金额、币种等信息。 2. 在事务内创建支付尝试记录。 3. 调用外部支付网关。 4. 保存网关返回结果。 5. 等待回调或主动查询交易状态。 6. 只有确认成功后,才通过条件更新将订单改为已支付。 外部支付调用不应长时间占用数据库事务。订单状态和支付尝试状态最好分开维护:接口超时时,支付尝试可以处于“处理中”或“结果待确认”,不能直接判定失败。 ### 5. 支付回调要可重复接收、只生效一次 回调处理时应: - 按网关规则验签。 - 校验商户账户、订单关联、金额、币种和交易号。 - 不能只根据“成功”字段或浏览器跳转页面判断支付成功。 - 持久化回调事件,并以渠道事件编号去重。 - 没有稳定事件编号时,依据渠道规范设计去重标识。 - 使用条件更新完成订单状态迁移,例如只允许“待支付”更新为“已支付”。 - 只有状态更新成功,才触发库存、发货、通知等后续业务。 事件去重记录、订单更新和待发送业务事件应尽量放在同一事务内提交;下游消费者也必须幂等,防止没有重复扣款却重复发货。 ### 6. 超时、晚到回调和切换渠道的补偿 - 页面超时后应提示“支付结果确认中”,提供查询入口,而不是立即引导重新支付。 - 后台通过主动查询和补偿任务收敛交易状态。 - 切换支付渠道时,应尽量关闭旧支付会话,并确认关闭后是否还可能扣款。 - 如果旧交易晚到成功且新交易也成功,应保留全部流水,识别为超额收款,进入人工审核或受控退款流程。 - 退款操作本身也要使用幂等键,并跟踪退款到账结果,不能把“退款已提交”当作“资金已退回”。 ### 7. 重试时避免重复扣减的关键规则 - 重试必须复用原支付尝试编号或幂等键。 - 状态未知时先查询原交易,不直接发起新扣款。 - 只有原交易明确失败、撤销或失效,才允许新的支付尝试。 - 超过网关幂等保留期时,也不能盲目重放,应先查询原交易。 - 扣款成功后的订单更新、记账、库存扣减和发货都要通过唯一约束或条件更新保证只执行一次。
    发布于 2026-10-28 19:38 •IP属地 黑龙江
    举报
    评论
    收藏