继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

慕神8447489
关注TA
已关注
手记 1310
粉丝 174
获赞 957

1.背景

  • 在传统的单体应用里,即同一进程内,对于一个函数的调用,结果只有两种:成功和失败。

  • 在分布式架构体系里,调用远程的接口服务,除了成功和失败,还会有第三种结果——超时。这个场景被称为:分布式的三态。而三态中的超时直接提升了分布式架构的复杂性,也带来了幂等的问题。


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

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

例1:入金场景

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

  • 在传统的单体应用里:只需把下单和支付放在一个事务内即可,下单和支付的操作要么全成功,要么全失败。如下伪代码:

@Transactionalpublic void orderPay(){
    initOrder();
    pay(accountId,amout);
    handleOrder();
}
initOrder(){} //制单pay(String accountId,long amout){} //支付handleOrder(){} //根据支付结果,处理订单
  • 在分布式架构里,我们把单体应用拆分为订单系统和支付系统,大部分情况下,订单系统通过RPC调用支付系统,最终完成一笔交易。

  • 在不做幂等设计场景下,如下图所示,支付系统在第3步扣款成功,但是由于网络原因,订单系统没有获取到结果,没经验的开发工程师经常会把超时当做失败处理,又在第6步再次发起支付,最终导致用户扣款两次。

webp

入金幂等.png


例2:出金场景

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

  • 在传统的单体应用里:只需把提现和代付放在一个事务内即可,提现和代付的操作要么全成功,要么全失败。

@Transactionalpublic void orderPay(){
    initWithDraw();
    issue(accountId,amout);
    handleWithDraw();
}
initWithDraw(){} //制单issue(String accountId,long amout){} //代付handleWithDraw(){} //根据代付结果,处理订单
  • 在分布式架构里,我们把单体应用拆分为提现系统代付系统,大部分情况下,提现系统通过RPC调用代付系统,最终完成一笔交易。

  • 在不做幂等设计场景下,如下图所示,代付系统在第3步付款成功,但是由于网络原因,提现系统没有获取到结果,没经验的开发工程师经常会把超时当做失败处理,又在第6步再次发起提现请求,最终导致给用户付款两次,造成资金损失。

webp

出金幂等.png


问题导火索

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

  • 1.应用程序对于同步超时的处理逻辑有误(参见上述的两个例子)。主要是由于开发人员经验不足导致。

  • 2.定时任务重复执行。这种情况较为常见,通常是由于开发或者运维人员误操作导致,本文后面章节有相应的解决方案。

  • 3.dubbo,zuul等第三方框架自身的重试机制。一些框架默认的配置就是:超时后自动重试。开发人员一旦不小心,就容易踩坑。

  • 4.HAProxy,Nginx等中间件自身的重试机制。部分企业里,由于开发和运维的部门壁垒,中间件的超时重试机制是最容易被忽视的。大部分企业的测试环境的网络配置远比生产环境简单,在一定程度上也导致中间件重试的问题在测试环境不会出现,只发生在生产的事故里。并且大部分的开发人员可能不清楚生产的系统用了哪些中间件,更不了解中间件的运行原理,也导致生产上排查问题效率低下。

  • 5.极限,高并发场景。极限和高并发场景是在测试用例不容易覆盖到的。


3.什么是幂等?

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


4.怎么做幂等设计?

4.1应用程序

  • 订单系统上送唯一标识orderId,支付系统根据orderId做订单唯一性校验。支付系统只要检测到扣款成功,就直接返回支付结果。参见下图:

webp

入金幂等ok.png

  • 状态机

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

webp

状态机.png

  • 支付系统pay()接口幂等实现伪代码:

//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数据库

  • 在实际生产的高并发、高复杂业务中,仅仅靠应用程序做幂等性设计是不足的。如在上例中的极限情况下,两笔相同报文的支付请求同时调用支付系统,在第一笔订单尚未入库前,第二笔订单也已经受理,两笔订单在做数据校验时,发现都没有相同的数据,而导致两笔订单全部入库。如下图:

webp

数据库幂等.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定时任务重复启动

常见问题

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

webp



作者:fantuanjiaozi
链接:https://www.jianshu.com/p/2c8cf162cf62


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP