分布式事务学习与理解
2026-09-30 02:29 ajj40123 阅读(11) 评论(0) 收藏 举报背景:在分布式系统中,由于各服务使用的数据库不一致,很难用单一数据库事务方式来解决事务问题
基本概念
参与角色
TC:事务协调者,协调管理全局事务
TM:事务发起者,负责发起全局事务
RM:事务参与者,负责最终执行本地事务
提交模式
两阶段提交(2PC):
1.准备阶段:TC通知RM准备提交,并获取响应
2.执行阶段
所有RM已经准备好,TC发送提交事务通知,所有RM提交事务
若某一RM未准备好,TC发送回滚通知,所有RM事务回滚
事务分类:
全局事务:由 TM 发起,包含多个分支事务的逻辑整体,对应一个唯一的 XID(分布式事务,包含所有的分支事务)。
分支事务:某个微服务节点(RM)上的本地事务,是全局事务的最小执行单元(又称为本地事务,某个服务的本地数据库事务)。
一致性分类:
强一致性:读写强一致,写什么就读什么
弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
最终一致性:弱一致性的升级版。系统会保证在一定时间内达到数据一致的状态。
解决方案
1. XA协议(数据库协议)
-
每个事务参与者,执行本地事务操作,执行完成后,通知事务协调者,但是不提交
-
汇总事务执行状态,根据结果处理
-
所有参与者执行成功,通知所有参与者提交
-
如果有某个参与者没有执行成功,通知所有参与者回滚
-
-
优劣
-
优点: 解决分布式事务的问题,强一致性
-
缺点:存在事务挂起,资源空耗,性能低,可能导致锁表
-
2. TCC(Try-Confirm-Cancel)
被协调者有三个接口
Try 预留或者锁定资源
Confirm 确认执行
Cancel 释放资源并逆向补偿
分为两阶段提交
准备阶段:TC调用所有服务的Try接口
提交阶段:
TC根据结果,如果所有成功,则调用所有服务的Confirm接口
如果存在失败,则调用所有服务的Cancel接口
-
优点:没有事务挂起,没有性能问题;灵活度很高;
-
缺点:
-
过程复杂,业务接口拆分三个,增加工作量和复杂度;
-
-
3. Seata AT模式
基于数据源 代理拦截 SQL,自动生成 undo_log,按全局事务状态提交或回滚
AT 自动事务处理,Seata 创新的一种非侵入式的分布式事务解决方案
实现方式
在各个事务的数据库中建表 undo_log表
第一阶段: 直接执行业务,seata代理数据源(DataSource),拦截sql执行,提取sql执行前后的数据镜像,将前后数据镜像转化成一条sql,加入到当前本地事务,执行提交,插入到undo_log表中
第二阶段:
如果所有参与者都处理成功,TC 协调所有参与者直接删除undo_log记录
如果存在参与者处理失败,TC协调所有的参与者回滚,参与者从undo_log表中提取前后镜像,计算得到逆向补偿sql,执行sql实现补偿
-
优点:实现分布式事务;没有事务挂起,性能高;操作简单,工作量很小,免费开源;
-
4. Saga
Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
优劣:
优点:适合长事务,无长时间锁
缺点:补偿逻辑复杂
问题1:seata的AT模式全局事务执行过程
1.TM 在TC注册全局事务,TC 开启全局事务并生成 XID返回给TM。
2.TM将XID 透传到下游服务,各服务的 RM 识别到 XID 后注册分支事务。
3.一阶段执行本地事务:RM 拦截 SQL,查询前镜像,执行业务 SQL,查询后镜像,生成 undo_log,将业务数据和 undo_log 在同一个本地事务中提交。
4.提交前获取全局锁:RM 在本地事务提交前向 TC 申请全局锁,获取成功后提交本地事务,释放本地数据库锁,获取失败则回滚本地事务并重试或超时放弃。
5.二阶段段处理
二阶段提交:若全局事务提交,TC 通知各 RM 异步删除 undo_log,过程很快。
二阶段回滚:若全局事务回滚,TC 通知 RM 根据 undo_log 的前后镜像生成反向 sql,执行sql实现补偿。
问题2:seata的AT模式如何实现写隔离?全局锁与本地锁的区别?
举例:
脏写产生:
业务一开启全局事务,其中包含分支事务A(修改 A)和分支事务 B(修改 B)
业务二修改 A,其中业务一执行分支事务 A 先获取本地锁
业务二则等待业务一执行完分支事务 A 之后
获得本地锁修改 A 并入库
业务一在执行分支事务时发生异常了
由于分支事务 A 的数据被业务二修改
导致业务一的全局事务无法回滚。
解决办法:
1.业务二执行时加 @GlobalTransactional注解
业务二在执行全局事务过程中,分支事务 A 提交前注册分支事务获取全局锁时,发现业务业务一全局锁还没执行完,因此业务二提交不了,抛异常回滚,所以不会发生脏写。
2.业务二执行时加 @GlobalLock注解:
与 @GlobalTransactional注解效果类似,只不过不需要开启全局事务,只在本地事务提交前,检查全局锁是否存在。
3.业务二执行时加 @GlobalLock 注解 + select for update语句。
如果加了select for update语句,则会在 update 前检查全局锁是否存在,只有当全局锁释放之后,业务二才能开始执行 updateA 操作
本地锁和全局锁区别
| 特性 | 全局锁 (Global Lock) | 本地锁 (Local Lock) |
|---|---|---|
| 作用范围 | 分布式事务中跨多个分支事务 | 单个本地事务内的数据操作 |
| 实现方式 | 基于数据库唯一约束的逻辑锁,由 TC 管理 | 数据库行锁或事务内隐式锁(如 SELECT FOR UPDATE) |
| 目标 | 防止多个全局事务同时修改同一条数据,避免写-写冲突 | 保证事务执行期间前/后镜像与业务 SQL 的一致性,防止脏写 |
| 使用场景 | 多服务并发修改同一数据,如库存扣减、秒杀场景 | 单表操作或事务内部 SQL 执行期间,确保 undo_log 正确生成 |
| 风险 | 高并发下可能导致锁等待或死锁,需要合理控制事务粒度 | 若锁释放不及时,可能阻塞本地事务或影响性能 |
问题3:缓存与数据库不同步,如何解决?
自答:
1.手工清缓存
2.实时清理缓存:准备一张缓存表,当数据变动时,在缓存表添加记录;实时监控缓存表,若有数据变化,获取缓存数据,执行缓存清理操作
参考答案:
1.同步删除:更新请求→更新数据库→删除缓存
2.延迟双删:更新请求→删除缓存→更新数据库→删除缓存
3.监听binlog删除+重试:更新请求→更新数据库→缓存管理服务订阅binlog→删除缓存消息推送至消息队列→删除缓存→失败重试
浙公网安备 33010602011771号