分布式事务
前记
随着业务的快速发展, 业务会越来越复杂, 架构也会跟着变复杂.传统的单体应用逐渐变得力不从心, 而微服务架构却能很好的解决问题. 但是微服务也会带来一些问题, 如本文说到的分布式事务, 分布式事务有几种解决的方法, 但都不是银弹, 分布式事务不可能100%的得到解决, 只能尽量提高成功率, 剩下的会失败的部分应该要做好监控或者任务补偿, 还需要人工介入.
1.单机事务到分布式事务的区别
单机的事务通过ACDI保证数据的强一致性,例如常见的MySQL
, 从事务开始后可以执行commit提交事务, 或者执行rollback回滚事务, 提交事务很简单, 而回滚事务是通过undo log
记录的反向日志进行回滚.单机事务可以准确的执行要么成功, 要么失败的操作, 没有其他的影响因素. 但是把单机事务拓展为分布式事务的时候, 就已经来到了分布式系统了, 这时候的事务管理会更加复杂, 会由于数据不一致,网络波动,程序bug,数据库挂掉等原因会导致一些奇怪的问题, 而这些因素都是不可控的.
一旦跟分布式有关, 就可以套用cap定理了, 根据cap定理可以知道, 任何系统只能满足其中两个条件, 无法三者兼得, 对于分布式系统而言,分区容错性是一个最基本的要求. 如果选择了一致性和分区容错性,放弃可用性,那么网络问题会导致系统不可用. 如果选择可用性和分区容错性,放弃一致性,不同的节点之间的数据不能及时同步数据而导致数据的不一致. 在分布式事务中, 虽然从单机变成多台机器, 但是总的事务逻辑都是不变的, 但是在可用性这方面是有区别的.
所以我们没有别的方案, 我们需要让分布式事务允许损失部分可用性,并且不同节点进行数据同步的过程存在延时,但是在经过一段时间的修复后,最终能够达到数据的最终一致性, 所以大多数是分布式事务都会考虑放弃一定的一致性, 通过一定的补偿方法让数据最终一致.
2.二阶段提交协议
假设现在有两个微服务B和C, B负责扣款, C负责减库存, 有个购买的服务A调用B和C, 在正常的微服务通信中, 很难做到同时判断B和C是否成功和失败, 而二阶段提交协议为了解决多个节点的协调问题, 引入了一个事务管理者来管理B和C, 而B和C被称为参与者.
二阶段提交协议就像名字一样, 共有两个阶段, 第一个阶段, 事务管理者D向参与者B和C发送准备命令, 并等待结果, 如果参与者可以执行, 就会执行操作, 但不提交, 然后把自己的操作结果返回给事务管理者D. 第二阶段, 如果全部参与者都返回提交成功, 则事务管理者会发送提交命令给参与者, 让参与者正式提交, 如果其中有一个提交失败, 则事务管理者会向所有的参与者发送回滚命令, 让参与者回滚数据.
可以看出二阶段提交协议非常的简单, 描述也就是一句话而已, 但是还是有缺陷的, 一个是所有操作全靠事务管理者来调度, 而事务管理者需要等待所有的参与者返回数据后才能进行下一步, 这样相当于有一个全局的大锁, 很容易造成同步阻塞的问题. 另一个是如果出现单点故障,整个流程都会阻塞, 如参与者出现故障无法响应, 事务管理者会一直等待响应, 事务管理者出现问题则失去控制者, 可能出现数据不一致.
3.三阶段提交协议
三阶段协议和二阶段协议很像, 不过增加了超时机制解决同步阻塞的问题, 同时引入了一个预备阶段, 该阶段会在准备执行事务时, 由事务管理者发送请求给所有的参与者, 如果有参与者回复超时或出错则停止事务, 如果都回复成功, 则像二阶段提交协议一样继续执行.
三阶段协议虽然解决了一个预阶段来防止大部分可能出现的全局阻塞问题, 但是还是不可避免的出现了全局锁和单点事务管理者的问题.
4.TCC模式
上面中引入了事务管理者, 容易出现单点瓶颈, 在业务的不断变大的情况下,系统的伸缩性可能存在问题, 同时,由于是同步操作, 引入的事务会被一个全局锁锁住,直至事务结束才释放, 所以性能压力会非常大, 而TCC模式都能解决上面的问题.
TCC模式将一个分布式任务拆分为Try, Confirm, Cancel. 由调用服务A发起流程, 而业务服务B和C提供TCC模式的三个阶段操作: 预留资源, 提交 or 回滚, 主要流程是:
- 第一阶段:调用服务A发起请求, 业务服务分别执行, B进行资金冻结, C进行库存冻结,
- 第二阶段:调用服务A检查两个服务的返回状态, 如果都返回成功, 则发起Confirm请求, 业务服务收到请求后, B进行资金扣减, C进行库存扣减
- 第三阶段:如果有一个服务失败, 则发起Cancel请求, 业务服务收到请求后, B移除资金冻结, C移除库存冻结
上面的每一步都是一个完整的子事务, 每次做完操作都会进行提交, 也就没有一个大锁锁住整个事务, 影响性能. 但是写起来也会比较麻烦, 在编写每个服务时都需要多写上创建预留资源和清除预留资源的代码, 同时在数据库应该创建预留资源的字段. 如库存数据库当前的库存为9, 用户下单一个产品后需要在预留资源的字段,也就是冻结字段的计数+1, 如果有其他是请求进来时, 会通过库存-冻结的库存发现只有9个库存能被使用. 确认事务提交成功后, 就可以进行Confirm, 把库存-1,然后把冻结的数字-1. 如果有事务失败, 则进入Cancel, 只需要把冻结的字段进行-1还原即可.
TCC模式在使用中应当注意几个问题:
- 幂等
由于TCC模式中, 每个阶段都是一个事务, 所以每个阶段都要确保幂等. 如上面的例子, 订单在每个阶段都只有一个状态, 如第一阶段是冻结, 第二个阶段是成功, 第三个阶段是失败, 在进行数据更新时, 一定要在where条件把状态限定, 这样就能达到幂等的效果了. - 空回滚
理论上,在执行Confirm和Cancel时,我们都要检查是否已经执行了Try, 如果没有执行Try就执行了Cancel, 则称为空回滚.TCC服务在实现时, 应当允许一些空回滚的执行, 不要抛异常. 但由于网络问题造成先执行Cancel再执行Try的情况则是不行的, 要抛异常.
5.事务补偿
可以从上面的演进中发现, 每个模式都有一种进步, 性能越来越越高, 但是错误还是无法避免的,即使他发生的概率很小很小, 但一影响到业务就可能炸了, 特别是跟金额有关的, 这时就需要一定的补偿机制, 可以是自动, 定时或者人工.
自动补偿–重试: 一般情况下, 对失败的数据都要进行幂等的最大努力重试, 来保证数据的最终一致性. 要确保每次重试的数据要不就只有成功的结果, 要不就只有失败的结果, 且多次同个最终状态的结果一定是一样的.同时需要确保有个良好的重试策略, 比如固定的重试次数内或时间失败了, 就不在进行重试, 而是发送报警交由人工干预, 同时每次重试的间隔最好是不一样的, 并且是逐步边长.
定时补偿: 比如金融系统中, 可能收到一笔还款成功的数据, 和还款失败的数据, 但是你没办法知道哪一笔才是有效的, 但当天内也数据提供方也无法给你提供准确的数据, 这时就需要定时任务在第二天通过提供方的数据和自己的数据进行对账, 并自动修复账单数据.
人工: 人工是事务补偿的兜底, 如果上面两种补偿失败或是没办法覆盖的情况, 都需要人工去手动处理. 所以在分布式事务中, 要做好良好的监控和日志打印, 同时也需要为每个分布式任务创建唯一Id, 并对整个分布式链路进行监控.
6.总结
分布式事务是基于分布式而诞生的东西, 自然而然的, 他拥有很多分布式自带的缺陷, 所以分布式事务不可能100%的得到解决, 我们只能尽量的去增加他的成功率, 减少对性能的消耗, 并维护一定的操作手册, 分布式事务日志和监控, 在紧急时刻可以快速的以人工的方式进行分布式事务的事务补偿.
所以分布式很高大上, 但没啥事就上分布式, 什么体量的系统就用什么样的方案, 如果上了分布式, 则要确保链路监控和日志能十分的完善.
- 本文作者:So1n
- 本文链接:http://so1n.me/2020/08/17/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/index.html
- 版权声明:本博客所有文章均采用 BY-NC-SA 许可协议,转载请注明出处!