“扣了钱却没下单”:分布式事务的交易真相

81 阅读11分钟

沉默是金,总会发光

大家好,我是沉默

一天晚上,已经快九点,办公室还亮着几盏加班的灯。

“有用户投诉,说他下单后扣了钱但没生成订单。”

运营在群里甩了一张截图,配着一句:“请技术看看?”

我放下刚背起的书包,脑子嗡地一下清醒过来。

点开后台一查,账务系统里记录确实有了:付款成功,流水号齐全;可订单系统那边,却啥也没有。库存也没动,用户就这么凭空“花了钱”,啥也没买着。

我们几个后端同事一边翻日志,一边脑补出了整件事的经过——用户付款后,请求先打到支付服务,支付成功后应该顺序调用订单服务、库存服务……但某个环节出了问题,请求“半路失踪”。

更糟的是,这已经不是第一次出现类似问题了:

  • 有时候,订单创建成功了,但库存没扣;

  • 有时候,扣了库存,余额也少了,可订单失败了。

说到底,这些服务之间各自为政,各用各的数据库,本地事务各保一亩三分地,但业务流程是跨系统的。

那一晚,我们彻底意识到:
系统已经“分布式”了,但事务处理还停留在“单机时代”。

如果你也经历过类似的夜晚,或者正准备把业务系统从“一个大坨”拆成多个服务,那这篇文章也许能替你提前避坑。

接下来,我们会带你一起回顾:

  • 什么是本地事务与分布式事务;

  • 分布式事务有哪些典型场景;

  • 分布式事务的经典解决方案;

  • 如何优雅地设计你的分布式事务方案;

**-**01-

什么是本地事务与分布式事务

什么是事务?

说起“事务”,大家最熟的是数据库里的“本地事务”。它保证了数据库操作的四个特性——ACID

  • 原子性(Atomicity):要么都成功,要么全失败;

  • 一致性(Consistency):事务前后数据要满足一致性约束;

  • 隔离性(Isolation):并发时互不干扰;

  • 持久性(Durability):一旦提交,掉电都不怕。

本地事务简单、高效,适合一个系统内部搞定一切的“小而美”应用。

分布式事务:

随着微服务和系统拆分成为主流,很多应用不再是一个系统独立完成所有业务流程,而是多个服务(通常对应多个数据库)协同完成

举个例子,一个电商平台的“下单”操作,可能要跨越这些服务:

  • 用户服务(扣用户积分);

  • 商品服务(减库存);

  • 订单服务(生成订单);

  • 支付服务(冻结余额);

每个服务都可能有自己的数据库,各自为政,本地事务解决不了多个服务间的数据一致性问题。这时,我们就需要“分布式事务”。

举个栗子 🌰

你去餐厅吃饭,点了三道菜:红烧肉、麻婆豆腐、小炒黄牛肉。服务员分头通知三个厨师做菜。

本地事务像是一个厨师做完所有菜;分布式事务则是三个厨师分别做菜,还得保证——要么三道菜都上齐,要么都别上,不能让你只吃上小炒黄牛肉,红烧肉没了,还得照单全收。

而这,正是分布式事务想解决的难题:让多个独立系统之间的数据操作保持“一致性”。

**-**02-

分布式事务有哪些典型场景

1. 微服务场景

微服务拆分后,每个服务独立部署、独立数据库。这时候,一个完整的业务流程往往要调用多个服务,例如:

  • 创建订单时,需要调用库存服务、账户服务、物流服务;

  • 用户注册时,可能要调用用户中心、CRM 系统、短信网关;

每个服务都用自己的数据库,本地事务只能保自己不出错,无法保证“整体一致”。
就像打排球,一个人扣球再猛,没人配合就赢不了比赛。

只靠本地事务不够看,得来点“团队协作”式的分布式事务支持

2. 传统中间件拆分场景

一些老系统最初是 All in One(全部模块放一个应用里),后来为了扩展或接入中台,开始把部分功能拆出去,比如:

  • 用户模块独立成用户服务;

  • 订单模块独立成订单服务;

  • 商品模块独立成商品服务;

这些服务拆开后,原来一个本地事务就能搞定的操作,现在变成了多个服务间的调用,比如:

  • 创建订单 → 减库存 → 扣余额

过去这一步事务在一个方法里,现在变成了远程调用+多数据库操作,一不小心就出现“扣了钱但没下单”、“下单成功但库存没减”的尴尬场景。

系统架构一旦分布化,事务也必须升级成“分布式事务”才行

3. 异构系统集成场景

有些业务流程需要跨系统打通,比如:

  • 电商平台对接外部支付平台;

  • SaaS 系统同步数据到企业内部系统;

  • 订单处理调用第三方供应商的接口(比如仓储系统);

这些系统之间 可能语言不同、数据库不同、架构不同,甚至不一定你能控制它们的事务。

这时候,如果还想保证流程的完整性和一致性,那就要引入一些更灵活、兼容性更强的分布式事务机制,比如 TCC 或消息队列事务等。

image.png

**-**03-

分布式事务的经典解决方案

搞清楚了为什么需要分布式事务,接下来就得看一看:怎么搞?有哪些套路?

1. 两阶段提交(2PC):

2PC(Two-Phase Commit)是分布式事务的鼻祖级方案,流程像军事行动:

  • 第一阶段:准备阶段(Prepare)
    总指挥(协调者)先发通知给所有小分队(参与者):
    “大家都准备一下,别动手,先告诉我能不能执行。”

    各分队收到后做“预处理”操作,写入事务日志,然后回复“我准备好了”。

  • 第二阶段:提交阶段(Commit)
    如果所有人都说 OK,总指挥就下达统一命令:“干!”
    每个分队正式提交事务,否则就发“取消任务”命令,全部回滚。

听起来很严谨对吧?但现实里这套流程也有几个老毛病:

  • 协调者单点故障:总指挥挂了,大家等指令等到老;

  • 阻塞问题严重:所有分队等着总指挥说话,谁也不敢动,系统容易“卡壳”;

  • 不支持非数据库资源:你要协调 MQ、缓存这类资源?对不起,不会。

2PC 更像是“书生治军”,讲究规矩但效率不高,现代微服务里已经不太流行。

2. Seata AT 模式:

AT(Automatic Transaction)是 Seata 的默认模式,设计思路是:帮你在数据库层偷偷搞定分布式事务

它有个核心组件叫 代理数据源,会在你执行 SQL 时自动做三件事:

  1. 执行前:记录“快照”,比如订单创建前,这条数据长啥样;

  2. 执行中:照你 SQL 正常执行;

  3. 回滚时:根据“快照”还原数据(像 Ctrl+Z 一样);

这套机制最大优点是——对业务开发透明。你写普通的 JDBC/MyBatis 代码,它就悄悄替你完成分布式事务控制。

但副作用也不少:

  • 只能操作关系型数据库,NoSQL、MQ 统统不支持;

  • 对 SQL 要求高,复杂 SQL、存储过程容易出问题;

  • 大数据量下性能损耗明显,毕竟要记录快照、做日志,还要支持回滚。

AT 模式像是会“时光倒流”的数据库代理人,平时默默无闻,一旦出事立马“时光修复”。

3. Seata TCC 模式:

TCC(Try-Confirm-Cancel)是经典的业务层分布式事务方案,讲究 预占资源 + 明确提交 + 明确取消,就像保险理赔流程一样:

  1. Try(尝试):试着“预定”资源,比如锁库存、冻结余额;

  2. Confirm(确认):业务成功后,正式扣减资源;

  3. Cancel(取消):业务失败后,释放资源,比如解冻、加回库存;

好处是:

  • 每个步骤都由你自定义实现,可以控制资源精度;

  • 不依赖数据库类型,适合对接第三方接口或 NoSQL 等异构系统;

  • 更贴近真实业务流程,比如电商的“锁库存”逻辑就是天然的 TCC 模式;

但难点也很现实:

  • 开发成本高:每个业务都得写三套接口(Try/Confirm/Cancel),劝退一批程序员;

  • 接口幂等性 + 空回滚 + 悬挂问题,一个不注意就翻车,测试压力大;

TCC 更像一套“强制上保险”的流程,保得住,但保费(研发成本)也不低。

**-**04-

如何优雅地设计你的分布式事务方案

很多人学完分布式事务后脑袋里冒出一个问题:“那我到底该选哪种方案?”

答案其实很现实——“看场景,看代价,看你能不能扛得住。”

下面咱们从几个维度,来讲讲工程上如何优雅、聪明、不掉坑地搞分布式事务。

1. 场景为王:别为了一只鸡,用高射炮

分布式事务不是银弹,更不是标配。绝大多数业务,不需要那么“事务性”强的解决方案。

来看几个例子:

  • 下单 + 扣库存 + 扣余额:属于强一致性场景,不能出现“钱扣了、订单没了”的情况,适合 TCC;

  • 订单状态同步到搜索引擎、推荐系统:弱一致性,允许几秒延迟,MQ 异步补偿就够;

  • 会员注册 + 送积分:注册成功是关键,送积分失败可以补发,不一定非要搞分布式事务;

所以第一步要问自己:“这几个操作,真的必须同时成功、同时失败吗?”

如果答案是“可以分步兜底”,那就别上 2PC / TCC 了,换轻量级方案。

2. 能拆就拆,别让事务撑破你的架构胃口

很多人一听“要分布式事务”,第一反应是引框架、上工具。其实工程界还有一个朴素哲学:

没有分布式,就没有分布式事务的烦恼。

这不只是玩笑,而是真·架构哲学。

举个栗子:

  • 你有个“下单服务”,原本一个接口做了:

  • 校验库存 → 扣减库存 → 插入订单 → 扣减余额;

  • 后来你拆成了库存服务 + 订单服务 + 支付服务,于是“需要分布式事务”了;

那如果换个思路:

拆服务,但保留部分事务在一个服务里,重要流程走同步,辅助操作异步 MQ 补偿。

这样你既有拆分的模块化,又减少了对强事务的依赖。少用分布式事务,其实是最好的分布式事务策略。

3. 方案选型指南:实战中的“战术地图”

根据你的业务场景、对一致性的要求、以及团队技术能力,可以做一个大致选型:

选型的本质,就是权衡:开发成本 vs 一致性需求 vs 性能可接受度。

4. 兜底方案不能少:要有“救火队”

再完美的事务设计,也挡不住现实世界出 Bug。所以在上线前你必须准备好:

  • 事务状态表:记录事务状态 + 恢复流程,防止中间挂掉后“失忆”;

  • 幂等性控制:防止事务重试时“扣多次库存”;

  • 补偿机制:比如消息重发、定时对账、人工处理通道;

  • 监控告警:一出问题,第一时间能发现、能查到、能还原;

俗话说:“设计是理想主义,运营是现实主义。”
搞分布式事务不怕出错,怕的是错了没人知道、修不了、补不上。


写在最后:分布式事务,是一场“工程权衡”的艺术

分布式事务看似是技术问题,其实更像工程哲学:

  • 要数据一致,还是系统高可用?

  • 要强控制力,还是开发快上线?

  • 要完美方案,还是可运营、可兜底?

你必须在技术、美学、现实之间做选择。因为没有哪种分布式事务方案是完美的,只有“对你当下业务、团队能力、资源限制”最合适的。

愿你在分布式事务的世界里,能收放自如,左手技术,右手哲学,带着业务一路向前冲。

**-**05-

粉丝福利

点点关注,送你 Spring Cloud 微服务实战,如果你正在做项目,又或者刚准备做。可以仔细阅读一下,或许对你有所帮助!