分布式系统的事务
事务,一组操作构成的可靠的独立执行的单元,事务具备原子性、一致性、隔离性和持久性。
分布式事务系统, Distributed Transaction System, 指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式的不同节点之上。
分布式事务,由不同的服务之间通过网络远程协作完成事务,是一次大活动有多个动作,由不同的小活动组成,这些活动要么全部成功,要么全部失败。
事务理论和概念
1. 数据库事务(本地事务)|分布式事务
数据库事务在实现时会将一次事务的所有操作全部纳入到一个不可分割的执行单元,该执行单元的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。支持严格的ACID,高效且可靠,隔离的最小单位受限于资源管理器。
分布式事务是由网络远程协作完成的一系列动作,各个服务是网络分区的;场景覆盖:跨多个进程远程调用,跨数据库实例操作。
数据库事务
begin transaction:
本地数据库操作:张三-100元
本地数据库操作:李四+100元
end transaction
分布式事务
begin transaction:
本地数据库操作:张三-100元
远程调用操作:李四+100元
end transaction
由于远程调用的李四+100成功,但是网络问题导致远程调用失败,则出现本地事务回滚,导致系统的数据不一致。
2. 分布式事务CAP理论
- 一致性 Consistency,多个节点数据是否强一致;
- 数据需要在各个节点同步,所以写操作是有一定的时延。对于一致性会要求对资源暂时锁定,待同步完成后释放锁定资源;如果请求数据同步失败的节点,应当返回失败而不是旧数据。
-
可用性 Availability, 分布式系统活跃的节点都能够处理所有操作且能响应查询,不会出现超时或失败;
- 确保从节点的数据可用性,不应将资源锁定,需要返回数据,即使是返回旧数据,不能返回错误或者响应超时;
-
分区容忍性 Partition tolerance, 出现网络故障,节点通信失败,但是系统仍能正常工作;
- 数据同步失败不影响当前系统的读写操作,系统部分节点故障不影响整个系统对外的响应,分区容忍性是分布式系统具备的基本能力。
只能同时满足上述两个要素,不可三者兼得;
当前大部分分布式系统选择的是AP(柔性事务),而对于一致性选择的是最终一致性,而不是强一致性,能够接受查询数据在一定时间内不是最新的;延伸出后续的BASE理论。
CP是放弃了可用性(刚性事务),zookeeper即使用该策略追求强一致性,同时跨行转账需要双方银行均响应后才完成整个事务。
CA是放弃了分区容忍性,不考虑网络不通或者节点挂掉的场景,系统将不是一个标准的分布式系统;
3. 分布式事务BASE理论
Basically Available,Soft state,Eventually consistent,牺牲强一致性获得可用性,出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间是不一致的,这段时间不影响系统可用性,但最终达到一致性状态。通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证了系统的吞吐量。
基于Base的柔性事务往往分为两类:通知型事务(依赖于消息中间件),补偿型事务(TCC)。
4. 本地事务的ACID特性
-
Atomic 原子性,整个事务过程中的所有操作,要么全部完成,要么全部不做,无中间状态,无部分成功。
- 对于单个事务,原子性就能保证一致性;但是对于多个并发执行的事务,由于执行顺序,最终效果可能不同;
-
Consistency 一致性,保证多个节点中数据的一致性,所有的节点在任何时间数据都是一致的;
- 业务操作支持重试,不会产生不利影响,需要满足幂等性;
- 强一致性,当更新操作完成后,任何多个线程访问都会返回最新的更新过后的数据;
- 弱一致性,系统并不保证线程访问返回最新的更新过后的值;不承诺立即,也不承诺具体多长时间;
- 最终一致性,弱一致性的特定形式,系统保证没有后续更新值前提下,系统最终返回上一次更新操作的值;期间不一致的主要因素主要有:通信延迟、系统负载、副本个数等;
-
Isolation 隔离性,多个事务之间往往是并发的,多个事务之间互相不干扰,一个事务不能看到其他事务的运行过程的中间状态;
-
每个事务都是有起止时间,每个事务的执行过程就是实现维度上的一个时间片段,多个时间片段上的并发交错,出现了不同的隔离性问题。数据库事务的隔离isolation级别:Read uncommitted,Read committed,Repeatable read,Serializable

-
-
Durability 持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。
分布式事务解决方案
1. 刚性事务-数据库事务
数据库一般由两个文件组成,数据文件及日志文件,往往日志文件比数据文件更大。对于任何数据库的写操作都是先写操作日志。 对于事务操作,数据库会先记录这个事务的redo日志,把日志文件写入磁盘,之后再开始操作数据库数据。即便在断电后操作尚未完成,重新启动时候会读取日志文件进行redo前滚或者是undo回滚,以保证数据的一致性;
2. 缓存最终一致性
现象:缓存用在数据库前面,作为数据读取缓存,使得I/O操作不直接落在数据库上。存在缓存数据与数据库数据不一致现象。
解决方案:
-
设置缓存过期时间,该时间即系统可以达到最终一致性的容忍时间;
-
数据在数据库更新成功后,同时刷新缓存中的数据;
3. 两阶段提交-强一致性 (2PC, 2 Phase Commit)
具有强一致性,是CP系统的典型实现;通常两个角色,协调器(事务管理器)及事务执行者,协调器负责整个分布式系统的提交和回滚,事务执行者负责本地事务的提交和回滚。
高并发场景涉及到性能问题,C系统需要等待多个资源,耗时较长。同步阻塞,执行者在事务未提交之前会一直锁定其占有的本地资源对象。单点故障,如果协调者出现故障后,所有的执行着会进入阻塞状态,导致本地被锁的资源无法被其他进程使用。数据不一致,当doCommit消息发送后由于网路抖动导致部分事务执行者没有搜到消息而不提交事务,导致最终不一致。
协调器的状态包括:init,preparing,committed,aborted
事务执行者的状态包括:working,prepared,committed
两个步骤:prepare极端、commit提交或rollback回滚阶段

统一标准组织定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)
- AP (Application Process) 应用程序,使用DTP分布式系统的程序
- RM (Resource Manager) 资源管理器,事务的参与者,资源管理器控制着分支事务,负责本地事务的提交;
- TM (Transaction Manager) 事务管理器,负责协调和管理事务,控制着全局事务,管理事务的生命周期,同时协调各个资源管理器。
- CRM (Communication Resource Manager) 通信资源管理器,控制一个TM域内或者跨TM域的分布式应用之间的通信。
- CP (Communication Protocol),通信协议,提供分布式应用之间的底层通信服务。
一个分布式事务即包含若干分支事务的全局事务,全局事务协调分支事务要么全部提交,要么全部回滚。
DTP模型定义了TM和RM直接通讯接口的规范(叫XA),TM向AP提供应用程序编程接口,AP通过TM进行事务提交和回滚事务;TM通过XA接口来通知RM事务的开始、结束以及提交和回滚。
对于2PC使用DTP来描述为:
-
准备阶段,RM执行实际的业务操作,但不提交事务,并且同时锁定资源。
-
提交阶段,TM接受RM在准备阶段的回复,任何一个RM失败,TM通知所有的RM执行回滚操作;否则,TM通知RM进行分支事务的提交。提交阶段结束后释放锁定资源。
4. 补偿模式-最终一致性(TCC, Try-Confirm-Cancel)
服务化的二阶段提交协议,要求分支事务实现三个操作:预处理Try,确认Confirm,撤销Cancel。预处理做业务检查和资源预留,确认做业务确认操作,撤销实现一个和确认完全相反的操作,回滚。TCC任务只要Confirm阶段或者Cancel阶段不会出错,只要Try成功,Confirm一定成功。若真出现失败,需要支持重试机制和人工处理介入。在事务链中的任何一个正向事务Confirm操作, 都必须存在一个完全符合回滚Cancel规则的可逆事务。
如果涉及重试,则需要实现为幂等,对于同一个操作无论执行多少次结果均相同。
TCC需要区分异常的处理机制,支持不同场景的异常处理:空回滚、幂等、悬挂。
空回滚:往往服务宕机和网络故障的时候,对于Try阶段就故障的时候,在故障恢复后,需要识别出是空回滚。依赖与全局事务的ID在分布式事务调用链的持久化。
幂等:确认/撤销操作的幂等性,确保不会重复使用或者重复释放资源。
悬挂:由于网络时延导致,第一阶段TM认为Try网络拥堵超时了,直接执行了第二阶段Cancel,但是第二阶段执行结束后,第一阶段Try才达到真正的执行者。
解决方案:
- 服务调用链关系需要记载;
- 每个服务提供则需要额外提供一组业务逻辑相反操作,互为补偿;
- 按失败的不同原因执行不同的回滚策略;
- 针对各个资源分别进行锁定,分别提交与释放,各个服务之间异步并行执行;

实例(账户扣钱转账30,账户总额100):
Try准备阶段,做资源的检查和预留。当前总额是100,但是30已经被冻结,不能用于其他的事务;
Confirm确认阶段,执行真正的扣钱。冻结的30扣除,账号余额为70;
Cancel撤销阶段,释放冻结的30,使账号回到最初状态,余额100;
2PC通常是在跨库的DB层面,TCC则在应用层面,需要通过业务逻辑来实现,对业务有侵入性。可以自行定义数据操作的粒度以降低锁冲突,提高吞吐量,但是侵入性强,每个分支事务均需要实现3个操作。按照网络状态、系统故障等不同的失败场景实现不同的回滚策略。
5. 三阶段提交 (3PC)
-
CanCommit(事务询问)
询问所有参与者是否可以执行事务的操作;
-
PreCommit(事务执行)
在事务询问都是okay的前提条件下,执行事务的操作;
-
DoCommit(事务提交)
真正的事务提交;
针对二阶段的缺陷,解决单点故障问题,减少阻塞;一旦参与者无法及时收到来自协调者的信息之后,会默认执行commit,而不会一直处于阻塞状态。这样会造成最终数据不一致。
上述解决方案依据具体使用场景而灵活变化,需要适应业务需求。
6. 消息事务-最终一致性
基于消息中间件的二阶段提交,将本地事务以及发放消息放在一个分布式事务中。
-
系统向消息系统中间发送预备消息;
-
消息中间件保存预备消息并返回成功;
-
执行本地事务;
-
发送提交消息给消息中间件;
1和2失败时候事务未执行;
3失败时候,回滚预备消息;系统实现消息中间件的回调接口,失败则回滚。
4失败时候,3步骤为成功,消息中间件能够检查到系统已经成功执行,消息中间件自行提交完成整个事务。
对消息中间件要求较高,保障消息不丢失,消息不重复不乱序。
针对投递Commit消息,接收订阅方存在三个问题:消息可能重复消费,消费者消费超时,消费失败;
针对第一个问题需要在消息消费方进行唯一标识识别,消费方对消息的状态进行保存,识别重复消息;
针对第二个问题即重试即可,直至消息消费成功;
针对第三个问题,消费失败如果由于系统bug导致,重试多次一样失败,如果此时需要回滚则有复杂的业务逻辑,最好的办法就是通过报警系统及时发现失败情况然后再人工处理。
往往第三个问题都是致命异常,要打印详细的错误日志,并设计一个报警系统在后台实时扫描和分析此类日志,检查出特殊情况并短信通知人工处理。

关键解决两个问题:
-
本地事务与消息的发送的原子性问题。
-
事务参与方接受消息的可靠性。
可靠消息最终一致性适合执行周期长实时性要求低的系统,解耦了服务之间的依赖,同时基于消息的异步操作,避免了分布式事务中的同步阻塞问题。
7. 重要结论
- 如果业务场景需要强一致性, 那么尽量避免将它们放在不同服务中, 也就是尽量使用本地事务, 避免使用强一致性的分布式事务.
- 如果业务场景能够接受最终一致性, 那么最好是使用基于消息的最终一致性的方案(异步确保型)来解决.
- 如果业务场景需要强一致性, 并且只能够进行分布式服务部署, 那么最好是使用TCC(try-confirm-cancel)方案而不是2PC方案来解决.
刚性事务:严格遵循ACID原则的事务,如单机数据库事务;
柔性事务:遵循BASE理论的事务,实现方式:两阶段提交、TCC补偿提交、消息异步确保、最大努力通知型
- 可见性,各个服务需要提供查询接口,来判断操作的处理情况。
- 幂等性,可重复操作,调用多次返回的业务结果相同。
本地事务采用刚性事务,分布式事务采用柔性事务。
MySQL的事务
隐式事务和显示事务,mysql默认就是隐式事务模式,当insert,update,delete时,数据库自动开启事务、提交或回滚事务。由autocommit控制,可以由下述命令进行查询;
show variables like 'autocommit';
1. 事务的实现
Mysql的Innodb存储引擎基于日志和锁来实现ACID,默认的事务隔离级别是Repeatable Read。
-
通过数据库锁机制和MVCC,确保事务的隔离性;
-
通过Redo Log重做日志,来保障事务的持久性;
- Redo Log是记录新数据的备份,事务提交前,将Redo Log持久化而不是数据持久化,当系统崩溃时,虽然数据没有持久化,但是Redo Log持久化,系统恢复后通过Redo Log恢复到崩溃之前的状态。
-
通过Undo Log撤销日志,来保障事务的原子性和一致性;
- 操作任何数据之前,先将数据备份到Undo Log,然后进行数据的修改。对于数据的每行数据,可能会存在多个版本的数据。如果出现Rollback,利用Undo Log中的备份数据恢复到事务开始的状态;
2. 事务的隔离
事务隔离级别主要是为了解决多个事务之间数据可见性及数据正确性的问题。
-
读未提交,Read Uncommitted
可以读取到其他事务还未提交的数据,多次读取的结果不一致。出现脏读,不可重复读,幻读;
-
读已提交,Read Committed
无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一致,不可重复读和幻读;
-
可重复读,Repeatable Read
无法读取到其他事务已经提交的数据,多次读取的结果一致,可重复读;会出现幻读;当两个事务,数据库唯一索引,基于事务中查询后插入的场景。
-
串行,Serializable
让并发的事务串行化,多个事务之间读写,写读,写写会产生互斥,多个事务间读读不会产生互斥。不会出现脏读,不可重复读、读已提交和幻读。
3. 并发控制
mysql并发控制主要通过加锁来实现。
-
读锁 read lock,也叫共享锁share lock,多个读请求可以同时共享一把锁来读取数据而不会阻塞;
-
写锁 write lock,也叫排他锁exclusive lock,写锁会排斥其他的所有获取锁的请求,一直阻塞直到数据写入;
锁的粒度分为表锁和行锁
- 表锁 table lock,锁定整张表,维护锁的开销最小但是降低了表的读写效率。一般alter table会进行表锁;
- 行锁 row lock,最大程度的支持并发,数据库锁的维护开销大。行锁一般由各个引擎实现,而非mysql服务器层面实现;
MVCC版本管理,多版本并发控制,借此做到的读写分离,实现不加锁实现读写并行。
- mysql innodb引擎通过在每行记录后保存两个列(创建时间和过期时间,逻辑时间的版本号),每开启一个新的事务,系统版本号会增长,事务开始的版本号会作为事务号,用来和查询的每行记录的版本号进行比较。
Spring事务管理
编程式事务管理,通过TransactionTemplate或者TransactionManager手动管理事务;相关代码逻辑
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
声明式事务管理,通过AOP实现,基于@Transaction注解实现。如果@Transaction使用在类上,表明该注解对该类中的所有public方法均生效。如果同一个内中的方法调用了@Transanction标记的方法,则事务会失效,因为其依赖的AOP实现机制底层是动态代理,对于外部类调用时候才生效。
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
spring事务框架在于接口的定义
-
PlatformTransactionManager:平台事务管理器,事务策略的核心,事务的管理接口;
package org.springframework.transaction; import org.springframework.lang.Nullable; public interface PlatformTransactionManager { //获得事务 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; //提交事务 void commit(TransactionStatus var1) throws TransactionException; //回滚事务 void rollback(TransactionStatus var1) throws TransactionException; } -
TransactionDefinition:事务定义信息,包括事务的传播、隔离级别、超时、只读、回滚规则等;
-
TransactionStatus:事务运行状态,定义了一组方法用于获取事务相应状态信息;
事务传播行为,为了解决业务层方法之间互相调用的事务问题;当事务方法被另一个事务方法调用的时候,必须指定事务应该如何传播。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = X)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Y)
public void bMethod {
//do something
}
}
TransactionDefinition.PROPAGATION_REQUIRED,如果当前存在事务,则加入该事务;如果没有事务,则创建一个新的事务。- 对于上述代码X和Y均为PROPAGATION_REQUIRED,只要aMethod或者bMethod任何方法回滚,整个事务就回滚;
TransactionDefinition.PROPAGATION_REQUIRES_NEW,创建一个新的事务,如果当前事务存在则挂起。不管外部是否有事务,当前方法均会开始新的事物,且开启的事务相互独立,互不干扰。- 对于上述代码,X为PROPAGATION_REQUIRED,Y为PROPAGATION_REQUIRED_NEW,aMethod的回滚不会导致bMethod的回滚,如果bMethod的方法回滚,且抛出了未被捕获的异常满足aMethod回滚的规则,则aMethod也会回滚。
TransactionDefinition.PROPAGATION_NESTED,外部开启事务的条件是,内部开启开启新的事务,作为嵌套事务存在。如果外部不存在,则单独开启一个事务,效果同PROPAGATION_REQUIRED。- 对于上述代码,X为PROPAGATION_REQUIRED,Y为PROPAGATION_NESTED,如果bMethod回滚后,aMethod也会跟着回滚。
TransactionDefinition.PROPAGATION_MANDATORY,如果当前存在事务,则加入该事务;如果没有事务,则直接抛出异常;使用场景较少;TransactionDefinition.PROPAGATION_SUPPORTS,如果当前存在事务,则加入事务;如果没有事务,则按照非事务的方式继续运行;使用场景较少;TransactionDefinition.PROPAGATION_NOT_SUPPORTED,以非事务的方式执行,如果当前存在事务,则把当前事务挂起;使用场景较少;TransactionDefinition.PROPAGATION_NEVER,以非实物的方式执行,如果当前存在事务,则直接抛出异常;使用场景较少;
事务超时行为,一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。默认值为-1,表示没有超时时间或者取决于底层事务系统;
事务只读属性,对于只有数据查询的事务,可以指定事务类型为read only,只读事务不涉及数据的修改,数据库会提供一些优化手段,适用于有多条数据查询的操作的方法中。
- Mysql默认对每个新建的连接使用autocommit模式,该模式下,每个发送到Mysql的服务器的sql都会在一个独立的事务中进行处理,事务处理后自动提交,然后开启一个新的事务。给方法加上@Transaction注解,该方法执行的sql会被放在一个事务中,如果是只读事务,mysql会去优化它的执行。
事务回滚机制,规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务遇到运行时异常和Error时才会回滚,对于受检异常时不会回滚。