10-分布式事务
什么是分布式事务:事务的参与者、资源服务器及事务管理器分别位于不同系统节点上,用于保障跨节点操作数据一致性的事务类型,其核心目标是实现跨多个服务或数据库的 ACID 属性(原子性、一致性、隔离性、持久性)
分布式事务:
强一致性:追求像本地数据库事务一样的ACID(原子性、一致性、隔离性、持久性),要么全成功,要么全失败,且实时可见。典型代表是XA协议(两阶段提交)。
最终一致性:允许在短时间内数据不一致,但保证在最终某个时间点数据会达成一致。这是互联网高并发场景下的主流选择。
| 方案名称 | 核心思想 | 一致性保证 | 典型代表 |
|---|---|---|---|
| XA协议(两阶段提交) | 强一致性,数据库层面锁定资源,分准备和提交两阶段 | 强一致性(ACID) | Seata AT、Atomikos |
| TCC模式(补偿事务) | 业务层面预留资源,通过Try-Confirm-Cancel三个接口人工编码 | 最终一致性 | Seata TCC、Hmily |
| 事务消息 | 利用半消息+本地事务+异步回查,解耦上下游 | 最终一致性 | RocketMQ事务消息 |
| 本地消息表+定时任务 | 本地数据库建消息表,事务中写入,定时轮询发送 | 最终一致性 | 自研封装 |
| 最大努力通知 | 不断异步重试,直到成功或达到最大尝试次数 | 最终一致性 | 支付宝/微信异步回调 |
一、AT模式
AT模式是Seata的看家本领,它的核心口号是:“对业务零侵入,像写本地事务一样写分布式事务”。
你只需要加一个 @GlobalTransactional 注解,Seata就会自动帮你完成分布式事务。它的秘密在于数据源代理和镜像回滚。
第一步:分支事务执行前(准备镜像)
当RM(资源管理器)接收到业务SQL(比如 update stock set count = count - 1 where id = 1)时,Seata的数据源代理不会直接把SQL发给数据库,而是先做两件事:
- 解析SQL:解析出SQL要修改的数据范围(
id=1)。 - 查询前置镜像(Before Image):执行
select * from stock where id = 1,把当前库存数(比如count=10)保存起来,作为回滚的凭据。
第二步:执行业务SQL
前置镜像保存成功后,才真正执行 update stock set count = count - 1 where id = 1。
第三步:查询后置镜像(After Image)并生成回滚日志
SQL执行后,再次查询 select * from stock where id = 1,拿到更新后的结果(count=9)。
接着,RM会把 “前置镜像”、“后置镜像” 和 “业务SQL” 组合成一条回滚日志(Undo Log),在同一个本地事务中插入到数据库的 undo_log 表里。
关键点:业务SQL和插入
undo_log是在同一个本地事务里提交的。如果业务SQL失败,undo_log也不会插入,本地事务回滚,一切归零。
第四步:全局提交或回滚
全局提交:如果所有分支都成功,TC通知各RM提交。RM收到后,会异步删除本地对应的
undo_log记录(因为不需要回滚了)。全局回滚:如果某个分支失败,TC通知所有RM回滚。RM要做的事非常巧妙——根据
undo_log里的“前置镜像”,生成一条逆向的SQL。
比如,前置镜像是
count=10,后置镜像是count=9,RM就会生成update stock set count = 10 where id = 1,执行这条SQL,数据就恢复了。
AT模式的优缺点:
- 优点:开发效率极高,一个注解搞定,无代码侵入。
- 缺点:性能损耗在“镜像查询”和“锁”上。因为要在执行SQL前后查两次数据,且为了防止脏写,Seata会对修改的行加全局排他锁,高并发下容易死锁。
二、TCC模式
它的实现原理从“数据库行锁”变成了“业务状态机”。它没有“魔法”,完全依赖开发者手动编码来实现资源预留和补偿。
第一阶段:Try(预留资源)
以扣库存为例,SQL不再是直接减,而是:
-- Try阶段:冻结库存(将可用库存转为冻结库存)
update stock
set used_count = used_count + 1, -- 冻结数量+1
available_count = available_count - 1 -- 可用数量-1
where id = 1 and available_count > 0;此时,数据状态变为“中间态”(数量被预留了,但还未真正扣除)。
第二阶段:Confirm(确认提交)
如果全局事务决议提交,TC会调用各个分支的Confirm方法。
-- Confirm阶段:真正扣除冻结库存(清空冻结字段)
update stock
set used_count = used_count - 1
where id = 1;此时,数据状态变为“最终态”(扣除完成)。
第二阶段(反向):Cancel(补偿回滚)
如果全局事务决议回滚,TC会调用各个分支的Cancel方法。
-- Cancel阶段:释放冻结库存(把冻结的还回去)
update stock
set used_count = used_count - 1,
available_count = available_count + 1
where id = 1;此时,数据状态恢复为初始态。
因为TCC需要手动编码,必须处理好以下三个问题:
空回滚:Try阶段没执行成功(比如网络超时),但TC以为超时了,直接调用了Cancel。Cancel直接去释放资源,发现根本没冻结,怎么办?Cancel里必须判断:如果Try没预留资源,Cancel直接返回成功。
悬挂:Try执行得特别慢,超时了。TC直接走了Cancel,Cancel释放了资源。然后Try又执行成功了(预留了资源),导致资源被永久冻结。解决办法:在Cancel执行时,记录一个“已回滚”标识;Try执行前先查这个标识,如果发现已被回滚,直接放弃执行。
幂等性:Confirm和Cancel由于网络抖动可能会被重复调用,必须支持幂等(通过状态机或分布式ID判断是否已执行过)。
三、saga方案
四、事务消息
- 典型场景:“下单成功 → 发积分/发短信/更新缓存”。
- 核心优势:彻底解耦。订单服务只需要保证消息发出去了,下游积分服务是否成功,订单服务不需要同步等待。
- 适用边界:只支持正向流程,不支持回滚下游。如果积分服务消费失败了,订单服务无法让订单也回滚,只能靠重试或人工介入。