学习分布式事务

前言

在使用 go-zero 微服务框架中遇到了需要使用分布式事务的场景:

有一签到任务的业务流程,在用户发出签到请求后,签到任务微服务会在签到数据库表中新增一条签到信息,然后会去请求用户服务中的修改用户积分操作(签到后得积分),需要保持该操作的 ACID,由于这两个步骤在两个微服务中,所使用的数据库不同,于是开始了解分布式事务。

两阶段提交协议

两阶段提交协议是一种分布式事务协调协议,用于保证分布式系统中事务的原子性,即所有参与节点要么都提交事务,要么都回滚事务。

协调者 参与者 准备 yes/no 提交/回滚 ACK 协调者 参与者

阶段一:准备阶段(Prepare)

  • 协调者:向所有参与者发送 Prepare 请求,并等待参与者的响应。
  • 参与者:接收到 Prepare 请求后,执行事务操作(但不真正提交),记录 undo 和 redo 日志(用于回滚和恢复)。如果事务操作执行成功,向协调者返回 Yes 响应,表示可以提交事务;如果执行失败,返回 No 响应,表示需要回滚事务。

阶段二:提交阶段(Commit)

  • 协调者收到所有 Yes 响应的情况:协调者向所有参与者发送 Commit 请求。参与者接收到 Commit 请求后,正式提交事务,释放事务执行过程中占用的资源,并向协调者返回 Ack(确认)消息。协调者收到所有参与者的 Ack 消息后,事务结束。
  • 协调者收到 No 响应的情况:协调者向所有参与者发送 Rollback 请求。参与者接收到 Rollback 请求后,根据 undo 日志回滚事务,释放资源,并向协调者返回 Ack 消息。协调者收到所有参与者的 Ack 消息后,事务结束。

缺陷

  • 最大缺点是在执行过程中节点都处于阻塞状态。也就是节点之间在等待对方的响应消息时,什么也做不了。特别是如果某个节点在已经占有了某项资源的情况下,为了等待其他节点的响应消息而陷入阻塞状态时,当第三个节点尝试访问该节点占有的资源时,这个节点也会连带着陷入阻塞状态。

  • 协调者也是关键,如果协调者崩溃,整个分布式事务都无法执行。所以,如果协调者是单节点,那么就容易出现单节点故障。而且协调者采用保守策略,如果一个节点在第一阶段没有返回响应,那么协调者会执行回滚。所以这可能会引起不必要的回滚。

  • 在第二阶段,协调者发送 Commit 的时候,参与者没有收到,协调者会不断重试,直到请求发送成功。

  • 如果参与者已经收到了 Commit 请求,但是在提交之前就宕机了,参与者在恢复过来之后会查看自己本地的日志,看有没有收到 Commit 指令,如果已经收到了,就会使用 Redo 信息来提交事务。

两阶段提交协议是分布式事务中最常用的协议之一,它可以有效地保证分布式事务的一致性和可靠性

三阶段提交协议

三阶段提交协议是在两阶段提交协议基础上的改进,通过增加一个额外阶段,减少两阶段协议引起的事务失败的可能,提高了系统的容错性。比较容易出现的一个情况是一个参与者在准备阶段把 Redo、 Undo 写好,另外一个参与者执行不了事务,要回滚。那么这个参与者准备阶段就白费了。

阶段一:CanCommit 阶段

  • 协调者:向所有参与者发送 CanCommit 请求,询问是否可以执行事务。
  • 参与者:接收到 CanCommit 请求后,检查自身资源状态等,判断是否有能力执行事务。如果可以,返回Yes响应;如果不行,返回No响应。

阶段二:PreCommit 阶段

  • 协调者收到所有 Yes 响应的情况:协调者向所有参与者发送 PreCommit 请求,参与者接收到 PreCommit 请求后,执行事务操作(但不真正提交),记录 undo 和 redo 日志,然后向协调者返回 Ack 消息。
  • 协调者收到 No 响应的情况:协调者向所有参与者发送 Abort 请求,参与者接收到 Abort 请求后,不执行事务操作,直接返回 Ack 消息。

阶段三:DoCommit 阶段

  • 协调者收到所有Ack响应(来自 PreCommit 阶段)的情况:协调者向所有参与者发送 DoCommit 请求,参与者接收到 DoCommit 请求后,正式提交事务,释放资源,并向协调者返回 Ack 消息。协调者收到所有参与者的 Ack 消息后,事务结束。
  • 协调者未收到所有 Ack 响应(来自 PreCommit 阶段)的情况:协调者向所有参与者发送 Rollback 请求,参与者接收到 Rollback 请求后,根据 undo 日志回滚事务,释放资源,并向协调者返回 Ack 消息。协调者收到所有参与者的 Ack 消息后,事务结束。

三阶段比二阶段多增加了一项检查参与者自身资源状态等,是否可以执行事务的阶段。但是在目前,三阶段提交协议并没有两阶段提交协议使用得那么广泛,原因有两个,一是两阶段提交协议已经足以解决大部分问题了,二是三阶段提交协议的收益和它的复杂度比起来, 性价比低。

XA 事务

XA 事务即 eXtended Architecture 事务 ,是 X/Open 组织提出的分布式交易处理规范,也是一种分布式事务协议。

工作原理

基于两阶段提交协议:

  1. 准备阶段:事务管理器给每个资源管理器发送准备指令,资源管理器执行事务操作(但不真正提交 ),记录 undo 和 redo 日志用于回滚和恢复。然后向事务管理器汇报是否准备好,比如数据库会锁定相关资源并检查能否提交事务。
  2. 提交阶段:若事务管理器收到所有资源管理器 “准备好” 的回复,就向它们发送提交指令,资源管理器正式提交事务并释放资源;若有一个资源管理器回复 “未准备好”,事务管理器则发送回滚指令,资源管理器根据 undo 日志回滚事务。
  • 资源管理器(RM):通常由数据库充当 ,像 Oracle、DB2、MySQL 等商业数据库都实现了 XA 接口 ,负责提供访问事务资源的方法,具体执行数据的存取和修改操作。
  • 事务管理器(TM):作为全局调度者,协调参与全局事务的各个事务,与所有资源管理器通信,决定事务是提交还是回滚。

优势

  • 确保数据一致性:通过严格的两阶段提交机制,保证分布式事务中所有参与者要么都提交,要么都回滚,避免数据不一致问题。
  • 支持跨数据库事务处理:允许在多个不同数据库系统间进行事务操作,应用程序能跨库操作且不用担心一致性,通过标准化接口实现不同数据库无缝集成。
  • 提高系统可靠性和可扩展性:事务管理器可依系统负载动态调整参与者数量和分布,提升整体性能与可扩展性。

TTC 事务

TCC 事务是 “Try - Confirm - Cancel” 的缩写 ,是一种分布式事务解决方案,用于保障分布式系统中事务的一致性。

工作机制

  • Try(尝试执行)阶段:完成业务可执行性检查,并预留事务所需资源。即对应于两阶段提交协议的准备阶段,执行事务但是不提交。
  • Confirm(确认执行)阶段:若所有参与事务的服务在 Try 阶段都成功,就进入此阶段。该阶段直接使用 Try 阶段预留的资源完成业务处理,不再进行业务检查。即对应于两阶段提交协议第二阶段的提交步骤。
  • Cancel(取消执行)阶段:当 Try 阶段有服务执行失败,或因业务逻辑需撤销事务时执行。该阶段释放 Try 阶段预留的资源,使资源回到事务开始前状态。即对应于两阶段提交协议第二阶段的回滚步骤。

之所以称为TCC,完全是因为 TCC 强调的是业务自定义逻辑。也就是说 Try 是执行业 务自定义逻辑,Confirm 也是执行业务自定义逻辑,Cancel 同样如此。开发人员可依业务需求定制每个阶段逻辑,针对不同业务场景灵活处理分布式事务。

TCC 在微服务架构里面比较常用,Try 对应一个微服务调用,Confirm 对应一个微服务调用,Cancel 也对应一个微服务调用。不过一些分库分表中间件也支持 TCC 模式,但是比较罕见。

TCC 与本地事务

在微服务架构中 Try-Confirm-Cacel 都对应一个微服务调用,TCC 的任何一个步骤都可以是本地事务。

在 TCC 里面,Try 可以是一个完整的本地事务,Confirm 也可以是一个完整的本地事务,Cancel 同样可以是一个完整的本地事务。

协调者 参与者1 参与者2 Try Try 插入数据并提交 status=init 插入数据并提交 status=init OK OK Confirm Confirm 更新数据并提交 status=active 更新数据并提交 status=active OK OK 协调者 参与者1 参与者2

比如在某个业务里面,Try 本身就是插入数据,但是处于初始化状态,还不能使用。后续 Confirm 的时候就是把状态更新为可用,而 Cancel 则是更新为不可用,当然直接删除也是可以的。

不过 TCC 怎样都是会出错的,比如说在 Confirm 阶段出错或者出现超时,所以搞不清楚究竟有没有提交的。

容错

正常来说,TCC 里面 T 阶段出错是没有关系的。比如说前面的那个例子里,数据处于初始化状态的时候,其实后续业务是用不了的,也就不会有问题。但是如果在 Confirm 的时候出错了,问题就比较严重了。比如说一部分业务已经将数据更新为可用了,另外一部分业务更新数据为可用失败,那么就会出现不一致的情况。

基本上这里就是只能考虑不断重试,确保在 Confirm 阶段都能提交成功。毫无疑问,不管怎么重试,最终都是要失败的,所以要做好监控和告警的机制。

  • 搞一个离线比对数据并修复的方案,就是用来查找这种相关联的数据的,一部分数据还处于初始化状态,但是一部分数据已经处于可用状态,然后将可用状态修复为初始化状态。

  • 另外一个方案则是在读取数据的时候,如果发现数据不一致,那么就丢弃这个数据,同时触发修复逻辑。在一些业务场景下,读请求是能够发现这种数据不一致的。那么它就会立刻丢弃这个数据,并且触发修复程序。

TCC 整体来说是追求最终一致性的,和它类似的是 SAGA 事务,也是一个追求最终一致性的事务解决方案,也不满足 ACID 的要求。

SAGA 事务

SAGA 把业务分成一个个步骤,当某一个步骤失败的时候,就反向补偿前面的步骤。反向补偿是指插入的时候已经提交了,然后在业务失败的时候执行删除。

调度器 步骤1 步骤2 步骤1 begin 插入数据 confirm OK 步骤2 失败 失败 反向补偿 删除数据 调度器 步骤1 步骤2

SAGA 的核心思想是反向补偿事务中已经成功的步骤。比如说某个业务,需要在数据库1 和数据库 2 中都插入一条数据,那么在数据库 1 插入之后,数据库 2 插入失败,那么就要删除原本数据库 1 的数据。但是要注意,在最开始数据库 1 插入的时候,事务是已经被提交了的。

AT 事务

比较流行的 AT 模式可以看作是 SAGA 的一种特殊形态,或者说简化形态。AT 是指如果你操作很多个数据库,那么分布式事务中间件会帮你生成这些数据库操作的反向操作。

AT 模式的核心是分布式事务中间件会帮你生成数据库的反向操作,比如说 INSERT 对应的就是 DELETE,UPDATE 对应的就是 UPDATE,DELETE 对应的就是 INSERT。这个机制有点类似于 undo log。

同样地,AT 事务也有容错的问题,它的容错和 SAGA 一样,都是在反向补偿的时候出错了该怎么办。

延迟事务

在分库分表中间件眼里,当你执行 Begin 的时候,它是无法预测你接下来会在哪些数据库上面开启事务的。比如说,在同一个场景下,某个请求过来,你处理的时候在分库分表中间件上调用了 Begin 方法,这个请求最终在 user_db_0 和 user_db_1 上开启了事务;但是另外一个请求过来,因为参数不同,它可能最终在 user_db_2 上开启了事务。

所以中间件只有两个选择,要么在 Begin 的时候就在全部数据库上开启事务,要么就是延迟到执行具体 SQL 的时候,知道要在哪些数据库上执行,再去开启事务。而在 Begin 的时候就直接开启事务过于粗暴,毕竟后面有些 DB 根本不会有任何查询。

  • 默认情况下,我们使用的是延迟事务。在正常的情况下,当我们执行 Begin 的 时候,其实并不知道后续事务里面的查询会命中哪些数据库和数据表,那么只 有两个选择,要么 Begin 的时候在所有的分库上都开启事务。但是这会浪费一 些资源,毕竟事务不太可能操作所有的库,因此才有了延迟事务。也就是在 Begin 的时候,分库分表中间件并没有真的开启事务,而是直到执行 SQL 的时 候,才在目标数据库上开启事务。
  • 举例来说,如果 SQL 命中了数据库 db_0,这个时候 db_0 还没有开始事务, 那么就会直接开启事务,然后执行 SQL;如果又来了一个 SQL,再次命中了 db_0,此时 db_0 上已经开启了事务,因此直接使用已有的事务。在提交或者 回滚的时候,就提交或者回滚所有开启的事务。不过提交或者回滚的时候,部分失败的问题比较难以解决
  • 部分失败并没有更好的解决办法。我们这里就是在 Commit 的时候,如果发现 某个数据库失败了,那么会立刻发起重试。如果连续重试失败,就会触发告警,人工介入处理。

总结

学习掌握两阶段提交协议、三阶段提交协议和 XA 协议的基本步骤这几个重要的知识点。到分布式事务的具体解决方案上,如果是跨服务的分布式事务,那么可以考虑 TCC、SAGA 和 AT。注意讨论容错部分。

容错基本上就是重试,重试失败之后就有三种方案,分别是:

  1. 监控 + 告警 + 人手工介入处理;
  2. 读请求 + 数据修复;
  3. 监控 + 告警 + 故障自动处理。

如果是单纯的分库分表跨库事务,那么可以考虑延迟事务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值