支付幂等:一锁二判三更新

一、前言

古人云:"一命;二运;三风水;四积功德;五读书。"

回顾过去的一年,公司的故障报告里 防重复问题 反复出现,比如:

  • 人民币钱包付款:只付款了一次,但是系统付了两笔,两笔的创单间隔时间是5s
  • 领取奖励金重复:重复获得 -> 盗刷
  • 疑似重复入账:平台背后的支付机构汇款异常,一笔交易打款两次(导致给客户入账两笔资金)
  • 等等

此类问题归类于没有做好幂等,那么幂等是什么?

对同一操作重试任意多次,系统的最终状态,以及对外可观测的结果不变;为容错与重试而生。

  • 网络问题 / 前端操作抖动,造成重复提交,后台系统仅处理一次。

针对这类问题,有一个口诀:一锁二判三更新

  • 一锁:先对同一业务唯一单号加锁,串行化并发请求,避免同时修改同一份数据。
  • 二判:拿到锁后先判定是否已处理过/是否允许处理(幂等判重、状态判定、业务约束)。已处理则直接返回历史结果,不再执行。
  • 三更新:要落库幂等记录(带唯一幂等键),重复请求直接命中并返回相同结果。

案例分析:资金安全

以下是3个案例场景。

案例一:防重复扣款(不要轻易换单)

用户下单场景:

  • 易错 1:向账户系统,重复扣余额
  • 易错 2:重复向渠道扣款

问题:如果扣款 / 扣余额步骤超时了,该怎么办?

即出现了中间态,可以先查询请求情况,再重试。

  • 超时关单不可取:可能账务系统那扣款成功了。

Tips:避免重复有如下准则

  • 扣钱时:先扣钱再处理业务
  • 加钱时:先处理业务再加钱

案例二:防重复入账

海外电商平台入账:平台收款

  • 各大银行通过 H2H方式/人工上传,解析入账文件,获得客户的银行入账流水信息
  • 根据店铺信息定期拉取账期数据
  • 根据亚马逊账单和银行流水,符合匹配规则后,进行入账

存在特殊入账:

  1. 银行钱先到,账期数据还没到
  2. 亚马逊 API 授权关闭了,但钱打过来了
  3. 等等

疑似重复入账:平台背后支付机构汇款异常,一笔交易打款两次,即入金 2 笔。

  • 后续银行直接按ACH debit 将资金从账户中扣除

案例三:防重复结算

支付场景是实时的,但要将钱打给商户要通过清结算系统。

  • 易错:重复结算、重复打款。

解决:

  1. 上下游单据一致性核对:支付 - 清结算,单据一致核对(笔数、金额)。
  2. 打款拦截:打款前规则判断,比如:每日打一次款。

二、实际工作中使用

实际工作中,通常采用组合拳方式:分布式锁 + 数据库兜底

  • "分布式锁"实现幂等:因为锁丢失 / 续约失败,仍有重复
  • 幂等表:用唯一约束 + 去重表做最终保障

代码如下:

1、分布式锁,一般使用 Redis,封装对应锁工具

java 复制代码
@RedisLock(lockName = "order", key = "#req.reqId")
public OrderResponse order(Req req) {
    // 业务处理
}

2、幂等表

java 复制代码
public void saveOrder(TradeOrder tradeOrder) {
    Order order = (Order) tradeOrder;
    transactionTemplate.execute(status -> {
        //幂等表
        repeatRepository.save(order);
        //订单表
        orderRepository.saveOrder(order);
    });
}
相关推荐
全栈弄潮儿2 小时前
Python实战第2期: 变量与数据类型
后端·python·agent
码事漫谈3 小时前
五道题,测测你有没有真的入门 AI
后端
蜗牛互联网3 小时前
Python + JSON Schema 实现工单结构化输出与本地复核
java·人工智能·后端
大侠归来5 小时前
Spring Boot 2.1 → 3.5 迁移推演:从实战出发的完整路线图
java·spring boot·后端
海马5 小时前
Spring Boot 开发知识整理
java·spring boot·后端
谢亮_vipxieliang5 小时前
Spring Boot 3.x 从零开始——环境搭建与第一个 REST 项目
java·spring boot·后端
念何架构之路5 小时前
go-grpc核心抽象接口
开发语言·后端·golang
FYKJ_20106 小时前
django个性化新闻推荐35173-计算机课程设计、毕业设计
java·spring boot·后端·python·架构·django·课程设计
夜之眷属6 小时前
服务启动即崩:InetAddress.getLocalHost() 抛 UnknownHostException 排查记
java·运维·后端·docker