罗招材,2016年7月加入去哪儿网技术团队。目前在火车票事业部/技术部,参与了火车票系统服务拆分、数据库中间件以及日志系统的构建,对系统性能优化和高并发的系统感兴趣。个人爱好户外和音乐。
引言
印象中事务是个不太好理解的概念,因为涉及的概念、原理和模型较多,本文梳理了事务涉及到的相关知识,包括事务的概念、特性、本地事务实现原理、Spring事务 和分布式事务模型以及相关的开源实现分析。
单机事务
分布式事务是建立在单机事务的基础上扩展而来的,因此需要首先了解单机事务的相关概念和基本的实现原理。
事务特性概念
说到事务,就不得不提事务提供的 ACID 特性,维基百科对事务的 ACID 对定义如下:
1.原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
2.一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
3.隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
4.持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
特性的理解
理解事务的 ACID 是理解事务的关键,同时分布式事务中的 ACID 也是沿用单机事务的 ACID 概念,因此需要首先理解单机事务的 ACID。对于事务提供了 ACID 的特性,其中原子性和持久性的含义还是比较明确的,但是一致性和隔离性就不那么好理解了,下面尝试分析一下这两个特性的含义。
隔离性
首先隔离性,隔离性是针对多个事务并发执行提供的并发安全性的保证,多个事务并发执行通常会导致以下几个问题,现象可以参考:
并发编程网 - 何为脏读、不可重复读、幻读
1.脏读(dirty read):事务能读到其他已回滚或者未提交的事务对数据的修改 。
2.不可重复读(non-repeatable read):事务多次读同一条数据得到的结果不一致。
不可重复读和脏读的区别在于脏读读的是其他已经回滚或者未提交的事务对数据的修改,而不可重复读虽然读的都是已经提交的事务的数据,但是可能是多个其他事务对同一条数据的修改,因此一个进行中的事务读一条数据时可能读到多个结果。
3.幻读(phantom read):事务多次读取时能读到其他事务新增的数据,幻读主要指的是新数据插入而不是修改。
事务为了解决上述问题因此引入了隔离性,隔离性的本质就是对并发的控制,但在实现上却需要考虑隔离性对事务并发度的影响,通常隔离的越严格,并发度就会越低,通常需要在二者之间找到合适的平衡点。因此就需要引入另一个描述隔离性的概念也就是隔离级别,SQL 标准的事务隔离级别包括:
读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )
1.读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到,这种级别存在脏读、不可重复读、幻读。
2.读已提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到,这种级别存在不可重复读、幻读。
3.可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的,这种级别存在幻读。
4.串行化(serializable ):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。这个级别下脏读、不可重复读、幻读问题都不存在。
如前面提到的,之所以需要定义这四种隔离级别是因为要平衡隔离性和并发效率,不同的隔离级别下能解决的并发导致的问题不一样。实际的应用中需要根据业务的容忍度,选择合适的隔离级别来达到最好的并发度。对于上述四个隔离级别,其中“读未提交”和“串行化”这两个隔离级别比较好理解,也就是“读未提交”对数据的访问完全不加并发控制,而“串行化”则严格的保证各个事务依次串行执行。
对于"读已提交"和"可重复读"这两个隔离级别比较难理解,用一个例子说明这几种隔离级别。假设我们有张 mysql 数据表如下,数据表 T 中只有一列,其中有一行的值为 1,
- mysql> create table T(c
- int
- ) engine=
- InnoDB
- ;
- insert
- into
- T(c) values(
- 1
- );
下面是按照时间顺序执行两个事务的行为,
![a6328539387b72d04407e6a4a4b1028a.png](https://i-blog.csdnimg.cn/blog_migrate/82b7df902b9e7fe74146fce56af9ef88.jpeg)
在不同的隔离级别下,图中 V1、V2、V3 的返回值是不太一致的,
1.若隔离级别是“读未提交”, 则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
2.若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
3.若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
4.若隔离级别是“串行化”,则在事务 B 执行将 1 改成 2 的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图(视图的概念后续实现中会再解释)。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。不同的隔离级别,数据的行为是不一致的。
一致性
数据库中一致性的概念也是比较含糊,理解可以参考Designing Data-Intensive Applications 以及 如何理解数据库事务中的一致性的概念 中的解释。
The idea of ACID consistency is that you have certain statements about your data (invariants) that must always be true—for example, in an accounting system, credits and debits across all accounts must always be balanced. If a transaction starts with a database that is valid according to these invariants, and any writes during the transaction preserve the validity, then you can be sure that the invariants are always satisfied.
However, this idea of consistency depends on the application’s notion of invariants, and it’s the application’s responsibility to define its transactions correctly so that they preserve consistency. This is not something that the database can guarantee: if you write bad data that