在业务逻辑中,常常会碰到提现需要。提现的实现个别分为两个步骤:
- 扣除余额
- 调用第三方领取接口进行提现(比方微信领取:企业付款到零钱)
假如咱们这样写 (伪代码):
<?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 的工作时,记得加个分布式锁,防止并发问题。