支付项目中常见的难点

支付平台与银联存在互相回调延迟,造成双方数据库支付记录不一致

总的来说,这是一个本地服务与第三方服务之间的分布式事务问题。
因为根本就不是一个公司的,所以是没办法保证强一致性的。
只能遵循BASE理论,也就是保证两个系统的最终一致性,允许中间有部分时间是由于延迟或者系统挂掉造成的不一致。

总的解决方案就是重试+补偿。

先说一下和第三方系统对接调用的流程:
假设本地服务是支付平台,第三方系统是银联,支付平台会调用银联的支付接口进行支付操作。
1.第一步其实是将各种支付参数比如支付ID、金额、签名等,封装成了一个html发送给银联的接口。
2.银联进行验签后修改自己的数据库为支付成功并异步返回这条信息给支付平台,通常是一条状态码比如200表示成功。
3.支付平台接到状态码后,修改自己数据库的状态,并返回给银联ok。
4.银联收到ok也就放心了,否则还会重试发送支付成功的消息,这个过程有点类似tcp的三次握手。
总而言之,就是要保证双方支付状态的一致性。
但是这个过程由于过于复杂,会遭遇一系列的问题,下面进行一一分析。
如果将问题细分,会有以下几个问题:

1.第三方接口的幂等性和一致性问题

假设出现网络延迟,银联可能没有收到、或者已经收到并在他们的数据库支付成功了,但没能及时回调,也就是我们并不清楚支付成功没有,此时如果贸然重试,有可能造成支付两次的情况(幂等问题),或者银联根本就挂掉了,我们一直重试也没用。
针对这种情况,我们一定时间内接不到回调肯定是要重试的,但是不能直接重试。
重试之前要通过支付ID主动去银联查一下支付状态,如果已经支付成功了,我们就不再重试,未支付则可以重试。
如果根本查不到状态(超时)、或者已经重试多次(要增量重试,1分钟5分钟15分钟这种),返回的状态码都不正确,那说明银联服务挂掉了。
那就要抛异常,然后AOP写入日志,将来手动补偿(仍然要查询银联判断是否已支付成功,保证幂等性),或采用定时任务自动补偿。
具体怎么记录日志,主要是通过AOP。
定义PointCut方法上的注解切(扫描)到我们的类上。
然后在PointCut的Before和After注解中记录请求和响应的内容,封装成json保存到数据库中。
其实,以上方案只能尽量保证99%的一致性,如果你非要说,我查询银联接口显示未支付,结果我支付前的一瞬间银联已支付了,造成重复消费,产生幂等性问题,这种情况有没有可能发生?
有!有是有,但概率极低,但凡和支付相关的所有操作,我们都要通过日志保存到数据库,可以定期和银行进行对账,剿灭这最后的不一致问题。

2.本地服务接口的幂等性和一致性问题

假设出现网络延迟,银联支付成功了,也给我们返回了状态码,我们返回ok却延迟了,也就是银联不清楚我们支付成功没有,此时银联自己会重试(默认10秒接不到ok就重试),但,重试有5次上限,超过了银联就不再重试,这又引申出两个问题,一是如果网络第11秒突然恢复了,此时我们接到两次银联的请求,都代表银联通知我们支付成功,可是我们在银联通知后除了设置数据库状态为支付成功,有可能还要发放支付成功送优惠券什么的,那岂不是会多送?二是如果我们的支付平台根本就是挂掉了,银行重试超过5次就不重试了,两边支付状态就永久不一致了。
针对第一种情况,我们要在自己本地服务的操作中保证幂等性。
通过某个全局唯一的东东,可以是这条支付链路的唯一ID比如订单ID,可以是Redis的唯一key,可以是数据库的唯一key,可以是zk的唯一节点……
总之在发放优惠券前进行判断,如果已经送过了,就不再送了。
针对第二种情况,本地服务出错肯定要记录日志了。
然后通过手动补偿(先查询银联支付状态,保证幂等),或者定时任务自动补偿,保证双方数据库支付状态的一致性。
亦或者通过定期对账保证最后1%的不一致的可能性。

3.和第三方接口对接时的数据安全问题

很简单,我们提交的表单有可能被黑客截获并篡改,然后发给银联。
这种情况通过签名解决,也就是RSA算法,银联有私钥,发给我们公钥,公钥签名,只有私钥才能验签,如果黑客对提交的表单数据进行了篡改,是通不过验签的。
但是万一黑客就是很牛逼,篡改了金额,本来1000块的东西,提交给银联,他给篡改成1分钱,银联也没验出来,怎么办?
为了防止这种情况,我们必须要在银联回调我们后,我们设置自己的数据库支付状态为成功之前,先对银联实际支付金额进行判断,如果和我们提交的不一致,则不能设置为支付成功,要设置成支付异常,然后报警。
但是万一黑客就是牛逼到跟上帝一样,银行回调的数据他又给改了,改回1000了咋办,我们也没法判断。
这时日志又派上用场了,我前面说过,所有支付流程必须记日志,所有操作步骤都是可查的,那么在每月一次的和银联的对账中,这种人是逃不了的,最终会被查出来,还是那句话,概率太小,所以也不用担心工作量的问题。

支付平台和订单平台、库存平台的分布式事务问题

这种属于内部系统各个微服务之间的分布式事务问题,如果要求强一致性,回滚迅速,自然要使用2PC/TCC之流。
只是2PC太消耗资源了。
一是事务连接巨慢,因为要统一被一个事务中间件管理,每次都要连接。
二是锁表,第一阶段等待应答过程中所有表都不能使用。
如果使用TCC补偿机制,对资源进行预锁定,失败了也能及时回滚,不失为一个好办法。
最适合的还是MQ+消息表实现最终一致性,具体又会产生以下一些细分问题。

4.消费者端事务失败了怎么办。

这个问题,生产者是不需要回滚的,因为MQ的本质就是把两端解耦了,没有任何关系。
只需要开启MQ的手动ACK,设置好重试次数。
首先说ACK+重试机制其实秘密就在@RabbitListener这个注解,原理是AOP。
当注解的方法没有执行成功,也就是没有发送ACK时,默认自动无限重试。
之所以要设置为手动,就是因为有可能是代码出了问题,这时候无限重试不是浪费资源嘛。
代码出了问题不需要重试,直接抛异常。
之所以设置重试次数,是为了补偿事务,如果超过重试次数,将消息发给死信队列。
让记日志的消费者消费死信队列,然后手动补偿或者定时任务补偿执行事务。
同时,补偿也好,重试也好,都要注意幂等性问题。
所有操作执行之前都要先查询对方是否已经执行过一次,执行过了就不能再次执行。
或者检查某个唯一ID,插入过了说明执行过了,不能再次重试。

5.生产者端事务失败了怎么办

问题比较复杂,就是说,如果生产者投递消息了,消费者也消费消息了,但是生产者事务这时候报错了。
为什么会发生这种情况?难道不是生产者事务成功后才发的消息?
其实是这样的,生产环境代码和业务极其复杂,提交事务也好,发送消息也好,设置返回Result也好,这几条关键代码中间不是原子性的,总会有可能出点差错……
万一偏偏就在修改了数据库订单,发送完消息后,方法结束之前,出了错,由于@Transactional是包裹整个方法的,还是会回滚的呦。
所以你说想要保证订单事务没问题了,再去发送消息,不存在的,没这么简单。
针对这个问题,我们需要再创建一个补偿队列,连接同一个生产者。
消费者发送消息的时候,补偿队列也收到生产者的信息了。
然后通过定时任务去判断生产者事务情况,如果事务回滚了,那么补偿队列去尝试重试生产者的事务。
比如订单调库存,补偿队列通过订单ID查到订单事务回滚了,没有创建,则再创建一次订单。
当然,这里仍然要做幂等性判断,如果订单已经存在了,是不能重复创建的。

  • 14
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值