事务中调用第三方,事务失败回滚,第三方成功
事务中调用第三方,事务失败回滚,第三方成功
✅ 原则:
- 不在事务中调用不可回滚接口!
- 把“第三方调用”设计为可重试的异步任务;
- 用数据库持久化任务(避免宕机丢失);
- 异步任务中记录调用结果,避免重复调用;
- 调用失败可定时重试 + 超过次数报警。
✅ ThinkPHP 实现方案(示例)
1️⃣ 创建一张表:
third_party_tasks
CREATE TABLE `third_party_tasks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`type` VARCHAR(50) NOT NULL, -- 接口类型,如 send_sms, push_crm
`payload` TEXT NOT NULL, -- JSON 格式参数
`status` TINYINT NOT NULL DEFAULT 0, -- 0 待处理,1 成功,2 失败
`retry_count` INT DEFAULT 0,
`created_at` DATETIME,
`updated_at` DATETIME
) ENGINE=InnoDB;
2️⃣ 在本地事务中仅写入任务:
use think\facade\Db;
use think\facade\Queue;
Db::startTrans();
try {
// 1. 写主业务
Db::table('user')->insert([
'name' => '张三',
// ...
]);
// 2. 写任务表(不要直接发第三方)
Db::table('third_party_tasks')->insert([
'type' => 'send_sms',
'payload' => json_encode([
'phone' => '13800000000',
'content' => '欢迎注册'
]),
'status' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
3️⃣ 写异步任务消费逻辑(定时任务或队列)
// app\command\ProcessThirdParty.php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;
class ProcessThirdParty extends Command
{
protected function configure()
{
$this->setName('process:thirdparty')->setDescription('处理第三方任务');
}
protected function execute(Input $input, Output $output)
{
$tasks = Db::table('third_party_tasks')
->where('status', 0)
->limit(10)
->select();
foreach ($tasks as $task) {
$data = json_decode($task['payload'], true);
$success = false;
try {
// 根据 type 调用对应接口
switch ($task['type']) {
case 'send_sms':
$res = sendToThirdPartySms($data['phone'], $data['content']);
$success = $res['code'] == 200;
break;
// 其他类型...
}
} catch (\Exception $e) {
$success = false;
}
Db::table('third_party_tasks')
->where('id', $task['id'])
->update([
'status' => $success ? 1 : 2,
'retry_count' => $task['retry_count'] + 1,
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
}
4️⃣ 调用第三方接口方法(可用 Curl 或 Guzzle)
function sendToThirdPartySms($phone, $content)
{
// 例:伪代码调用第三方 API
$url = 'https://third.api/send';
$response = json_decode(file_get_contents($url . "?phone={$phone}&content=" . urlencode($content)), true);
return $response;
}
5️⃣ 设置定时任务(每分钟跑一次)
* * * * * php think process:thirdparty >> /tmp/third.log 2>&1
✅ 补充建议
- 可为任务表加唯一约束,防止重复写入;
- 增加 max_retry 字段,超过次数后报警;
- 使用队列(如 Redis 队列)也可以代替定时任务;
- 后台加任务状态展示界面,便于排查和手动补发。
✅ 总结
项目 | 当前方式 | 改进方式 |
---|---|---|
第三方接口 | 事务中直接调用 | 写入任务表后异步调用 |
接口是否可撤销 | ❌ 否 | ✅ 异步前保证事务成功 |
数据一致性保障 | ❌ 低 | ✅ 最终一致性、可重试 |
异常恢复能力 | ❌ 弱 | ✅ 有补偿、可监控 |
封装一个完整的ThirdPartyService
✅ 设计目标
- 支持多类型第三方调用(如发送短信、同步CRM等)
- 事务中写入任务,不直接调用第三方
- 异步执行,记录调用结果,可重试
- 统一入口 ThirdPartyService::dispatch($type, $payload)
** 文件结构建议(ThinkPHP 6)**
app/
├── common/
│ └── service/
│ └── ThirdPartyService.php ✅ 统一调度服务类
│
├── model/
│ └── ThirdPartyTask.php ✅ 任务模型
│
├── command/
│ └── ProcessThirdParty.php ✅ 定时处理命令
✅ 1. 数据表:third_party_tasks
CREATE TABLE `third_party_tasks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`type` VARCHAR(50) NOT NULL,
`payload` TEXT NOT NULL,
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理,1成功,2失败',
`retry_count` INT DEFAULT 0,
`max_retry` INT DEFAULT 3,
`error_message` TEXT,
`created_at` DATETIME,
`updated_at` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
✅ 2. 模型类:
app/model/ThirdPartyTask.php
namespace app\model;
use think\Model;
class ThirdPartyTask extends Model
{
protected $table = 'third_party_tasks';
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
}
✅ 3. 服务类:
app/common/service/ThirdPartyService.php
namespace app\common\service;
use app\model\ThirdPartyTask;
class ThirdPartyService
{
/**
* 调度第三方接口任务
* @param string $type 接口类型标识,如 send_sms、push_crm
* @param array $payload 参数数组
* @param int $maxRetry 最大重试次数
*/
public static function dispatch(string $type, array $payload, int $maxRetry = 3)
{
return ThirdPartyTask::create([
'type' => $type,
'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE),
'status' => 0,
'retry_count' => 0,
'max_retry' => $maxRetry,
]);
}
/**
* 执行一个任务(可用于队列或定时任务)
*/
public static function execute(ThirdPartyTask $task)
{
$data = json_decode($task->payload, true);
$success = false;
$error = '';
try {
switch ($task->type) {
case 'send_sms':
$success = self::sendSms($data);
break;
case 'push_crm':
$success = self::pushToCRM($data);
break;
default:
throw new \Exception("未知任务类型:{$task->type}");
}
} catch (\Throwable $e) {
$error = $e->getMessage();
}
$task->retry_count++;
$task->status = $success ? 1 : 2;
$task->error_message = $success ? null : $error;
$task->save();
}
// 示例:发送短信
private static function sendSms(array $data): bool
{
// 模拟调用第三方短信接口
if (empty($data['phone']) || empty($data['content'])) {
throw new \Exception("缺少短信参数");
}
// 伪代码:curl_post('https://api.xxx.com/sms', $data);
return true;
}
// 示例:推送CRM
private static function pushToCRM(array $data): bool
{
// 模拟调用 CRM 接口
if (empty($data['user_id'])) {
throw new \Exception("CRM 参数无效");
}
return true;
}
}
✅ 4. 定时任务:
app/command/ProcessThirdParty.php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use app\model\ThirdPartyTask;
use app\common\service\ThirdPartyService;
class ProcessThirdParty extends Command
{
protected function configure()
{
$this->setName('process:thirdparty')->setDescription('处理第三方任务队列');
}
protected function execute(Input $input, Output $output)
{
$tasks = ThirdPartyTask::where('status', 0)
->where('retry_count', '<', \Db::raw('max_retry'))
->limit(10)
->select();
foreach ($tasks as $task) {
ThirdPartyService::execute($task);
$output->writeln("处理任务 #{$task->id} 状态:{$task->status}");
}
}
}
✅ 5. 调用示例(业务中使用)
use think\facade\Db;
use app\common\service\ThirdPartyService;
Db::startTrans();
try {
Db::table('user')->insert([
'name' => '张三',
]);
// 写入异步任务
ThirdPartyService::dispatch('send_sms', [
'phone' => '13800000000',
'content' => '欢迎加入!'
]);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
✅ 6. 定时任务配置(每分钟执行)
* * * * * php think process:thirdparty >> /tmp/tptask.log 2>&1
✅ 效果回顾
功能点 | 实现方式 |
---|---|
第三方接口避免事务污染 | ✅ dispatch 写入任务表 |
支持多类型任务分发 | ✅ switch-case 结构可扩展 |
支持重试、失败记录 | ✅ retry_count + status |
可观察/监控任务执行情况 | ✅ 可查数据库任务状态 |
避免重复调用 | ✅ 可扩展幂等逻辑 |
你要保守你心,胜过保守一切。
本文来自博客园,作者:刘俊涛的博客,转载请注明原文链接:https://www.cnblogs.com/lovebing/p/18970656