前提:为什么会出现分布式事务?
在以往的单体项目只有一个数据库,而微服务中有多个数据库。
因为本地事务管不了跨服务、跨数据库的操作。所以,出现分布式事务解决之问题
一.分布式事务基本概念
1.参与者角色。
2.二阶段提交(2pc)
3.全局事务&分支事务
(一)参与者角色
- TC:事务协调者。协调所有参与者,管理全局事务提交或回滚
- TM:事务发起者。发起全局事务,并接受 TC 协调
- RM:事务参与者。参与全局事务,管理本地资源,如本地数据库事务
(二)二阶段提交(2pc)
- 第一阶段:
- TC 通知所有参与者准备提交,参与者执行本地事务但不提交,将准备结果通知 TC
- 第二阶段:
- 如果所有参与者都成功,TC 协调所有参与者提交事务
- 如果存在参与者准备失败,TC 协调所有参与者回滚事务
(三)全局事务&分支事务
- 全局事务:又称分布式事务,包含所有分支事务
- 分支事务:又称本地事务,是某个服务中的本地数据库事务
二.常见的解决方案
1.XA 协议
2.TCC
3.Seata AT 模式
4.Saga
(一)XA 协议
XA 是数据库层面的分布式事务协议,基于 2PC
1.原理
- 步骤一:每个事务的参与者执行本地事务操作,执行完之后通知事务的协调者,但是不提交
- 步骤二:协调者汇总所有参与者状态:
- 全部成功,通知所有参与者提交
- 任意失败,通知所有参与者回滚
2.优点
解决分布式事务的问题
3.缺点
存在事务挂起,资源空耗,性能低下
(二)TCC
TCC是将业务接口拆成三个接口。分别为:Try、Confirm、Cancel
1.三个阶段
三个阶段对应三个接口
- Try:尝试,资源锁定。例如转账时冻结资金
- Confirm:确认,真正执行业务。例如解冻并完成转账
- Cancel:取消,逆向补偿。例如解冻资金
2.两阶段
- 准备阶段:TC调用所有服务的Try接口
- 提交阶段:(TC根据结果调用接口)
- 全部成功,调用 Confirm
- 存在失败,调用 Cancel
3.优点
- 能实现分布式事务
- 没有事务挂起,性能较好
- 灵活度高
4.缺点
- 业务接口一拆三,工作量和复杂度高。
- 需要进行业务改造,工作量大,实现复杂
(三)Seata AT 模式
AT 是 Seata 创新的一种非侵入式分布式事务解决方案。
1.原理
- 前提:在各个事务的数据库中建表 undo_log表
- 第一阶段:
- 直接执行业务,seata代理数据源(DataSource),拦截sql执行,
- 提取sql执行前后的数据镜像,将前后数据镜像转化成一条sql,
- 加入到当前本地事务,执行提交,插入到undo_log表中
- 第二阶段:TC 协调所有参与者
- 全部成功:直接删除
undo_log。 - 存在失败:根据
undo_log中的前后镜像生成逆向补偿 SQL,执行回滚。
- 全部成功:直接删除
(四)Saga
Saga 是长事务解决方案。
- 一阶段:正向服务。
- 二阶段:补偿服务。
三.seata的AT模式全局事务执行过程
第一阶段:全局事务开启与分支事务执行
- TM向TC开启全局事务,TC生成全局唯一XID并返回。
- TM调用下游服务,透传XID。
- 下游RM获取XID,向TC注册分支事务。
- RM执行SQL前,生成前置镜像(Before Image)。
- RM执行SQL,生成后置镜像(After Image),并将两者记录至undo_log。
- 申请全局锁(写隔离核心):RM提交本地事务前,向TC申请该行数据的全局锁。
- 成功:继续执行。
- 失败:重试,超时则本地回滚并抛异常。
- 获取全局锁后,RM提交本地事务并释放本地锁,但继续保持持有全局锁。
第二阶段:全局事务结束与锁释放
- TM根据执行情况,向TC发起全局提交或回滚。
- 全局提交:TC通知RM清理undo_log,释放全局锁。
- 全局回滚:TC通知RM利用undo_log反向补偿回滚,完成后释放全局锁。
核心逻辑:本地事务提交前必须拿到全局锁,且在全局事务彻底结束前不释放,以此防止分布式环境下的脏写。
四.seata的AT模式如何实现写隔离?
- 获取本地锁:分支事务开始时,会先获取数据库的本地锁,执行数据更新操作。
- 申请全局锁:在本地事务提交前,必须向 TC 申请该数据记录的全局锁。
- 等待与重试:如果全局锁已被其他全局事务持有,当前事务会释放本地锁并回滚,然后通过循环不断重试,尝试重新获取本地锁和全局锁,直到超过预设的超时时间。
- 提交与释放:成功获取全局锁后,本地事务才会提交,并释放本地锁。全局锁会一直持有到整个全局事务完成(二阶段结束)后才释放。
五.全局锁与本地锁的区别
- 管理方:全局锁由 Seata TC 管理;本地锁由本地数据库管理。
- 范围:全局锁跨服务;本地锁只在单个服务内。
- 粒度:全局锁锁数据行;本地锁锁连接或行。
- 释放时机:全局锁等整个全局事务结束才释放;本地锁本地事务一提交或回滚就释放。
- 目的:全局锁防止全局事务之间脏写;本地锁保证本地事务 ACID。
核心区别:全局锁管分布式写隔离,本地锁管单机事务一致性。
六.缓存与数据库不同步如何解决?
- Cache Aside(最常用)
- 读:先读缓存,未命中读数据库,再回填缓存,并设过期时间。
- 写:先更新数据库,再删除缓存。
- 不要更新缓存,直接删,避免并发写覆盖。
- 延迟双删
- 更新数据库后删一次缓存,延迟几百毫秒再删一次。
- 用来应对并发读回填旧数据、主从延迟等问题。
- 订阅 binlog + MQ(高可靠)
- 用 Canal 等监听 MySQL binlog,发消息到 MQ,消费者再删缓存。
- 业务解耦,失败可重试,保证最终一致。
- 分布式锁 / 版本号
- 同一 key 的读写串行化,或缓存带版本号做 CAS。
- 适合强一致场景,但性能差。
- 过期时间兜底
- 所有缓存都设 TTL,即使短期不一致,最终也会恢复。