欢迎来到啾啾的博客🐱。
这是一个致力于构建完善的Java程序员知识体系的博客📚,记录学习的点滴,分享工作的思考、实用的技巧,偶尔也分享一些杂谈💬。
欢迎评论交流,感谢您的阅读😄。
本篇为事务处理全盘第二部分,上篇为[Java微服务架构]7-1_事务处理——事务特性与本地事务
3.全局事务(Global Transaction)
全局事务主要讨论单个服务使用多个数据源的多数据源事务问题。
2PC
在多个数据源的场景中,可以将多个事务的提交划分为两个阶段,即2PC。
1.准备阶段
又叫作投票阶段,在这一阶段,协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复Prepared,否则回复Non-Prepared。
对于数据库来说,准备操作是在Undo Log中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条Commit Record而已。
即上面一MySQL为例进行到第四步前。
2.提交阶段
又叫作执行阶段,协调者如果在上一阶段收到所有事务参与者回复的Prepared消息,则先自己在本地持久化事务状态为Commit,然后向所有参与者发送Commit指令,让所有参与者立即执行提交操作。
任意一个参与者回复了Non-Prepared消息,或任意一个参与者超时未回复时,协调者将在自己完成事务状态为Abort持久化后,向所有参与者发送Abort指令,让参与者立即执行回滚操作。
对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条Commit Record而已,通常能够快速完成,只有收到Abort指令时,才需要根据回滚日志清理已提交的数据,这可能是相对重负载操作。
2PC能够成功保证一致性还需要一些其他前提条件:
必须假设网络在提交阶段的短时间内是可靠的,即提交阶段不会丢失消息。同时也假设网络通信在全过程都不会出现误差,即可以丢失消息,但不会传递错误的消息。
3PC
不难看出2PC存在协调者等待参与者会有超时问题(也称单点问题),且所有参与者相当于被绑定为一个统一调度的整体,期间要经过两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久化,提交阶段在日志写入提交记录),整个过程将持续到参与者集群中最慢的那一个处理操作结束为止,这决定了两段式提交的性能通常都较差。
且2PC需要可靠的网络,当网络不稳定时2PC失效。
FLP不可能原理:如果宕机最后不能恢复,那就不存在任何一种分布式协议可以正确地达成一致性结果。
三段式提交把原本的两段式提交的准备阶段再细分为两个阶段,分别称为CanCommit、PreCommit,把提交阶段改称为DoCommit阶段。其中,新增的CanCommit是一个询问阶段,即协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。
将准备阶段一分为二的理由是这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,它们所涉及的数据资源即被锁住,如果此时某一个参与者宣告无法完成提交,相当于大家都做了一轮无用功。
所以,增加一轮询问阶段,如果都得到了正面的响应,那事务能够成功提交的把握就比较大了,这也意味着因某个参与者提交时发生崩溃而导致大家全部回滚的风险相对变小。因此,在事务需要回滚的场景中,三段式提交的性能通常要比两段式提交好很多,但在事务能够正常提交的场景中,两者的性能都很差,甚至三段式因为多了一次询问,还要稍微更差一些。
同样也是由于事务失败回滚概率变小,在三段式提交中,如果在PreCommit阶段之后发生了协调者宕机,即参与者没有等到DoCommit的消息的话,默认的操作策略将是提交事务而不是回滚事务或者持续等待,这就相当于避免了协调者单点问题的风险。
从以上过程可以看出,三段式提交对单点问题(协调者等待参与者的超时问题)和回滚时的性能问题有所改善,但是对一致性风险问题并未有任何改进,甚至是略有增加的。譬如,进入PreCommit阶段之后,协调者发出的指令不是Ack而是Abort,而此时因网络问题,有部分参与者直至超时都未能收到协调者的Abort指令的话,这些参与者将会错误地提交事务,这就产生了不同参与者之间数据不一致的问题。
4.共享事务(Share Transaction)
与全局事务相反,共享事务是指多个服务共用同一个数据源。
很显然,多个服务使用一个数据源会有竞争问题,但是因为数据源的事务特性,其实和本地事务也一样。
共享事务的问题主要是“事务边界模糊”与“隔离级别不足问题”,复杂度高于本地事务
- 事务边界模糊
即“事务的开始和结束由各个服务独立控制,难以保证事务边界的清晰性。如果某个服务未正确提交或回滚,可能影响其他服务的数据一致性”。 - 隔离级别不足
业务逻辑的复杂性可能超出隔离级别的控制范围,即需要服务应用协调控制来保证数据一致性。
这个“隔离级别不足”问题记一下,分布式事务也存在这个问题。为什么不足呢?因为资源也是分布式的,有多数据源需要进行数据隔离,ACID是本地性质的,做不了多数据源的。
本篇主要还是讨论事务,不拓展单点访问性能瓶颈、资源消耗问题。
共享数据源连接
一种理论可行的方案是让各个服务共享数据库连接,这样可以共享事务,将共享事务转化成全局事务。
“一些中间件服务器就是使用共享数据源连接的方式,如WebSphere。”
“共享的前提是所有使用数据源的服务都在同一个进程内”
看到这里可能就有疑惑了,如果所有服务都在同一个进程,那不就是个单服务是一样的了么?
没错,所以这种方案需要一个“中间服务层”,使用数据源的多个服务通过“中间服务层”来连接数据源。
如下图所示:
中间服务可以是一个服务用于处理数据库连接,现实中有类似ProxySQL、MaxScale这样用于多个数据库实例做负载均衡的数据库代理,其代理单个数据源时比较接近这种模式。
还有一种变种方式是使用消息队列当做中间服务。
不过因为通常情况下,一个服务集群里数据库才是压力最大而又最不容易伸缩拓展的“重灾区”。
实际很少有这样的方案,毕竟多服务至少是拆分了微服务的情况,都拆分微服务了目的肯定是低耦合易拓展,这种模式……