
分布式事务是指在分布式系统中一个业务操作需要跨越多个服务、数据库或消息队列等多个资源管理器且这些操作必须作为一个整体要么全部成功要么全部失败。为了实现分布式事务我们先了解三大参与者角色。一、三大参与者角色TCTransaction Coordinator事务协调者协调所有参与者管理全局事务。TMTransaction Manager事务发起者发起全局事务接收 TC 协调。RMResource Manager事务参与者参与全局事务。它们之间的关系TM 向 TC 申请开启全局事务 → TC 生成 XID → RM 执行业务时携带 XID 向 TC 注册分支 → TC 根据所有 RM 的汇报决定最终命运。二、两个核心概念全局事务由 TM 发起包含多个分支事务的逻辑整体对应一个唯一的 XID分布式事务包含所有的分支事务。分支事务某个微服务节点上的本地事务是全局事务的最小执行单元又称为本地事务某个服务的本地数据库事务。三、2PC 和 3PC3.1 二阶段提交2PC2PC 是分布式系统中实现强一致性最经典的协议它的核心思想是将事务的决策和执行分离通过一个协调者来统一管控所有参与者的状态。工作流程第一阶段准备阶段TC 通知所有的参与者准备提交将准备的结果通知 TC。详细TC 发起询问TC 向所有参与者发送 prepare 请求。RM 执行并锁定参与者执行本地事务操作写 Redo/Undo 日志但不真正提交。此时锁住相关资源进入预备状态。RM 响应如果执行成功回复 Yes失败或超时回复 No。第二阶段提交/回滚阶段所有参与者都准备成功TC 协调所有参与者提交事务如果存在参与者准备失败TC 协调所有参与者回滚事务。详细全部 Yes只有当 TC 收到所有参与者的 Yes 时才向所有参与者发送 Commit 指令。参与者正式提交事务释放锁。任一 No 或超时只要有一个参与者回复 No 或超时未响应TC 就向所有参与者发送 Rollback 指令。参与者回滚事务释放锁。三大致命缺点同步阻塞最大痛点在第一阶段结束后、第二阶段开始前所有参与者都持有数据库锁且处于等待状态。此时任何其他事务都无法访问这些资源性能极差。单点故障如果 TC 在第二阶段宕机所有参与者会因为收不到 Commit/Rollback 指令而无限期持锁等待导致整个系统瘫痪。数据不一致风险在第二阶段下发 Commit 指令时如果发生网络分区部分参与者收到了 Commit 并提交了另一部分没收到而回滚了就会导致数据不一致。2PC 总结2PC 是一种“保守策略”只要有任何不确定性超时、故障就默认回滚。它保证了原子性但牺牲了可用性和性能。XA 协议就是 2PC 的标准工业实现。3.2 三阶段提交3PC3PC 是为了解决 2PC 的同步阻塞和单点故障问题而提出的理论改进方案它在 prepare 和 commit 之间插入了一个缓存阶段并引入了超时机制。工作流程第一阶段CanCommit询问阶段TC 向所有参与者发送 CanCommit 请求询问“当前条件是否允许执行事务”参与者只做轻量级检查如连接是否正常、资源是否存在不执行任何实际操作也不锁资源。回复 Yes 或 No。此阶段失败成本极低。第二阶段PreCommit预提交阶段如果全部 YesTC 发送 PreCommit 指令。参与者执行事务操作写日志锁定资源但不最终提交。关键改进参与者进入 PreCommit 状态后会启动一个超时计时器。如果在规定时间内没收到第三阶段的指令参与者会根据预设策略自动决定是继续提交还是回滚不再傻等。第三阶段DoCommit真正提交阶段如果 TC 收到所有参与者的 PreCommit 确认发送 DoCommit 指令。参与者正式提交事务释放锁清除超时计时器。如果任一环节失败TC 发送 Abort 指令参与者回滚。相比 2PC 的改进引入超时机制解决了 TC 宕机导致的无限阻塞问题参与者可以“自救”。增加 CanCommit 阶段提前过滤掉明显无法执行的节点减少无效的资源锁定。为什么工程中几乎不用 3PC网络分区下的不一致如果在 PreCommit 阶段后发生网络分区一部分参与者与 TC 失联触发超时根据策略自动提交了事务。另一部分参与者收到了 TC 的 Abort 指令回滚了事务。结果数据不一致四、四大解决方案4.1 XA 协议数据库级强一致每个事务参与者执行本地事务操作执行完成后通知事务协调者但是不提交。TC 汇总事务执行状态根据结果处理如果所有参与者执行成功通知所有参与者提交。如果有某个参与者没有执行成功通知所有参与者回滚。优劣优点解决了分布式事务的问题。缺点存在事务挂起、资源空耗、性能低下。4.2 TCCTry-Confirm-Cancel三个阶段对应三个接口所有业务接口一拆三Try尝试资源锁定以转账为例冻结资金update 冻结余额-可用余额 where user_id1; commit;通知 TC TrysuccessConfirm确认解冻资金update -冻结余额可用余额 where user_id1;执行转账update 可用余额 where user_id转入方update -可用余额 where user_id转出方insert into 转账记录commit;Cancel取消逆向补偿操作解冻资金update -冻结余额可用余额 where user_id1; commit;两阶段准备阶段TC 调用所有服务的 Try 接口。提交阶段TC 根据结果如果所有成功则调用所有服务的 Confirm 接口。如果存在失败则调用所有服务的 Cancel 接口。优劣优点能实现分布式事务没有事务挂起没有性能问题灵活度很高。缺点过程复杂业务接口拆分三个增加工作量和复杂度。需要进行业务改造工作量大实现复杂。4.3 Seata AT 模式Apache Seataincubating是一款开源的分布式事务解决方案致力于在微服务架构下提供高性能和简单易用的分布式事务服务。AT 自动事务处理Seata 创新的一种非侵入式的分布式事务解决方案。实现方式在各个事务的数据库中建表 undo_log 表。第一阶段直接执行业务Seata 代理数据源DataSource拦截 SQL 执行提取 SQL 执行前后的数据镜像将前后数据镜像转化成一条 SQL加入到当前本地事务执行提交插入到 undo_log 表中。第二阶段如果所有参与者都处理成功TC 协调所有参与者直接删除 undo_log 记录。如果存在参与者处理失败TC 协调所有的参与者回滚参与者从 undo_log 表中提取前后镜像计算得到逆向补偿 SQL执行 SQL 实现补偿。优劣优点实现分布式事务没有事务挂起性能高操作简单工作量很小。缺点每个数据库要建一张表灵活度不高无法处理复杂事务。4.4 Saga 模式Saga 模式是 Seata 提供的长事务解决方案。在 Saga 模式中业务流程中每个参与者都提交本地事务当出现某一个参与者失败则补偿前面已经成功的参与者一阶段正向服务和二阶段补偿服务都由业务开发实现。五、Seata AT 模式详解5.1 Seata AT 全局事务执行过程TM 开启全局事务业务入口方法标注GlobalTransactional。TM 向 TC 申请开启全局事务。TC 创建全局事务并生成全局唯一 XID。XID 传播XID 通过 RPC 上下文传播到各个微服务。各微服务中的 RM 携带 XID 参与全局事务。RM 一阶段执行Seata 代理数据源拦截业务 SQL。解析 SQL查询执行前镜像before image。执行业务 SQL。查询执行后镜像after image。将 before/after 镜像写入undo_log。向 TC 注册分支事务并申请全局锁。提交本地事务释放本地锁。注意此时本地事务已经提交但全局锁仍由 TC 持有直到全局事务结束。TC 决策所有分支事务执行成功TM 向 TC 发起全局提交。任一分支失败TM 向 TC 发起全局回滚。二阶段提交TC 通知各 RM 全局提交。RM 异步删除undo_log。TC 释放全局锁。二阶段回滚TC 通知已成功分支 RM 回滚。RM 读取undo_log校验 after image 与当前数据是否一致。根据 before image 生成反向补偿 SQL。执行补偿恢复数据。删除undo_log。释放全局锁。关键点AT 的一阶段本地事务已经提交二阶段回滚不是数据库层面的事务回滚而是基于undo_log的补偿回滚。因此补偿逻辑必须尽量保证幂等。5.2 Seata AT 模式如何实现写隔离Seata AT 通过全局锁实现写隔离。核心过程RM 在一阶段本地事务提交前向 TC 注册分支事务并申请该行数据的全局锁。TC 维护全局锁记录锁的维度通常是resourceId tableName primaryKey。如果该行数据已经被其他全局事务持有全局锁当前分支申请失败。当前分支会按配置重试例如client.rm.lock.retryInterval10client.rm.lock.retryTimes30如果重试后仍无法获得全局锁则本地事务回滚全局事务最终回滚。只有拿到全局锁本地事务才提交。全局事务提交或回滚完成后TC 释放全局锁。这样不同全局事务不能同时修改同一行数据从而避免跨服务的脏写。需要注意Seata AT 默认提供的是全局读未提交写操作有隔离。如果读操作也要求隔离可以使用SELECT ... FOR UPDATE或GlobalLock等方式让读操作也检查全局锁。5.3 全局锁与本地锁的区别在 Seata AT 模式中全局锁和本地锁是两个容易混淆但又完全不同的概念。本地锁由数据库自身管理保证单个本地事务内的并发安全而全局锁由 TC 统一协调保证跨服务的全局事务写隔离。下面从多个维度对比它们的区别维度全局锁本地锁管理方TC 事务协调者本地数据库如 InnoDB作用范围跨服务、跨数据库单个本地事务、单个数据库锁对象逻辑行通常是表名 主键值数据库物理行锁、表锁等生命周期从分支申请到全局事务提交/回滚完成本地事务提交或回滚后释放释放时机全局事务结束后由 TC 释放本地事务提交/回滚时由数据库释放目的防止跨全局事务的脏写实现写隔离防止本地事务并发冲突性能影响跨服务协调可能重试等待数据库内部管理相对高效核心区别管理方不同本地锁由数据库如 InnoDB自行管理全局锁由 TC 事务协调者统一维护。作用范围不同本地锁只作用于单个数据库内的本地事务全局锁跨越多个服务、多个数据库。锁对象不同本地锁锁的是数据库物理行、表等资源全局锁锁的是逻辑行维度通常是resourceId tableName primaryKey。生命周期不同本地锁在本地事务提交或回滚后立即释放全局锁从分支申请开始一直持有到整个全局事务提交或回滚完成才由 TC 释放。目的不同本地锁防止单个数据库内的并发冲突全局锁防止不同全局事务跨服务修改同一行数据导致的脏写。为什么需要两层锁在 Seata AT 模式的一阶段RM 执行本地事务时数据库会加本地锁保证本地事务的并发安全同时 RM 向 TC 申请全局锁保证跨服务的写隔离。本地事务提交后本地锁立即释放但全局锁仍由 TC 持有直到全局事务结束。这样既保证了本地事务的高效并发又避免了跨服务的脏写问题。一句话总结本地锁解决单库本地事务并发全局锁解决跨服务全局事务写隔离。六、缓存与数据库不同步如何解决缓存与数据库不同步通常追求最终一致而不是强一致。数据库是权威数据源缓存只是加速读。6.1 常见问题先更新数据库再更新缓存。先删除缓存再更新数据库。先更新数据库再删除缓存。并发读写导致旧值回填。删除缓存失败。主从复制延迟导致读到旧数据。6.2 推荐方案1. 先更新数据库再删除缓存这是最常见的 Cache Aside 模式。流程更新数据库。删除缓存。后续请求发现缓存不存在从数据库加载新值并回填缓存。为什么不推荐直接更新缓存直接更新缓存存在两个明显问题写操作频繁导致缓存被无效更新如果业务中写多读少直接更新缓存会让缓存频繁写入而真正被读取的次数很少造成缓存资源浪费。并发写导致缓存与数据库不一致多个线程并发更新数据库和缓存时由于执行顺序不确定可能出现数据库最终是新值、缓存却是旧值的情况且这种错误很难被自动纠正。相比之下先更新数据库再删除缓存即使删除缓存失败也只会让下一次读取拿到旧值并回填最终仍能通过重试或过期策略收敛到一致状态因此更稳妥。2. 删除缓存失败怎么办删除缓存失败是 Cache Aside 模式最典型的隐患。如果更新数据库成功、删除缓存失败缓存中会一直保留旧值导致后续请求持续读到脏数据。常见应对方案如下重试机制删除失败后立即重试几次仍失败则放入消息队列由后台任务异步重试删除直到成功为止。设置过期时间给缓存设置合理的 TTL过期时间即使删除失败缓存到期后也会自动失效从数据库重新加载新值最终达到一致。延迟双删更新数据库后先删除一次缓存间隔一段时间如几百毫秒后再删除一次。这样可以兜底处理并发读请求把旧值回填进缓存的情况。3. 并发读写导致旧值回填怎么办并发场景下即使采用先更新数据库再删除缓存的策略仍可能出现旧值回填线程 A 更新数据库为新值线程 B 在删除缓存前读到了旧值并把旧值写回缓存导致缓存里又变成旧数据。解决思路主要有两种延迟双删删除缓存后等待一小段时间再删一次确保并发读请求已经完成回填第二次删除把旧值清掉。版本号或时间戳校验缓存中同时保存数据的版本号或更新时间回填时校验版本只有新版本才允许写入缓存从源头避免旧值覆盖新值。4. 主从复制延迟导致读到旧数据怎么办在读写分离架构下更新主库成功后从库可能还没同步完成。此时如果删除缓存后续请求会去从库读取可能读到旧值并回填缓存造成缓存与主库不一致。常见应对方式延迟双删删除缓存后等待主从同步的延迟时间通常几百毫秒再删除一次确保从库已同步完成避免旧值回填。强制读主库对一致性要求高的场景在缓存失效后强制走主库读取避免从库延迟带来的旧数据。缩短主从延迟优化同步策略、提升从库性能尽量缩短复制延迟窗口。5. 总结与对比综合来看几种常见策略的对比可以归纳如下策略核心思路优点缺点先更新数据库再删除缓存更新库后删缓存读时回填实现简单是主流 Cache Aside 模式删除失败或并发回填可能短暂不一致先删除缓存再更新数据库先删缓存再改库实现简单更新库期间读请求会穿透到数据库压力大先更新数据库再更新缓存更新库后直接写缓存缓存始终有值读命中率高并发写易导致缓存与库不一致且写多读少时浪费资源延迟双删删缓存后延迟再删一次能兜底并发回填和主从延迟问题实现稍复杂延迟时间需要合理设置消息队列异步重试删除删除失败后异步重试保证最终删除成功可靠性高引入消息队列增加系统复杂度一句话总结缓存与数据库的同步没有银弹工程上最常用的是先更新数据库再删除缓存并配合延迟双删、过期时间、异步重试等手段把不一致的窗口压缩到最小最终达到最终一致。