小编在目前所在公司参与过很多系统的研发、维护等工作。其中印象最深的那段代码,就是『 花钱 』,公司账户上的钱,你可以随便花,如果你这段代码没有防重,说实话,就算是高启强都不敢用这段代码。
我们一般在用支付功能对外付钱的时候,大多都是调用第三方支付接口或者银企支付,小编在公司参与开发过的佣金发放系统,相当于是调用了公司支付部门对内部提供的接口,支付部门是将银企接口进行了一次包装。
不是你的钱也不能一直花
就拿小编曾经参与的佣金发放系统为例(以下称为佣金系统),这个系统主要功能是定时计算每个订单需要发放多少佣金,然后通过调用支付系统,将钱实时付出去。听起来很简单吧,但是小编告诉你,涉及到钱的事,都不简单,写代码的时候,要时刻提醒自己,你刑。
小编这里就直接从支付这块讲起。下面有一段伪代码,小编公司实际的佣金系统要比这个复杂的多,这里只做曾经想到的一些基本的问题演示
@Transactional(rollbackFor = Exception.class)
public void pay(String orderNo) {
//加锁
lock(orderNo);
//获取订单
getOrder(orderNo);
//判断佣金支付状态,是否为已发放佣金,如果已发放,直接return
if(alreadyPay){
return;
}
//生成转账ID,银行一笔转账的唯一标识
String uuid = UUID.randomUUID()
//计算佣金
BigDecimal commission = calCommission(orderNo);
//调用第三方接口进行佣金发放,一笔转账对应一个唯一的编号
pay(commission,uuid);
//更新佣金发放状态并绑定唯一标识
if(paySuccess){
updateState(orderNo,uuid);
}
//解锁,这里只做示例,就不写try catch finally了。
unLock(orderno);
}
『 这是一段最简单基础的支付业务代码,大家看这段代码有什么问题 ?』
搭眼一看,可能也没有什么大问题,但是大家不妨想一想我们支付的时候是调用的第三方系统,如果第三方系统支付成功了,我们这边等返回结果的时候,超时了。那么就会导致,下一次定时任务还会调用到这个订单,导致佣金重复发放的问题
那我们应该怎么办呢?可能很多同学都想到了,我们可以在调支付接口之前将支付唯一标识保存下来,查一下第三方系统是否有过支付成功的记录。
那好,接下来我们把代码改一下。
@Transactional(rollbackFor = Exception.class)
public void pay(String orderNo) {
//加锁
lock(orderNo);
//获取订单
getOrder(orderNo);
//判断佣金支付状态,是否为已发放佣金,如果已发放,直接return
if(alreadyPay){
return;
}
//如果该订单的支付唯一编号不等于空,就去第三方系统查有没有支付成功过
if(uuid != null){
queryPayResult(uuid);
if(paySuccess){
updateState(orderNo);
return;
}
}
//生成转账ID,银行一笔转账的唯一编号
String uuid = UUID.randomUUID()
//保存唯一标识
saveOrderUUID(orderNo,uuid);
//计算佣金
BigDecimal commission = calCommission(orderNo);
//调用第三方接口进行佣金发放,一笔转账对应一个唯一的编号
pay(commission,uuid);
//更新佣金发放状态
if(paySuccess){
updateState(orderNo);
}
//解锁,这里只做示例,就不写try catch finally了。
unLock(orderno);
}
『 这样看是不是就好多了呢?』
但是你的领导正在你的后面摩拳擦掌!!!所以你又看了看代码
你再一看,突然发现两个问题,
- 你整个方法都在一个事物里。如果第一次调用第三方支付超时了,但其实佣金已经发放成功了,那么前面的保存唯一编号就会发生回滚操作。这一样也什么都没解决。。。。
- 如果调用第三方支付成功了,但是更新佣金发放状态失败了,也会导致整体代码回滚。再调用到这个订单的时候,发现没有支付唯一标识,也会重新生成,重新调用。
所以我们这里可以引入一个中间态->支付中,就相当于是我已经跑过这个订单了,但是不知道现在是否支付成功了,而我们支付佣金的定时任务,只查待支付佣金的订单。
同时呢,我们根据业务需求,也要将事物去掉(事物还是要根据业务需求去用,不能盲目用)。
//能传到这个方法里的一定是待支付订单,所以就不用判断支付状态了
public void commissionPay(String orderNo){
//加锁
lock(orderNo);
//生成转账ID,银行一笔转账的唯一编号
String uuid = UUID.randomUUID()
//保存唯一标识并且将佣金状态改为支付中
saveOrderUUIDAndState(orderNo,uuid);
//计算佣金
BigDecimal commission = calCommission(orderNo);
//调用第三方接口进行佣金发放,一笔转账对应一个唯一的编号
pay(commission,uuid);
//更新佣金发放状态为已发放
updateState(orderNo);
//解锁,这里只做示例,就不写try catch finally了。
unLock(orderno);
}
此外我们还差一个定时任务,需要我们对一直处于支付中中的订单进行补偿。这个定时任务会拿支付中的订单去第三方系统查,如果是支付成功,就将其状态改为支付成功,否则改为待支付。
//能传到这个方法里的一定是支付中的订单
public void compensate(String orderNo){
//获取锁,如果没有获取到,直接return
lock(orderNo);
//如果该订单的支付唯一编号不等于空,就去第三方系统查有没有支付成功过
if(uuid != null){
queryPayResult(uuid);
if(paySuccess){
//更新佣金发放状态
updateState(orderNo);
return;
}
//非支付成功或者没查到数据,就将状态更改为 待支付
updateState(orderNo);
}
}
到这里,我们这个引起重复支付的其中一个问题就解决了。
同学们可以思考一下。
- 我们是否一定要按照订单维度进行发放,如果订单数量特别大怎么优化。
- 是否需要对每一次调用进行记录。
- 有没有什么更好的方法防止重复支付。