3. 回写方案设计原则:四个必须守住的底线
做决策引擎回写,说白了就是在跟数据较劲。你想想看,一条决策结果从引擎出来,要写回业务库,中间可能经过网络、队列、缓存,任何一个环节出问题,数据就乱了。我做了这么多年,见过太多因为回写设计不合理导致的线上事故。今天聊的这四个原则,是我个人觉得最核心的底线。
核心原则一览:数据一致性 → 性能优先 → 可追溯性 → 容错与幂等性。顺序不能乱,前一个是后一个的基础。
3.1 数据一致性原则:别让业务数据"打架"
数据一致性,说白了就是引擎算出来的结果,跟写回业务库的结果必须一模一样。我在项目中遇到过最典型的问题:引擎返回"通过",但回写时因为字段映射错了,业务库写成了"拒绝"。这种问题排查起来特别痛苦。
要做到一致性,我建议从三个层面下手:
- 字段级映射校验:引擎输出的字段名、类型、长度,必须跟目标表严格对应。我习惯在回写前做一次 schema 校验,不匹配就直接报错,不往下走。
- 事务边界控制:如果一次回写涉及多张表,必须用本地事务或分布式事务保证原子性。要么全成功,要么全回滚。
- 最终一致性兜底:对于高并发场景,允许短暂不一致,但必须通过补偿机制最终对齐。比如用对账任务定期校验。
| 一致性级别 | 适用场景 | 典型方案 |
|---|---|---|
| 强一致性 | 金融、风控核心决策 | XA 事务、TCC |
| 最终一致性 | 非实时推荐、批量评分 | 消息队列 + 对账 |
| 读写一致性 | 用户实时查询决策结果 | 主从同步 + 缓存穿透保护 |
注意:不要为了追求强一致性而牺牲可用性。我见过有人把所有回写都包在分布式事务里,结果高峰期事务超时,整个链路都堵死了。该让步的时候要让步。
3.2 性能优先原则:别让回写拖垮引擎
决策引擎本身计算很快,但如果回写环节设计得不好,反而成了瓶颈。我记得有个项目,引擎处理一条决策只要 5 毫秒,但回写数据库因为没做批量处理,硬生生拖到了 50 毫秒。你想想看,10 倍的差距。
性能优化的几个关键点:
- 批量回写:不要一条一条 insert,攒够一批再写。我一般设置 100 条或 200 毫秒触发一次,具体看业务容忍的延迟。
- 异步化:核心决策结果同步回写,非核心字段(比如日志、扩展属性)异步写。用线程池或者消息队列解耦。
- 连接池复用:每次回写都新建数据库连接是大忌。连接池大小根据 QPS 和数据库吞吐量算好,别拍脑袋设。
- 缓存热点数据:如果同一个决策结果会被频繁查询,先写缓存再异步落库。读请求直接走缓存,别每次都查库。
我的经验:性能优化一定要做压测。别凭感觉调参数,用 JMeter 或者自研工具模拟真实流量,看数据库的 CPU、IO、连接数曲线。我曾经把连接池从 20 调到 50,结果数据库被打挂了——因为没算上慢查询的放大效应。
3.3 可追溯性原则:出了问题能查得清
回写出问题不可怕,可怕的是出了问题不知道哪条数据写错了、谁写的、什么时候写的。可追溯性,就是给每条回写记录一个"身份证",随时能查。
我建议至少保留以下信息:
- 回写流水号:全局唯一,关联引擎决策 ID 和业务主键。查问题的时候,拿着流水号就能串起全链路。
- 时间戳:引擎决策时间、回写开始时间、回写完成时间。三个时间一对比,延迟在哪一目了然。
- 状态变更记录:每次回写尝试、重试、成功、失败,都要记日志。我习惯用单独的审计表存,不跟业务表混在一起。
- 原始结果快照:引擎返回的完整 JSON 或 XML,写一份到日志或归档表。万一业务数据被覆盖了,还能从快照恢复。
-- 回写审计表示例
CREATE TABLE write_back_audit (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
flow_id VARCHAR(64) NOT NULL COMMENT '回写流水号',
decision_id VARCHAR(64) NOT NULL COMMENT '决策ID',
biz_key VARCHAR(128) NOT NULL COMMENT '业务主键',
engine_result TEXT COMMENT '引擎原始结果快照',
write_status TINYINT COMMENT '0-待处理 1-成功 2-失败 3-重试中',
write_start_time DATETIME,
write_end_time DATETIME,
error_msg VARCHAR(1024),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键:审计表不能跟业务表在同一个事务里写。否则业务事务回滚,审计日志也没了,等于白记。我习惯用独立连接或者消息队列异步写审计。
3.4 容错与幂等性原则:扛得住异常,经得起重试
这是最后一个原则,但可能是最重要的。回写过程中网络抖动、数据库超时、服务重启,都是家常便饭。容错保证系统不崩溃,幂等保证重试不出错。
容错设计我一般分三层:
- 重试机制:临时失败(比如网络超时、锁等待)自动重试。我习惯用指数退避,第一次等 100ms,第二次 200ms,最多重试 3 次。
- 降级策略:如果重试都失败了,走降级路径。比如写本地文件、发到备用队列,等系统恢复后再补写。
- 熔断保护:连续失败超过阈值(比如 10 次),暂时停止回写,避免雪崩。等恢复后再慢慢放量。
幂等性,说白了就是同一条结果写多次,效果跟写一次一样。怎么做?
- 唯一键约束:用决策 ID + 业务主键做唯一索引。重复插入直接报错,但不会产生脏数据。
- 状态机校验:回写前检查当前状态,如果已经是"已回写",直接跳过。我习惯用乐观锁实现,CAS 更新状态。
- 去重表:专门建一张去重表,记录已经处理过的流水号。每次回写前先查去重表,存在就跳过。
避坑指南:我曾经犯过一个错——幂等判断只依赖数据库唯一键,但没考虑缓存。结果同一批数据并发写,唯一键冲突导致大量死锁。后来我改成先查缓存再写库,缓存里没有才去竞争锁,死锁问题就解决了。
3.5 四个原则的关系与取舍
这四个原则不是孤立的,它们之间有关系,有时候还得取舍。我画了一张图,帮你理清思路:
从图上你能看出来,四个原则是层层递进的。一致性是地基,没有一致性其他都白搭。性能是上层建筑,但你不能为了性能牺牲一致性。可追溯性是事后诸葛亮,但关键时刻能救命。容错和幂等性是最后一道防线,扛得住异常才算合格。
实际项目中,我一般这样权衡:
- 金融交易类回写:一致性 > 容错 > 可追溯 > 性能
- 实时推荐类回写:性能 > 容错 > 可追溯 > 一致性(允许最终一致)
- 日志审计类回写:可追溯 > 容错 > 性能 > 一致性
我的建议:不要试图一次性把所有原则都做到极致。先保证一致性和幂等性不出错,再逐步优化性能和可追溯性。迭代着来,比一开始就追求完美要靠谱得多。