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

在业务逻辑中,经常会碰到提现需求。提现的实现一般分为两个步骤:

  • 扣除余额
  • 调用第三方支付接口进行提现(比如微信支付:企业付款到零钱)

假设我们这样写(伪代码):

<?php

DB::beginTransaction();

try {
    $member = Member::find($id);
    $member->money -= $withdrawMoney;
    $member->save();

    $wechat->payment->pay($openid, $withdrawMoney);

    DB::commit();
} catch (\Exception $e) {
    DB::rollback();

    Log::error($e->getMessage());
}

这样写会有什么问题?当执行commit的时候,由于网络原因,数据库突然连不上了或者数据库挂了,怎么办?会导致什么后果?

会导致钱付出去了,但是余额没扣。

还有同学可能会有其它疑问,如果数据库操作成功,接口调用因为网络原因失败了,会怎么样?就我们上面这段代码而言,如果接口调用失败,并且调用失败后会抛出异常的话,那就会被catch到,try里面甚至都走不到commit,直接rollback了。所以接口调用失败时,不会有原子性问题。

怎么样保证提现操作是原子性的,要么扣余额和调接口同时成功,要么同时失败?

把调微信接口的操作,转化成一个异步任务。

<?php

class WechatWithdrawJob
{
    public static function withdraw($openid, $money)
    {
        $result = $wechat->payment->pay($openid, $money);

        if ($result->return_msg == 'SUCCESS') {
            return true;
        }

        return false;
    }
}
<?php

DB::beginTransaction();

try {
    $member = Member::find($id);
    $member->money -= $withdrawMoney;
    $member->save();

    $task = new Task;
    $task->callback = json_encode([WechatWithdrawJob::class, 'withdraw']);
    $task->params = json_encode([$openid, $withdrawMoney]);
    $task->add_time = time();
    $task->save();

    DB::commit();
} catch (\Exception $e) {
    DB::rollback();

    Log::error($e->getMessage());
}

起一个异步任务消费进程,不停地轮询消费。

<?php

$tasks = Task::whereIsNull('finish_time')->where('retries', '<', self::MAX_RETRIES)->get();

foreach ($tasks as $task) {
    if ($task->retries == self::MAX_RETRIES - 1) {
        //notify administrator
        
    }
    $callback = json_decode($task->callback, true);
    $params = json_decode($task->params, true);

    if ($result = call_user_func_array($callback, $params)) {
        $task->finish_time = time();
    } else {
        $task->retries += 1;
    }

    $task->save();
}

这样就把分布式的事务转化成了本地事务,保证了提现的原子性。

除了提现,这种编程方法还可以用在所有需要调用外部接口的业务上,保证业务的原子性。

基于这种方法,还有一些优化的思路。

  • 如果异步任务比较多,有些任务可能会比较耗时,有必要多起几个消费者进程,每个进程负责不同类型的异步任务。
  • 也可以参照GO的GPM模型,给异步任务加个type,不同进程消费不同的type,未完成的同一个type的任务量设置一个上限,比如说100。达到上限后,再入库的异步任务就把type设置成global,type为global时,任意一个消费者进程都可以消费。消费type为global的任务时,记得加个分布式锁,避免并发问题。

shuizhuniurou
shuizhuniurou
3.6k 声望
66 粉丝
0 条评论
推荐阅读
关于php源码中refcount的疑问
明显,如果返回值类型是scalar,也就是标量(基本数据类型,整型、浮点型、字符串等),那么refcount指定为0,否则为N。如果设置了注释,那么以注释为最高优先级。

church1阅读 365

怎样用 PHP 来实现枚举?
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...

唯一丶25阅读 6.4k评论 4

PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 2k评论 3

封面图
图片防盗链破解 解决图片防盗链问题 反向代理
当客户端(浏览器)向服务器请求内容的时候,会提交一个header,这个header中包含了如:浏览器信息、cookie等内容,那么有一个叫referer的东东,也包含在这里面。

TANKING7阅读 11.3k评论 5

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 2.3k评论 2

封面图
Hyperf 3.0 发布,PHP 新时代
在过去的一年半时间里,Hyperf 2.2 共发布了 35 个小版本,使 Hyperf 达到了一个前所未有的高度,这里也获得了一些不错的数据反馈。

huangzhhui4阅读 1.1k评论 1

封面图
微信公众号开发:自动回复文本/图片/图文消息/关键词回复/上传素材/自定义菜单
对接流程1、申请微信公众号测试账号URL:[链接]2、登录,配置开发者服务器URL和Token开发者服务器配置代码:config.php {代码...} URL是config.php在你服务器的URLToken是上面代码自己设置的Token搞定之后,就能完...

TANKING2阅读 10.1k

3.6k 声望
66 粉丝
宣传栏