分布式事务解决方案:我在实习转正项目里踩过的坑与填坑姿势
大家好,我是小林,普通一本 CS 专业大四狗,目前在某二线互联网公司后端组混日子,已经拿到秋招 offer 等着入职了。这学期基本没啥课,每天就窝在公司工位上写代码、改 bug、刷 LeetCode——没错,一边准备入职前的“技术保温”,一边顺便把简历上的“分布式系统经验”那行字再夯实点。
说到分布式事务,其实我原本以为这玩意儿是架构师才操心的事。结果去年双11前两周,我们组接了个新需求:用户投递简历后,要自动触发一个异步任务去抓取他 GitHub 上的公开项目信息(用爬虫),然后通过内部算法模型打个“工程能力分”存进数据库。听起来人畜无害对吧?
但问题来了:整个流程横跨三个服务——
- 用户服务(MySQL):记录简历投递事件
- 爬虫服务(Go 写的):调 GitHub API 拿数据
- 算法服务(Python + TensorFlow Serving):跑模型打分
而且 PM 死活要求“要么全成功,要么全回滚”。我当场就懵了:这不就是典型的分布式事务场景吗?!更离谱的是,测试同学还特意问:“如果爬虫超时了,简历状态还能不能回退?”——好家伙,这题我会,但没实战过啊!
被逼上梁山:为什么本地事务不够用?
刚开始我天真的想法是:在用户服务里开个事务,先 insert 简历,再 HTTP 调爬虫服务,成功了再 commit。结果被组长一巴掌拍醒:“你当其他服务是你家后花园?网络抖一下你就把简历锁死半小时?”
确实,传统单体应用里 BEGIN; INSERT; UPDATE; COMMIT 那套在微服务时代直接失效。一旦跨服务、跨数据库,ACID 就只剩个 A(原子性)在风中凌乱。
我们线上环境用的是 MySQL 8.0 + Redis + Kafka,团队技术栈偏 Go(爬虫和核心业务都是 Go),所以方案必须兼容这些。经过和后端老哥们的头脑风暴(其实就是茶水间蹲坑时的牢骚大会),我们锁定了三种主流解法:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC(两阶段提交) | 协调者统一询问+提交 | 强一致性 | 同步阻塞、性能差、协调者单点 | 金融等强一致场景 |
| TCC(Try-Confirm-Cancel) | 业务层实现补偿逻辑 | 灵活、性能较好 | 开发成本高 | 核心交易链路 |
| Saga(事件驱动) | 顺序执行+失败回滚 | 异步、扩展性好 | 最终一致性、需设计补偿 | 长流程、非关键业务 |
考虑到我们的场景:非金融级、允许短暂不一致、但必须保证最终状态正确,再加上爬虫本身就有重试机制,Saga 模式成了最优解。而且 Kafka 天然支持事件驱动,简直是天作之合。
实战:用 Saga + Kafka 搞定简历-爬虫-算法链路
架构设计
用户服务 → [Kafka Topic: resume_submitted]
↓
爬虫服务(消费)→ 抓取 GitHub → [Kafka Topic: github_fetched]
↓
算法服务(消费)→ 打分 → 更新用户表
关键点:每个步骤都必须幂等!因为 Kafka 可能重复投递消息(比如消费者挂了重启)。
第一步:用户服务发事件(Go)
// user_service.go
func SubmitResume(ctx context.Context, req *SubmitResumeReq) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 1. 插入简历记录(状态=processing)
_, err = tx.ExecContext(ctx,
"INSERT INTO resumes (user_id, status, created_at) VALUES (?, 'processing', NOW())",
req.UserID)
if err != nil {
return err
}
// 2. 发送 Kafka 事件(注意:必须在 DB 事务内!)
event := ResumeSubmittedEvent{UserID: req.UserID, ResumeID: newID}
if err := kafkaClient.PublishInTx(ctx, tx, "resume_submitted", event); err != nil {
return err // 这里会触发 rollback
}
return tx.Commit()
}
💡 坑点:早期我把 Kafka 发送放在事务外,结果 DB 提交成功但 Kafka 失败,导致漏消息!后来用了 go-mysql-kafka-transaction 这种库,确保 DB 和 Kafka 的原子性。
第二步:爬虫服务消费 + 补偿(Go)
爬虫服务收到 resume_submitted 后:
- 成功:抓取 GitHub → 发
github_fetched事件 - 失败(比如 GitHub API 限流):发
resume_failed事件
// crawler_service.go
func HandleResumeSubmitted(event ResumeSubmittedEvent) {
githubData, err := fetchGitHubProfile(event.UserID)
if err != nil {
// 关键:失败也要发事件!
kafkaClient.Publish("resume_failed", ResumeFailedEvent{
ResumeID: event.ResumeID,
Reason: "github_api_error",
})
return
}
kafkaClient.Publish("github_fetched", GithubFetchedEvent{
ResumeID: event.ResumeID,
Data: githubaData,
})
}
第三步:用户服务监听失败事件,回滚状态
// user_service_compensate.go
func HandleResumeFailed(event ResumeFailedEvent) {
// 直接更新状态为 failed,不走事务(因为原事务已结束)
db.Exec("UPDATE resumes SET status = 'failed', error_msg = ? WHERE id = ?",
event.Reason, event.ResumeID)
}
🤯 灵魂拷问:如果
github_fetched之后算法服务挂了怎么办?
答:算法服务也要做同样逻辑——失败就发scoring_failed,用户服务再监听它。每个环节都要有“逃生舱”!
踩过的坑 & 生产经验
1. 幂等性不是说说而已
有次测试同学疯狂点“重新投递”,导致同个 resume_id 被处理十几次。我们紧急加了幂等表:
CREATE TABLE idempotent_keys (
key VARCHAR(64) PRIMARY KEY, -- e.g., "resume_submitted:123"
created_at TIMESTAMP
);
每次处理前先 INSERT IGNORE,失败就跳过。虽然简单粗暴,但管用。
2. 补偿操作可能失败!
最恐怖的场景:爬虫成功了,但发 github_fetched 时 Kafka 挂了。这时候简历卡在 processing 状态,用户以为没成功。
我们的解法:
- 定时扫描:每天凌晨跑脚本,把超过 1 小时还是
processing的简历标记为timeout - 人工干预接口:运维后台加了个“强制重试”按钮(别笑,救过命)
3. 别信“最终一致性”的鬼话
上线第一周,监控告警炸了:5% 的简历卡在中间状态。查日志发现是算法服务 OOM 了(模型加载太吃内存)。后来我们:
- 给 Kafka 消费者加了 backoff retry(指数退避)
- 算法服务拆成独立 Pod,资源隔离
- 关键事件加 dead letter queue(DLQ),方便人工排查
性能 & 运维考量
延迟 vs 一致性
Saga 是异步的,所以端到端延迟从 200ms(同步)涨到 2s+。但 PM 居然接受了!理由是:“用户又不是实时看分,第二天看到就行”。果然,业务容忍度决定技术方案。
监控必须到位
我们在 Grafana 上建了三个核心指标:
resume_processing_duration_seconds(P99 < 5s)kafka_lag_by_topic(不能堆积超过 1000 条)compensation_rate(补偿率 > 1% 就告警)
有次半夜报警,发现是 GitHub API 配额用完了,自动切备用账号搞定。没有监控的分布式系统等于裸奔。
写在最后:这玩意儿真能写进简历吗?
说实话,做完这个项目我才算真正理解了“分布式事务不是银弹”。2PC 太重,TCC 成本太高,Saga 虽然灵活但得自己兜底。不过好处是——现在面试官问我“如何保证跨服务数据一致性”,我能掏出一整套故事+数据+代码,而不是背八股文。
上周五晚上,我又被拉去 review 一个新需求:用户删除简历时,要同时删掉算法生成的画像。我脱口而出:“用 Saga 啊!发个 resume_deleted 事件,下游自己清理。” —— 组长笑着点头:“行啊小子,可以出师了。”
虽然离真正的高并发、强一致场景还差得远(比如支付、库存),但至少下次写简历时,“熟悉分布式事务解决方案”这行字,我可以理直气壮地加上去了。
对了,最近在啃《Designing Data-Intensive Applications》,顺便学点 AI(毕竟算法服务老甩锅说“模型不准”)。要是哪天能把 LLM 用在事务补偿决策上……算了,先搞定入职再说吧 😅
附:关键配置参考
Kafka 消费者配置(Go Sarama):
consumer:
retries: 3
backoff_ms: 1000
max_processing_time: 30s # 超时自动 nack
enable_dlq: true # 开启死信队列
MySQL 事务隔离级别:
-- 必须 READ COMMITTED!避免幻读导致重复消费
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

评论 0