支付中的业务逻辑|支付幂等性

支付中的业务逻辑|支付幂等性_第1张图片

ヾ( ̄▽ ̄) Hi,欢迎来到『业务杂谈』专栏!不定期收录业务逻辑相关内容,期待您的关注


☕ 前言:何为幂等性?

幂等 这个概念对于我这个在校学生而言还是一个陌生的概念,第一次听说「幂等性」是在一个讲解线程安全的课上,粗浅地了解到「幂等性」就是指 “一个操作任意多次执行所产生的影响均与一次执行的影响相同”,听起来很简单,直到在找工作面试的时候, 经常被面试官提问 “你做的支付模块是如何保证「支付幂等性」的?” 这个问题才意识到这个概念原来在实际项目中居然如此重要!

幂等性其实是一个数学上的概念,在计算机领域,幂等是指一个方法被多次重复执行的时候所期望的结果要和第一次执行所期望的结果保持一致。


支付场景的幂等性问题

很多操作天然具有幂等性,比如查询数据库,获取缓存数据,查询接口等,这些操作不管执行多少次都不会对系统产生影响;而新增、修改和删除操作可能会导致系统状态发生改变,因此在设计接口时要注意保证其幂等性。

接口幂等性

「接口幂等性」是指一个接口在多次请求同样的操作时,对系统状态不产生任何影响。具体到支付场景,对于同一笔交易的多次提交,只会产生一次支付结果,并且对账单和余额不会重复扣减,也就是说,无论是因为网络故障、超时等原因,还是由于用户意外或者恶意操作导致的多次提交,都不会对支付系统的状态产生任何影响。

支付中的业务逻辑|支付幂等性_第2张图片

保证支付接口的幂等性非常重要,因为一旦出现支付失效、重复扣款等问题,不仅会导致用户体验变差,还可能引发纠纷和信任危机,进而影响品牌形象和商业利益。

重复支付是怎么产生的?

想要保证支付幂等,就要了解一下为什么支付是非幂等的,又是哪些场景造成了支付接口的非幂等,这样我们才能 “对症下药”,找到解决支付幂等性的合理解决方案。

我们先来看看一套支付流程中的操作:

1.用户选择支付方式 -> 2.跳转到收银台 -> 3.用户支付成功 -> 4.第三方支付平台回调 -> 5.修改订单状态

支付中的业务逻辑|支付幂等性_第3张图片

导致重复支付的操作是在第三步与第四步。

❌ 首先针对第三步,用户在支付的过程中是可以选择多种付款方式的,如果用户准备用微信支付,发现余额不足,或者因为网络原因付款后没有显示成功,又立即切换了支付宝再次支付,可能会导致同一笔订单产生两笔扣款;再或者用户短时间内点击了两次 “立即支付”,同时跳转了两个页面,在一个支付页面上完成了付款又去另一个页面进行付款…

❌ 再来看第四步,用户发起支付后会产生待支付订单,但是支付状态是不可控的,支付状态需要由后端从第三方平台(微信、支付宝)的异步回调信息中获取,同样可能因为网络的原因后端无法获取到正确的支付信息,导致待支付订单的再次产生。


支付幂等方案

解决方案一:前端防抖

经过笔者在实际支付项目中的经验,微信支付平台的扫码支付是不允许同一个订单号短时间内二次创建支付的,但是对于其他平台可能还需要我们手动做出限制~

前端防抖是防止重复支付的第一道防线,整个支付流程就是从用户在前端点击『支付按钮』开始的。对于用户可能在短时间连续点击多次支付,所以我们可以在前端进行控制,限制用户在间隔 n n n 毫秒内,最多只会执行一次。

仅仅在前端做出限制还不能完全解决重复支付的问题,对于网络原因以及第三方支付平台的异步回调都有可能导致重复支付的发生,因此还要再后端进行幂等处理。

解决方案二:Token 令牌

如果你还不知道 Token 是什么,可以看看下面的视频~

Cookie、Session 与 Token

简单理解就是用户初次访问服务器,后端服务器会生成一串加密字符,在其中保存一些信息,返还给客户端,后面客户端再次访问服务器时都会携带着这串加密字符,后端解密以后可以获取到之前保存的信息,这串加密字符就是 Token(令牌)。

Token 可以看成 Session 的升级版,都能保存信息,但 Token 的信息是经过加密的,因此更加安全,常用来检验用户的登录状态。

说回支付幂等性,我们可以在用户提交订单的时候,签发一个 token,并且把 token 保存在 Redis 缓存数据库中,每个订单的 token 都是不同的,同样一个订单的 token 只能使用一次,用完即删,那么同一笔订单再次调用支付接口的时候,发现 Redis 里的 token 不存在,代表这笔订单已经被支付过来,就会拒绝请求。

支付中的业务逻辑|支付幂等性_第4张图片

Redis + Token 的方案在多线程并发的条件下仍然不能避免重复支付,应为上述的操作并不是原子性的,我们还需要借助 Lua 脚本和分布式锁,保证从 Redis 获取令牌,对比令牌,生成单号,删除令牌这一些列操作是原子性的。

Lua 脚本的示例:

-- 获取传入参数
local token = ARGV[1]
local key = ARGV[2]
local value = ARGV[3]

-- 检查 token 是否存在于 Redis 中
if redis.call("GET", "token:" .. token) then
  -- 如果 token 已经存在,说明该请求已经处理过,直接返回结果
  return redis.call("GET", key)
else
  -- 如果 token 不存在,则将 token 存储到 Redis 中,并且设置过期时间
  redis.call("SET", "token:" .. token, 1)
  redis.call("EXPIRE", "token:" .. token, 60)

  -- 执行真正的支付操作,并将结果存储到 Redis 中
  local result = do_payment(value)
  redis.call("SET", key, result)
  redis.call("EXPIRE", key, 3600)

  -- 返回支付结果
  return result
end

这个脚本假设 do_payment 是自定义函数,用于执行真正的支付操作。在代码中,我们首先获取传入的 token、key 和 value 参数,然后检查 token 是否存在于 Redis 中,如果存在则直接返回之前处理过的结果,否则将 token 存储到 Redis 中并执行真正的支付操作。最后,将支付结果存储到 Redis 中,并设置过期时间。

通过使用 Redis 和 Token 解决支付幂等性,我们可以保证同一个请求只会被处理一次,从而避免重复支付的问题。

解决方案三:引入 “支付中” 状态

我们可以在支付订单中引入一个 “支付中” 的中间状态,用户发起支付以后后端要把支付订单的状态设为 “支付中”,这样即便是支付没有成功,也可以防止重复支付的产生。

支付中的业务逻辑|支付幂等性_第5张图片

但是这种方式存在『卡单』的问题,因为支付结果需要第三方平台(支付宝,微信)异步回调给后端系统,这其中可能由于各种异常导致第三方平台没有回调支付结果后者后端没有接收到回调信息,从而导致支付订单一直处于 “支付中”,为了解决『卡单』,后端需要加入主动查询的逻辑,去第三方支付平台查询支付结果。


┊南朝四百八十寺,多少楼台烟雨中┊
文末已至,咱们下篇再见!

四连引导

你可能感兴趣的:(业务杂谈,java,开发语言)