单库拆分为分库分表之后,一个巨大的挑战就是本地事务变成了分布式事务。事实上,即使没有分库分表,在微服务架构之下我们也还是会面临分布式事务的问题。
前置知识
分布式事务既可以是纯粹多个数据库实例之间的分布式事务,也可以是跨越不同中间件的业务层面上的分布式事务。前表一般是分库分表中间件提供支持,后者一般是独立的第三方中间件提供支持,比如Seata。在面试的时候,要根据上下文确定面试官问你的分布式事务是哪一类。
先介绍分布式事务中几个比较常用的协议
两阶段提交
两阶段提交协议(Two Phase Commit)是分布式事务中的一种常见协议,算法思路可以概括为参与者将操作成败通知给协调者,再由协调者根据所有参与者的反馈情况决定各参与者要提交操作还是中止操作。
可以分为两个阶段:准备阶段和提交阶段
- 准备阶段:协调者让参与者执行事务,但是并不提交,协调者返回执行情况。这个阶段参与者会记录
Redo,Undo
信息,用于后续提交和回滚 - 提交阶段:协调者根据准备阶段的情况,要求参与者提交或者回滚,参与者返回提交或回滚的结果。准备阶段任何一个节点执行失败了,就都会回滚。全部执行就提交。
两阶段提交协议缺点很多。最大缺点是在执行过程中节点都处于阻塞状态,也就是节点之间在等待对方的响应消息时,什么也做不了。特别是如果某个节点在已经占有了某项资源的情况下,为了等待其他节点的响应消息而陷入阻塞状态时,当第三个节点尝试访问该节点占用的资源时,这个节点也会连带着陷入阻塞状态。
此外,协调者也是关键,如果协调者崩溃,整个分布式事务都无法执行。所以,如果协调者是单节点,那么就容易出现单节点故障。而且协调者采用保守策略。如果一个节点在第一阶段没有响应,那么协调者会执行回滚。所以可能会引起不必要的回滚。
这里还有一个问题,很少有人会想到。如果第二阶段,协调者发送Commit的时候,参与者没有收到会怎么样?
那么协调者会不断重试,直到请求发送成功。
但是如果参与者已经收到了Commit请求,但是在提交前就宕机了又该怎么样?
参与者在恢复过来之后会查看自己本地的日志,看有没有收到Commit的指令,如果已经收到了,就会使用Redo信息来提交事务。
总的来说,两阶段提交协议是分布式事务中最常用的协议之一,它可以有效地保证分布式事务地一致性和可靠性。
三阶段提交
三阶段提交协议是在两阶段协议地基础上进行地改进,三阶段提交协议引入了一个额外阶段来确保执行事务之前有足够的资源,减少两阶段协议引起的事务失败的可能。
在两阶段协议里面,比较容易出现的一个情况就是参与者在准备阶段辛辛苦苦把Redo,Undo
写好,结果另外一个参与者说自己这边执行不了事务,要回滚。那么这个参与者就白费功夫了。
因此在两阶段提交的基础上,三阶段提交引入了一个新阶段,协调者会先问一下参与者能不能执行这个事务。所以,整个三阶段提交协议的三个阶段是这样的
- 第一阶段
CanCommit
:协调者问一下各个参与者能不能执行事务。参与者这时候一般是检查一下自己有没有足够的资源。 - 第二阶段
PreCommit
:类似两阶段提交的第一个阶段,执行事务但是不提交 - 第三阶段
Commit
:直接提交或回滚
目前来看,三阶段提交协议并没有两阶段提交协议使用得那么广泛,原因有两个:一是两阶段提交协议已经足以解决大部分问题了,二是三阶段提交得收益和它得复杂度比起来,性价比有点低
XA事务
XA事务遵循了两阶段提交协议,我个人认为,两阶段协议是一种学术理论,而XA则是把两阶段提交协议具象化后得一个标准。它定义了协调者和参与者之间的接口。用专业的术语来说,就是定义了事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口。
小结
在提交阶段,协调者会不断重试直到把 Commit 请求发送给协调者;协调者如果在提交阶段中途崩溃,也要确定是否需要提交或者回滚。那么你就应该可以理解,在重试成功之前,或者在协调者恢复过来重新提交或者回滚之前,数据是不一致的。所以我个人倾向 XA 不满足 ACID。但是相比其他的方案,它更加接近 ACID。
面试准备
- 如果公司使用了分库分表,是否允许跨库事务?
- 如果允许跨库事务,那么是如何解决的?
- 如果你使用了分库分表中间件,那么它支持哪些类型的事务
- 在微服务层面,使用的是什么样的分布式事务方案?是TCC、SAGA还是AT?
- 在使用分布式事务的时候,中间步骤出错了怎么办?
最好收集一些实际的案例,在面试的时候作为证据
面试微服务架构的时候就可能面到分布式事务,面试可能会问这两个问题?
- 在单体应用拆分为微服务架构之后,怎么解决分布式事务?
- 服务是共享一个数据库吗?不是的话,怎么解决分布式事务问题
在分库分表里也会有类似的问法。
- 在单库拆分之后,怎么解决分布式事务问题
- 当你开启一个事务的时候,分库分表中间件做了什么
- 怎么在分库分表的事务里保证ACID
有些时候面试官不会直接问分布式事务,而是问你数据一致性的问题,其实基本上也是问的分布式事务。他可能这样问:“如果你的 DELETE 语句,经过分库分表之后要删除多张表的数据,那你怎么保证数据一致性?”
所以对于数据一致性的问题,你也要做好准备。其实面试翻车的一个主要原因就是你不熟悉各种异常情况的处理方案,所以在接下来各种方案里面,容错都是一个比较重要的部分。
基本思路
一句话总结,就是既想要ACID,又想要分布式事务,以目前的条件来说基本不可能。所以所有解决分布式事务的方案,立足点都是最终一致性。因此不管从哪里提到了分布式事务,如果面试官问起来,你都可以先从理论上强调这一点。
分布式事务或者说跨库事务基本上都只能依赖于最终一致性,ACID 是不太可能的。比如说常见的 TCC、AT、SAGA,又或者比较罕见的延迟事务,其实都是追求最终一致性。
这里提到了 TCC、AT 和 SAGA 这些比较具体的方案,你就可以根据后面的内容来进一步解释,或者等面试官询问。
注意,如果面试官认为 XA 是支持 ACID 的,那么他可能会问:“难道没有什么能够保证 ACID 吗?”通过这种问题你就可以知道面试官的倾向了,那么你就可以抛开个人立场,回答 XA 事务。
有,XA 事务可以看作支持 ACID。
如果面试官直接问 XA,那么你就可以按照自己的真实想法来回答。
TCC事务
TCC是一个追求最终一致性的,而不是严格一致性的事务解决方案,他不满足ACID要求。
TCC是Try Confirm Cancel
的缩写,他勉强也算是两阶段提交协议的一种实现。
Try
:对应于两阶段提交协议的准备阶段,执行事务但是不提交Confirm
:对应两阶段提交协议第二阶段的提交步骤Cancel
:对于两阶段提交协议的回滚步骤
之所以给它一个新名字,完全是因为TCC强调的是业务自定义逻辑。也就是说Try
是执行业务自定义逻辑,Confirm
也是执行业务自定义逻辑,Cancel
也是。
TCC在微服务架构里比较常用,这三个各自对应一个微服务调用。不过一些分库分表中间件也支持TCC模式,但是比较罕见。
接下来可以从两个角度深入讨论
亮点1:TCC和本地事务
在微服务架构中Try-Confirm-Cancel
都对应一个微服务调用,你就可以猜测到,TCC的任何一个步骤都可以是本地事务。
在TCC里面,Try可以是一个完整的本地事务,Confirm 也可以是一个完整的本地事务,Cancel 同样可以是一个完整的本地事务。
比如在我的某个业务里面,Try 本身就是插入数据,但是处于初始化状态,还不能使用。后续 Confirm 的时候就是把状态更新为可用,而 Cancel 则是更新为不可用,当然直接删除也是可以的。
不过TCC怎样都是是出错的,比如说在Confirm
阶段出错或出现超时,所以你搞不清楚究竟有没有提交的。这里可以补一句,引出下面的亮点。
TCC用起来还是比较简单的,但是要想做好容错还是很不容易的。
亮点2:容错
容错很多时候就是重试,重试失败之后人工介入或者引入自动故障处理机制,后续尝试修复数据。
面试的时候需要一步步分析,首先要分析出错的场景。
正常来说,TCC里面T阶段出错是没有关系的。比如说前面的那个例子里,数据处于初始化状态的时候,其实后续业务是用不了的,也不会有问题。但是如果在
Confirm
的时候出错了,问题就比较严重了。比如说一部分业务已经将数据更新为可用了,另外一部分业务更新数据为可用失败,那么就会出现不一致的情况。
基本上这里只能考虑不断地重试,确保在Confirm
阶段都能提交成功。毫无疑问,不管怎么重试,最终都是要失败的,所以要做好监控和告警的机制。
这里我们提到了重试最终都可能失败,所以紧接着你要进一步补充重试失败了以后怎么办。
有两个方案,第一个方案是异步比较数据并修复。
搞了一个离线比对数据并修复的方案,就是用来查找这种相关联的数据的,一部分数据还处于初始化状态,但是一部分数据已经处于可用状态,然后修复那部分初始化的数据。
另外一个方案是:在读取数据的时候,如果发现数据不一致,就丢弃这个数据,同时触发修复逻辑
在一些业务场景下,读请求是能够发现这种数据不一致的。那么它就会立刻丢弃这个数据,并且触发修复程序。
到这里 TCC 你已经讨论得比较深入了。接下来你可以考虑尝试把话题引到 SAGA
TCC 整体来说是追求最终一致性的,和它类似的是 SAGA 事务,也是一个追求最终一致性的事务解决方案,也不满足 ACID 的要求。
SAGA事务
核心思想:把业务分成一个个步骤,当某一个步骤失败的时候,就反向补偿前面的步骤
注意区分下:回滚和反向补偿
举个例子:某个步骤是插入数据,如果是回滚的话,那么是指插入的时候没有提交,然后业务失败的时候回滚;如果是反向补偿的话,那么是插入的时候已经提交了,在业务失败的时候执行删除
SAGA 的核心思想是反向补偿事务中已经成功的步骤。比如说某个业务,需要在数据库 1 和数据库 2 中都插入一条数据,那么在数据库 1 插入之后,数据库 2 插入失败,那么就要删除原本数据库 1 的数据。但是要注意,在最开始数据库 1 插入的时候,事务是已经被提交了的。
大部分人没有使用过 SAGA。我这里给出我曾经使用过一个实现比较复杂但是理论很简单的 SAGA 调度机制,你可以用来刷亮点,关键词是并发调度
早期我设计过一个比较复杂的 SAGA 机制,它支持并发调度。也就是说如果整个分布式事务中有可以并发执行的步骤,那么就并发执行,在后续出错的时候,这些并发执行的步骤也可以并发反向补偿。
SAGA 本身也是需要考虑容错的,难点就是在反向补偿的时候失败了怎么办?比如说在前面的例子里,你准备删除数据的时候失败了。那么还是没有特别好的办法,无非就是不断重试,这一部分你可以参考 TCC 中讨论的容错内容。
在讲完容错之后,紧接着你可以尝试把话题引导到 AT。
我个人认为最近比较流行的 AT 模式可以看作是 SAGA 的一种特殊形态,或者说简化形态。
AT事务
AT 是指如果你操作很多个数据库,那么分布式事务中间件会帮你生成这些数据库操作的反向操作。
这就有点类似于 undo log。比如说你数据库操作是一个 INSERT,那么对应的反向补偿操作就是 DELETE 了。你在回答的时候就可以结合 undo log 一起回答,顺便把话题引导到 undo log 上。
AT 模式的核心是分布式事务中间件会帮你生成数据库的反向操作,比如说 INSERT 对应的就是 DELETE,UPDATE 对应的就是 UPDATE,DELETE 对应的就是 INSERT。这个机制有点类似于 undo log。
同样地,AT 事务也有容错的问题,它的容错和 SAGA 一样,都是在反向补偿的时候出错了该怎么办。这里我就不赘述了,你可以参考前面的内容。
在回答了这些内容之后,你还可以进一步强调可以考虑禁用跨库事务。
如果是单纯使用分库分表,不涉及多个服务的分布式事务,可以考虑直接禁用跨库事务,一了百了。
禁用跨库事务
在实践中,解决分库分表中的分布式事务问题,最简单的方式就是直接禁用跨库事务。正常来说,在分库分表之后,你的业务就应该操作特定的某个数据库中的某个表。最多就是操作某个数据库上的某几张表,跨库本身就是一个不好的实践
所以可以从公司规范上直接禁用了跨库事务。
我们公司是直接禁止跨库事务。所以在分库分表之后我们要做的就是改造业务代码,确保不会出现跨库事务。
但是这样又有点太牵强了,那么你接下来就可以补充说明如果真的要使用跨库事务,你可以怎么解决,也就是把话题引导到延迟事务这个方案上。
亮点方案:延迟事务
分库分表中间件经常采用的方案。从理论上来说,这个方案其实并不比SAGA复杂,但是TCC、SAGA、AT属于烂大街的答案,拉不开差距,但是延迟事务可以。
在分库分表中间件眼里,当你执行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 的时候,某些数据库 COMMIT 成功了,但是另外一些数据库 COMMIT 失败了怎么办?当然,回滚也有类似的问题
其实这里没有完美的解决方案,只能考虑重试,类似于在讨论 TCC、SAGA 和 AT 的时候那样。
部分失败并没有更好的解决办法。我们这里就是在 Commit 的时候,如果发现某个数据库失败了,那么会立刻发起重试。如果连续重试失败,就会触发告警,人工介入处理。
再给出一个处理重试失败的高级方案。你可以用我们在高可用微服务架构里面提到的自动故障处理机制。
在重试失败的时候,最开始我们公司就是告警,然后人手工介入处理。后来我改进了这个机制,引入了自动故障处理机制。也就是说如果一个事务里面部分数据库提交或者回滚失败,触发告警,然后自动故障处理机制就会根据告警的上下文来修复数据。
修复数据本身分成两种,一种是用已经提交的数据库的数据来修复没有提交成功的数据库的数据;另外一种则是用没有提交成功的数据库的数据来还原已经提交的数据库的数据。具体采用哪种,根据业务来决定。
在我引入这个机制之后,很多业务都接入了自己的自动修复逻辑,整体上数据出错之后的持续时间和出错本身的比率都大幅度下降了,系统可用性提升到了三个九。
实际上,这种自动修复的逻辑是跟业务强相关的,所以你可以提供一些简单的通用处理机制,但是如果比较复杂的话,就需要业务方来控制如何修复了。
总结
重点:两阶段提交协议、三阶段提交协议和XA协议的基本步骤
跨服务的分布式事务:TCC、SAGA、AT
容错基本就是重试
重试失败后有三种方案:
- 监控+告警+人工介入处理
- 读数据+数据修复
- 监控+告警+故障自动处理
单纯的分库分表跨表事务,可以考虑延迟事务
标签:事务,分库,数据库,中间件,提交,搞懂,TCC,分布式 From: https://blog.csdn.net/LightOfNight/article/details/141106166