一、事务简介
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元。在关系型数据库中,一个事务由一组sql语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常被称为ACID特性。
- 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
- 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。
- 隔离性(isolation):一个事务的执行不能被其它事务所干扰。即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。
- 持久性(durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其有任何影响。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务,分布式事务,即使不能都很好的满足,也要考虑支持到什么程度。
二、本地事务
@Transaction
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务被称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。
在JDBC编程中,我们通过java.sql.Connection对象来开启、关闭或者提交事务。代码如下所示:
Connection conn = ......; //获取数据库连接
conn.setAutoCommit(false); //开启事务
try{
//执行增删改查sql
conn.commit(); //提交事务
}catch(Exception e){
conn.rollback(); //事务回滚
}finally{
conn.close(); //关闭连接
}
三、常见的分布式事务解决方案
- seata阿里分布式事务框架
- 消息队列
- saga
- XA
他们都有一个共同点,都是"两阶段(2PC)"。两阶段是指完成整个分布式事务,划分成两个步骤完成。
实际上这四种常见的分布式事务解决方案,分别对应着分布式事务的四种模式:AT、TCC、Saga、XA;
四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出;每种模式都有他的适用场景,同样每个模式也都诞生有各自的代表产品,而这些代表产品,可能就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC)。
在看具体实现之前,先说下分布式事务的理论基础。
分布式事务理论基础
解决分布式事务,也有相应的规范和协议。分布式事务相关的协议有2PC、3PC。
由于三阶段提交协议3PC非常难实现,目前市面主流的分布式事务解决方案都是2PC协议。
有些文章分析2PC时,几乎都会用TCC两阶段的例子,第一阶段try,第二阶段完成confirm或cancel。其实2PC并不是专为实现TCC设计的,2PC具有普适性,目前绝大多数分布式事务解决方案都是以两阶段提交协议2PC为基础的。
TCC(Try-Confirm-Cancel)实际上是服务化的两阶段提交协议。
两阶段提交协议
顾名思义,分为两个阶段:Prepare和Commit。
Prepare:提交事务请求
基本流程如下图:
- 询问:协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。
- 执行:各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录)并将Undo和Redo信息记录事务日志中。
- 响应:如果参与者成功执行了事务并写入Undo和Redo信息,则向协调者返回YES响应,否则返回NO响应。当然,参与者也可能宕机,从而不会返回响应。
Commit:执行事务提交
执行事务提交分别两种情况,正常提交和回滚。
正常提交事务
流程如下图 :
- commit请求:协调者向所有参与者发送Commit请求。
- 事务提交:参与者收到Commit请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
- 反馈结果:参与者执行事务提交后向协调者发送Ack响应。
- 完成事务:接收到所有参与者的Ack响应后,完成事务提交。
回滚事务
在执行Prepare步骤过程中,如果某些参与者执行事务失败、宕机或协调者之间的网络中断,那么协调者就无法收到参与者的YES响应,或者某个参与者返回了NO响应,此时,协调者就会进入回滚流程,对事务进行回滚。
流程如下图:
- rollback请求:协调者向所有参与者发送Rallback请求。
- 事务回滚:参与者收到Rallback请求后,使用Prepare阶段的Undo日志执行事务回滚,完成后释放事务执行期占用的所有资源。
- 反馈结果:参与者执行事务回滚后向协调者发送Ack响应。
- 回滚事务:接收到所有参与者的Ack响应后,完成事务回滚。
两阶段提交协议的问题
1.同步阻塞
参与者在等待协调者的指令时,其实是在等待其它参与者响应,在此过程中,参与者是无法进行其它操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。
2.单点
在2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用资源。如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待Prepare响应的时长等),所以也无法顺利处理上一个事务。
3.数据不一致
Commit事务过程中,Commit/Rallback请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到Commit/Rallback请求,而其他参与者则正常收到执行了Commit/Rallback操作,没有收到请求的参与者继续阻塞。这时参与者之间的数据就不再一致了。
4.环境可靠性依赖
协调者Prepare请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在2PC中,协调者会等待一定时间,然后超时后,会触发事务回滚。在这个过程中,协调者和所有参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。
下面我们分别来看4种模式的分布式事务实现。
AT模式(Auto Transaction)
AT模式是一种无侵入的分布式事务解决方案。
阿里seata框架,实现了该模式。
在AT模式下,用户只需关注自己的业务sql,用户的业务sql作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。
AT模式如何做到对业务的无侵入:
一阶段
在一阶段,Seata会拦截业务sql,首先解析sql语义,找到业务sql要更新的业务数据,在业务数据被更新之前,将其保存成before image,然后执行业务sql更新业务数据,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果是提交的话,因为业务sql在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删除,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,Seata就需要回滚一阶段已执行的业务sql,还原业务数据。回滚方式便是用before image 还原业务数据,但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据。如果不一致,就说明有脏写,出现脏写就需要转人工处理。
AT模式的一阶段,二阶段提交和回滚都是由Seata框架自动生成,用户只需编写业务sql,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案。
TCC模式
CC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作。事务发起方在一阶段执行Try方法,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法。
优点:在整个过程中基本没有锁,性能更强。
缺点: 侵入性比较强,并且需要用户自己实现相关事务控制逻辑。
四、Seata
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)。
Seata的三大角色
在Seata的架构中,一共有三大角色:
- TC(Transaction Coordinator)- 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM(Transaction Manager)- 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务
- RM(Resource Manager)- 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC为单独部署的Server服务端,TM和RM为嵌入到应用中的Client客户端。
- TM请求TC开启一个全局事务。TC会生成一个XID作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
- RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联。
- TM请求TC告诉XID对应的全局事务是进行提交还是回滚。
- TC驱动RM们将XID对应的自己的本地事务进行提交还是回滚。
设计思路
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图:
第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务 sql 进行解析,转换成undolog,并同时入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析。
第二阶段
分布式事务操作成功,则TC通知RM异步删除undolog
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过XID和Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新sql 并执行,以完成分支的回滚。
设计亮点
相比其他分布式事务框架,Seata架构的亮点主要有几个:
- 应用层基于sql解析实现了自动补偿,从而最大程度的降低业务侵入性。
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚。
- 通过全局锁实现了写隔离和读隔离。
存在的问题
性能损耗
一条update的sql,则需要全局事务xid获取(与TC通讯)、before image(解析sql,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要运行一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写sql都会增加这么多开销,粗略估计会增加5倍响应时间。
性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个成功率是有多高,或者说分布式事务失败需要回滚的有多少比例?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
全局锁
热点数据
相比XA,Seata虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。
回滚锁释放时间
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。