Java技术升华程序员技术架构

第三方支付微服务幂等设计

2018-09-04  本文已影响432人  fantuanjiaozi

目录

1.背景


2.第三方支付的幂等场景

在支付领域,通常分为入金和出金两种交易场景。以下根据这两种场景来分析:不做幂等设计时,带来的问题。

例1:入金场景

幂等需求:对于同一笔订单,无论用户支付多少次,最终只能成功一次(扣一次款)。

@Transactional
public void orderPay(){
    initOrder();
    pay(accountId,amout);
    handleOrder();
}
initOrder(){} //制单
pay(String accountId,long amout){} //支付
handleOrder(){} //根据支付结果,处理订单
入金幂等.png

例2:出金场景

幂等需求:对于同一笔提现订单(同样报文),无论用户提现多少次,最终只能成功代付一次。

@Transactional
public void orderPay(){
    initWithDraw();
    issue(accountId,amout);
    handleWithDraw();
}
initWithDraw(){} //制单
issue(String accountId,long amout){} //代付
handleWithDraw(){} //根据代付结果,处理订单
出金幂等.png

问题导火索

在实际的线上环境中,造成重复出金和入金,主要有以下几种原因:


3.什么是幂等?

用户对于同一操作发起的一次请求和多次请求的结果是一致的。不会因为多次请求而产生副作用。


4.怎么做幂等设计?

4.1应用程序

入金幂等ok.png

涉及到资金交易的系统,通常会把交易流程分解成多个阶段,每一个阶段用不同的状态来标识。如在订单受理或初始化阶段,把订单状态设为0;开始付款时设为1,标识当前订单是处于付款中状态;支付完成后,根据实际的结果,将订单状态设为成功或者失败。

状态机.png
//1.查询当前订单
select order_status from t_pay where order_id=#{orderId} and system_no='订单系统'
//2.1订单已经存在且状态为终态,则直接返回
if("2".equals(orderStatus)){
    return "成功";
}
if("3".equals(orderStatus)){
    return "失败";
}
//2.2订单已经存在且状态非终态
if("1".equals(orderStatus)){
    //查询下游系统or重试。
}
//2.3订单初始化
if("0".equals(orderStatus)){
    //不再入库,直接调用支付逻辑以及后续处理
}
//2.4订单不存在
if(null==orderStatus){
    //3.订单初始化入库,状态order_status=0
    insert into t_pay(order_id,order_status,account_id,amount,....) values(#{orderId},'0',#{accountId},#{amount},...)
    //3.1各种准备工作,如路由,记账等
    //3.2更新为付款中
    update t_pay set order_status='1' where order_id=#{orderId} and system_no='订单系统' and order_status='0'
    //4.调用支付
    status=doPay(accountId,amount);
    //4.1更新订单状态
    update t_pay set order_status=#{status} where order_id=#{orderId} and system_no='订单系统' and order_status='1'
    return status;
}

注:

1.如果实际情况没有3.1,可以省略3.2,入库时状态order_status直接设成1。
2.如果调用支付功能是本地调用,可以考虑使用事务。

4.2数据库

数据库幂等.png

两笔相同订单全都入库,通常会出现两种可能:1.重复出入金;2.两笔订单状态不一致,造成上游系统无法正常处理业务。同时对账模块也会受到影响。

有一种思路是通过类似Redis的NOSQL,使用Redis的高性能以及串行化特性。

思路1.将部分订单的关键信息存储在Redis中,通过Redis进行校验。但这又带来了额外的问题:一是要保证Redis的高可用,应用也要做针对Redis故障的降级处理,增加了应用程序的复杂度。二是在极限情况或是Redis发生卡顿时,也会造成数据写入Redis不及时,而引起两笔订单重复入DB的情况。

思路2.为了解决写入Redis不及时的问题,订单系统将订单关键信息写入Redis,支付系统直接取同一份Redis数据做校验。但是这种方案违反了单一自责原则。

建立数据库的唯一约束是目前比较常用解决办法。在实际的支付业务中,通常把订单号orderId和请求系统编码system_no(或是请求商户号merchantNo)做为数据库的联合唯一约束。保证同样的订单在数据库只有唯一的一条记录。当有重复数据请求时,应用程序在捕获此SQL异常后,重新调用pay()实现。

4.3定时任务重复启动

常见问题

在分布式系统的复杂架构中,即使应用程序做了幂等设计,数据库也做了唯一约束,仍会产生重复出金和入金,比如定时任务系统重复启动,如图:

定时任务重复.png

1.两个相同的代付定时任务A和B同时启动,且同时从数据库中获取到一条订单号orderId=1 and 状态status=0的的订单。
2.两个定时任务执行:将该订单状态改成付款中=1。
3.更新成功后,两个定时任务将该订单发送到银行。导致重复出金。

解决办法
定时任务重复解决-sql.png

在更新订单状态时,where条件增加约束status=0,当返回更新条数=1时,说明更新成功,则发送到通道;当返回更新条数=0,说明该订单可能已经被其他的定时任务抓取,则停止发送。

依靠中心化的定时任务调度中心,各应用暴露业务逻辑接口,由定时任务调度中心统一发起调度任务。调度过程中,通过抢占数据库锁(也可以通过redis分布式锁实现,但是需要保证redis集群高可用)来实现幂等。

调度中心.png
上一篇下一篇

猜你喜欢

热点阅读