当前位置: 首页 > 后端技术 > PHP

第三方支付平台提现原子问题解决

时间:2023-03-29 15:10:47 PHP

在业务逻辑中,经常会遇到提现需求。提现的实现一般分为两步:扣除余额和调用第三方支付接口提现(如微信支付:企业支付找零)。假设我们这样写(伪代码):money-=$withdrawMoney;$成员->保存();$wechat->payment->pay($openid,$withdrawMoney);DB::commit();}catch(\Exception$e){DB::rollback();Log::error($e->getMessage());}这样写有什么问题吗?执行commit时,由于网络原因,突然无法连接数据库或数据库宕机,怎么办?后果是什么?钱会付出去,但余额不会被扣除。一些学生可能还有其他问题。如果数据库操作成功,但是由于网络原因导致接口调用失败,会出现什么情况?就我们上面的代码而言,如果接口调用失败,调用失败后抛出异常,就会被捕获,try连commit都不会去,直接rollback。所以当接口调用失败时,不会出现原子性问题。如何保证提现操作是原子的,扣余额和接口调整要么同时成功,要么同时失败?将调整微信界面的操作转化为异步任务。payment->pay($openid,$money);如果($result->return_msg=='SUCCESS'){returntrue;}返回假;}}money-=$withdrawMoney;$成员->保存();$task=新任务;$task->callback=json_encode([WechatWithdrawJob::class,'withdraw']);$task->params=json_encode([$openid,$withdrawMoney]);$task->add_time=时间();$任务->保存();DB::commit();}catch(\Exception$e){DB::rollback();Log::error($e->getMessage());}一个异步任务消费过程,不断轮询消费。where('retries','<',self::MAX_RETRIES)->get();foreach($tasksas$task){if($tasksas$task){if($task->retries==self::MAX_RETRIES-1){//通知管理员}$callback=json_decode($task->callback,true);$params=json_decode($task->params,true);如果($任务->参数,真);如果($任务->参数,真);结果=call_user_func_array($callback,$params)){$task->finish_time=time();}else{$task->retries+=1;$task->save();}这样分布式的Transactions就变成了本地的Transactions,保证了取款的原子性。除了提现,这种编程方式还可以用于所有需要调用外部接口的业务,保证业务的原子性。基于这种方法,有一些优化思路。如果有很多异步任务,有些任务可能会很耗时。需要启动几个消费者进程,每个进程负责不同类型的异步任务。也可以参考GO的GPM模型给异步任务增加一个类型,不同的进程消费不同的类型,并且对同类型任务未完成的数量设置一个上限,比如100个。达到上限后,异步任务重入库的类型设置为全局。当类型是全局时,任何消费者进程都可以消费。消费类型为全局的任务时,记得加分布式锁,避免并发问题。