4. 分布式事务(下):TCC、Saga、本地消息表,风控中的选型对比
好,咱们接着聊分布式事务的下半场。
上一章我们把两阶段提交(2PC)和最大努力通知讲透了。这一章,我重点聊聊风控场景里真正用得上的三种模式:TCC、Saga 和 本地消息表。
说实话,2PC 在风控里几乎没人用——性能太差,而且阻塞。你想想看,风控系统每天要处理几千万次请求,要是每个请求都锁资源,系统早挂了。
4.1 TCC 模式:Try-Confirm-Cancel
TCC 是「补偿型事务」的典型代表。它把一笔分布式事务拆成三个阶段:
- Try:预留资源,检查业务可行性
- Confirm:确认执行,真正提交
- Cancel:取消执行,释放预留资源
我在风控项目中用过 TCC 来处理「额度扣减」的场景。举个例子:用户发起一笔转账,风控系统需要先冻结这笔额度,等业务系统确认转账成功后,再真正扣减。
// TCC 接口定义示例
public interface QuotaTccService {
// 第一阶段:Try,预留额度
boolean tryFreeze(QuotaRequest request);
// 第二阶段:Confirm,确认扣减
boolean confirmDeduct(QuotaRequest request);
// 第三阶段:Cancel,释放额度
boolean cancelFreeze(QuotaRequest request);
}
核心要点:TCC 要求每个参与者都实现 Try、Confirm、Cancel 三个接口。Confirm 和 Cancel 必须保证幂等。
嗯,这里要注意。TCC 的 Try 阶段如果失败了,整个事务直接回滚,不会执行 Confirm。但如果 Try 成功了,Confirm 或 Cancel 就必须被执行成功——哪怕重试也要成功。
我曾经踩过的坑:有一次 Confirm 接口因为数据库连接池满了,一直重试失败。结果用户的额度被冻结了整整 30 分钟。后来我加了个「异步补偿线程」,专门处理那些重试失败的 Confirm/Cancel 请求。
4.2 Saga 模式:长事务的救星
Saga 模式跟 TCC 有点像,但思路完全不同。
Saga 把一个大事务拆成多个本地事务,每个本地事务都有对应的补偿操作。如果某个本地事务失败了,就依次执行前面所有事务的补偿操作。
我个人习惯把 Saga 分成两种实现方式:
- 编排式(Choreography):每个服务自己监听事件,自己决定下一步做什么
- 协调式(Orchestration):一个中央协调器统一调度所有步骤
在风控场景里,我推荐用协调式。为什么?因为风控流程太复杂了,编排式容易让逻辑散得到处都是,出了问题很难排查。
// Saga 协调器伪代码
public class RiskSagaOrchestrator {
public void executeRiskCheck(RiskContext ctx) {
try {
// 步骤1:用户身份校验
identityService.check(ctx);
// 步骤2:规则引擎匹配
ruleEngine.evaluate(ctx);
// 步骤3:额度检查
quotaService.check(ctx);
// 步骤4:写入风控结果
riskResultService.save(ctx);
} catch (Exception e) {
// 补偿:逆序执行
riskResultService.compensate(ctx);
quotaService.compensate(ctx);
ruleEngine.compensate(ctx);
identityService.compensate(ctx);
}
}
}
我的经验:Saga 的补偿操作一定要设计成「可重入」的。我曾经遇到过补偿操作执行了一半,系统又挂了,结果数据处于「半补偿」状态,查了好久才定位到问题。
4.3 本地消息表:最朴素但最可靠
本地消息表,说白了就是「业务操作 + 消息记录」放在同一个本地事务里。
流程是这样的:
- 业务操作和消息记录在同一个数据库事务中提交
- 后台有个定时任务,扫描未发送的消息,重新投递
- 消费者处理成功后,回调更新消息状态
你想想看,这个方案虽然「土」,但胜在简单可靠。我在早期做风控系统时,就用本地消息表来处理「风控结果同步到业务系统」的场景。
-- 本地消息表结构
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
business_id VARCHAR(64) NOT NULL COMMENT '业务ID',
message_body TEXT NOT NULL COMMENT '消息内容',
status TINYINT DEFAULT 0 COMMENT '0:待发送 1:已发送 2:已确认',
retry_count INT DEFAULT 0 COMMENT '重试次数',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 定时任务扫描待发送消息
SELECT * FROM local_message
WHERE status = 0
AND retry_count < 3
AND create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR);
注意:本地消息表有个硬伤——它要求业务数据库和消息表在同一个数据库实例里。如果业务库已经分库分表了,这个方案就不太适用了。
4.4 风控场景选型对比
好了,三种模式都讲完了。咱们直接上对比表,一目了然:
| 对比维度 | TCC | Saga | 本地消息表 |
|---|---|---|---|
| 一致性级别 | 强一致性 | 最终一致性 | 最终一致性 |
| 性能 | 中等(有网络开销) | 高(异步执行) | 高(本地事务) |
| 实现复杂度 | 高(需实现3个接口) | 中(需设计补偿逻辑) | 低(表结构+定时任务) |
| 适用场景 | 额度冻结、库存扣减 | 长流程风控、多步骤审核 | 结果同步、状态通知 |
| 隔离性 | 支持(Try阶段加锁) | 不支持(需业务层处理) | 不支持 |
| 典型风控用例 | 交易额度预占 | 反欺诈多步骤审核 | 风控结果推送 |
我个人建议这样选:
- 如果业务要求强一致性,比如额度预占、资金冻结,选 TCC
- 如果业务流程很长,比如反欺诈审核要经过 5-6 个步骤,选 Saga
- 如果只是异步通知,比如风控结果同步给下游,本地消息表就够了
一句话总结:没有银弹。TCC 适合短事务强一致,Saga 适合长事务弱一致,本地消息表适合简单场景。选型时别贪心,够用就好。
4.5 核心知识体系
下面这张图,是我梳理的本章知识结构。你可以把它当作选型时的「速查地图」:
好了,这一章的内容就到这里。三种模式各有优劣,关键是根据你的业务场景做取舍。记住,没有完美的方案,只有合适的方案。