前言
本文是前面一篇文章聊了基于sharding的分库分表后拓展出来的关于分布式事务的讨论,本文中将详细讲一下sharding中的分布式事务以及市面上常见的分布式事务解决方案。
1.sharding的分布式事务
Sharding JDBC: Sharding JDBC 提供了对分布式事务的支持。它通过两阶段提交(2PC)协议来实现跨多个数据源的分布式事务。Sharding JDBC 会将事务中的操作发送到各个数据源执行,并在提交阶段协调所有数据源的提交操作,以保证事务的一致性。
Sharding Proxy: Sharding Proxy 本身并不直接支持分布式事务。它主要是一个中间件,负责路由和转发客户端的 SQL 请求到后端的多个数据源。在 Sharding Proxy 中,跨多个数据源的事务操作会被拆分成多个独立的本地事务,每个数据源独立处理。这就意味着在 Sharding Proxy 中,跨多个数据源的事务并不会保证全局的一致性,需要应用程序自行处理分布式事务的逻辑。
sharing jdbc后在spring boot中如何使用分布式事务,代码示例:
@Service public class MyService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void performDistributedTransaction() { // 在事务范围内执行数据库操作 jdbcTemplate.update("INSERT INTO table1 (column1) VALUES (?)", "value1"); jdbcTemplate.update("INSERT INTO table2 (column1) VALUES (?)", "value2"); // 手动抛出异常,模拟事务中的异常情况 throw new RuntimeException("Simulated exception"); } }
是的你没看错,不用任何额外的配置,就是用spring的事务即可。因为sharding jdbc作为一个分库分表的技术栈,天生就有分布式事务问题,所以其默认就是开启分布式事务的,它的事务都是两阶段提交的。
说到分布式事务,我们来回顾一下之前聊过的分布式事务的内容。
2.分布式事务的产生原因
分布式事务是指在在分布式架构下一个事务中的不同子操作是在不同机器上执行的,子操作之间状态无法相互感知,其中有子操作出错后其他机器上的子操作无法正确回滚。
分布式事务产生的根本原因:各个服务之间无感知
举一个例子:
一个下单事务,包含创建订单,库存服务,需要顺序调用订单服务、库存服务。如果库存服务挂掉后,订单服务也是无法回滚的,因为订单服务感知不到库存服务是否成功,即无法感知到库存服务的状态。
3.分布式事务的解决方案
3.1.DTP模型
1994年,X/Open组织(即现在的Open Group)定义了分布式事务处理的DTP模型,该模型包括几个角色:
- 应用程序(AP),服务
- 事务管理器(TM),全局事务管理者
- 资源管理器(RM),一般是数据库
- 通信资源管理器(CRM),TM和RM之间的通信中间件
DTP模型中,一个分布式事务(全局事务)可以被拆分成多个本地事务,运行在不同的AP和和RM上。每个本地事务的ACID由自身保证,全局事务必须保证每个本地事务必须同时成功,若有一个失败其他事务必须回滚。由于本地事务之间相互是无法感知状态的,因此要通过CRM来通知各个本地事务,同步事务执行的状态。
由于各个本地事务之间需要通信,通信就需要标准,因此配套提出了XA通信标准。XA规定了DTP中CRM和TM之间的通信的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商之间必须实现该接口。
3.2.分阶段提交
分阶段提交,也叫两阶段提交(two-phase commit)。DTP模型落地实现的一种思想。
两阶段提交中,将事务分成两个阶段来执行:
- 阶段一,准备阶段,各个本地事务执行自己事务内的逻辑,完成提交前的准备工作。
- 阶段二,执行阶段,各个事务根据上一阶段的执行结果,进行提交或者回滚。
两阶段提交中有两个角色,协调者(coordinator)、参与者(voter)。
正常情况:
异常情况:
准备阶段协调者会询问每个参与者是否可以执行事务,每个事务参与者执行事务写入redo、undo日志,然后反馈事务的执行结果,只要有一个参与者返回的是disagree则说明失败,协调者会像各个参与者发出abort指令,各个事务收到指令后各自回滚事务。
两阶段提交的优点:
- 方案成熟,很稳
- 能保证强一致性
两阶段提交的缺点:
- 单点故障,当协调者挂了之后后续的步骤就没办法进行了,被锁住的数据没办法解锁,会造成阻塞。
- 数据锁定,分阶段提交由于过程被拆成两段,会造成本地事务的执行过程变长,数据会长时间处于锁定状态。
3.3.TCC模式
TCC模式是专门用来解决分阶段提交的缺陷的,减少数据锁定,避免阻塞问题。
TCC相当于是强化了两阶段提交,在分阶段提交中埋入了以下几个点位:
- try,资源的检测和预留。
- confirm,执行的业务操作提交,要求try成功的话confrim一定要能成功。
- cancel,预留资源释放
经过TCC的强化埋点后两阶段提交变成了:
- 准备阶段。try,资源预留,通知每个参与者去try一下数据资源,判断一下数据资源是否足以支撑之后的事务运行。每个参与者均try成功再执行下一步,否则直接cancel。
- 执行阶段,confrim,让协调者通知各个参与者执行并提交事务(因为资源已经准备好了,执行和提交可以一气呵成),各个参与者confrim成功则成功,但凡有一个失败则通知协调者组织全局回滚、释放资源、退出。
TCC的优点:
TCC其实每个阶段都是单独的事务,try其实是个事务只是去摸一下看下阶段要的数据资源是否能用,confrim则是原来的包含业务逻辑的事务。这样的话就避免了两阶段提交中agree过程中对于数据的锁定,尽量避免了影响其他事务的执行。
TCC的缺点:
- 编码成本,存在代码侵入需要开发人员手动编写TCC三步,开发的成本会比较高。
- 弱一致性,TCC保证的是最终一致性,因为单个本地事务的执行和提交都包含在confirm一个步骤中,如果出现问题回滚前其实是没有保证一致性的,在这中间有其他读操作进来是能读到已经变动的数据的。
3.4.可靠消息服务
可靠消息服务起源自eBay,是将各个服务的本地事务串成一个链,依托MQ来保障分布式事务,比如服务A执行完服务后向MQ中存放自己执行成功的信息,MQ再向服务B推送消息叫它执行本地事务,服务B执行完本地事务后又告诉MQ,MQ继续向后续要执行本地事务的服务推送消息。
缺点:
由于是MQ主动向后续服务推送消息,后续服务要是失败了,前置服务感知不到,前置服务无法进行感知回滚。。所以可靠消息服务适用的场景有限。
3.5.AT模式
AT模式是Alibaba的seata组件开源出来的一种分布式事务解决方案,是对TCC的一种优化,解决了TCC模式中的的代码侵入、编码复杂等问题。
AT模式中,用户只需关注自己的业务SQL,seata框架会自动生成书屋的二阶段提交和回滚操作。
AT模式整个流程和TCC大致相同,不同点是在于AT模式将执行放在了一阶段:
- 一阶段(开发者实现),正常编写业务逻辑,然后执行SQL,执行本地事务,并返回执行结果。
- 二阶段(框架托管),根据一阶段的结果判断二阶段的做法,提交或者回滚。
AT模式的底层原理:
在阶段一的时候拦截下所有SQL,框架会去解析这条SQL然后去数据库中查询出要操作的数据,存一份镜像叫before image,接下来执行SQL,执行完SQL后再查询一次,存一份叫after image。
到了阶段二的时候,比对before镜像和after镜像从而判断操作是否成功,如果一阶段的所有本地事务操作都是成功的,就会清空before镜像和after镜像,如果有一个是失败的各个本地事务就会根据属于自己的before镜像进行回滚。
after镜像是为了在回滚的时候进行比对,要是回滚时发现数据库中此时的数据和after镜像中记录的数据不相同,则说明数据又被动过了,产生了脏数据,此时就需要人工介入了。由于行锁的存在,产生脏数据的概率很低很低,after镜像只是留了一手。
AT模式中的角色:
AT模式中一共有三个角色
- TC,服务协调者,是一个单独的服务。
- TM,通信中间件。
- RM,资源管理器,管理分支的事务处理,与TC进行通信注册分支事务和报告事务状态,驱动分支事务提交或者回滚。
TM、RM和服务是耦合在一起的,但是不侵入代码,以jar包的方式体现。
3.6.Seata
Seata是目前为止常用、流行切稳定的分布式事务解决方案,其在使用上对代码没有侵入,直接是基于配置的,使用方法见官方手册即可。在遇到分布式事务场景时,可以优先考虑使用seata来解决。