文章目录
- 优秀借鉴
- 1、事务由来
- 2、何为ACID
- 2.1、Atomicity原子性
- 2.2、Consistency一致性
- 2.3、Isolation隔离性
- 2.4、Durability持久性
- 3、聊回事务
- 3.1、概念
- 3.2、语法
- 3.2.1、开启事务
- 3.2.2、提交事务
- 3.2.3、回滚事务
- 4、隔离级别
- 4.1、引入
- 4.2、并发问题
- 4.2.1、脏读
- 4.2.2、不可重复读
- 4.2.3、幻读
- 4.2.4、三者区别
- 4.2.5、补充
- 4.3、四种隔离级别
优秀借鉴
书籍:
- 《MySQL是怎样运行的:从根儿上理解 MySQL》
- 大幅度借鉴了上述书籍,一些晦涩难懂的概念解释的很到位,推荐条件允许的小伙伴入手一本
博客&论文:
- 深入学习MySQL事务:ACID特性的实现原理
- 数据库事务与MySQL事务总结
- 不可重复读和幻读的区别
- A Critique of ANSI SQL Isolation Levels-译版
- A Critique of ANSI SQL Isolation Levels-原版
1、事务由来
对于大部分程序员来说,他们的任务就是把现实世界的业务场景映射到数据库世界中。举一个祖传转账的例子,银行会为人们的账户信息建立一个account
表:
CREATE TABLE account (
id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
name VARCHAR(100) COMMENT '客户名称',
balance INT COMMENT '余额',
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
二狗和五郎是一对好基友,他们一起到了这家银行开了一个账户,其中二狗是地主家的儿子,存了十一块巨款,而五郎比较精简,就只有两块钱。
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 二狗 | 11 |
| 2 | 五郎 | 2 |
+----+--------+---------+
有一天五郎想要吃个豪华早餐但钱不够,便找了二狗借五块钱,因此二狗拿出了他的大哥大给五郎转了五块钱。熟悉数据库的我们都知道,对数据库而言,这相当于执行了下面两条语句:
UPDATE account SET balance = balance - 5 WHERE id = 1;
UPDATE account SET balance = balance + 5 WHERE id = 2;
突然在转账过程中,一个不知名小鸡子用鸡爪把服务器电源线给弄断了,导致刚刚执行完第一条语句把二狗的钱扣了,但是五郎还是只有一块钱的窘境,不但吃不起豪华早餐,连二狗的五块钱都不翼而飞了。
从上面的小案例中可以看出,因为小鸡子的鸡爪,导致二狗和五郎的转账出现了事故,但在我们现实世界中却很难出现这种情况,要么就是我把钱顺利的给你了,要么就是中间出现了什么事故我把拿出来的钱收回来了,事故出现的概率极低。而数据库世界中为了避免这种事故的发生,便引入了广大求职人又爱又恨的事务(Transaction)。下面将针对事务的特性进行展开介绍。
2、何为ACID
2.1、Atomicity原子性
在现实世界中,转账操作是不可分割的操作。也就是说要么压根没转,要么转账成功,不存在中间状态,即上面说到转到一半的情况。
因此便出现原子性这一规则:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
2.2、Consistency一致性
数据库世界只是现实世界的一个映射,现实世界中存在的约束当然也要在数据库世界有所体现。如果数据库中的数据全部符合现实世界中的约束,我们就说这些数据就是一致的,或者说符合一致性的。
如何保证数据库中数据的一致性(就是符合所有现实世界的约束)呢?这其实靠两方面的努力:
- 数据库本身能为我们保证一部分一致性需求。
我们知道MySQL
数据库可以为表建立主键、唯一索引、外键、声明某个列为NOT NULL
来拒绝NULL
值的插入。比如说当我们对某个列建立唯一索引时,如果插入某条记录时该列的值重复了,那么MySQL
就会报错并且拒绝插入。除了这些我们已经非常熟悉的保证一致性的功能,MySQL
还支持CHECK
语法来自定义约束,但也仅仅是支持CHECK语法,但实际上并没有一点卵用,MySQL
并不会去检查CHECK
子句中的约束是否成立。
- 更多的一致性需求需要靠写业务代码的程序员自己保证。
现实生活中复杂的一致性需求比比皆是,而由于性能问题把一致性需求交给数据库去解决这是不现实的,所以这个锅就甩给了业务端程序员,没错,可能就是我们这一坨写Java的。比方说我们的account
表,只要编写业务的程序员在自己的业务代码里判断一下,当某个操作会将balance
列的值更新为小于0的值时,就不执行该操作就可以很简单的解决了这一问题。
2.3、Isolation隔离性
现实世界中的两次状态转换应该是互不影响的,比如说二狗向五郎同时进行的两次金额为5元的转账(假设可以在两个ATM机上同时操作)。那么最后二狗的账户里肯定会少10元,五郎的账户里肯定多了10元。但是到对应的数据库世界中,事情又变的复杂了一些。为了简化问题,我们粗略的假设二狗向五郎转账5元的过程是由下面几个步骤组成的:
- 步骤一:读取二哈账户的余额到变量A中,这一步骤简写为
read(A)
。 - 步骤二:将二哈账户的余额减去转账金额,这一步骤简写为
A = A - 5
。 - 步骤三:将二哈账户修改过的余额写到磁盘里,这一步骤简写为
write(A)
。 - 步骤四:读取五郎账户的余额到变量B,这一步骤简写为
read(B)
。 - 步骤五:将五郎账户的余额加上转账金额,这一步骤简写为
B = B + 5
。 - 步骤六:将五郎账户修改过的余额写到磁盘里,这一步骤简写为
write(B)
。
我们将二哈向五郎同时进行的两次转账操作分别称为T1
和T2
,在现实世界中T1
和T2
是应该没有关系的,可以先执行完T1
,再执行T2
,或者先执行完T2
,再执行T1
,对应的数据库操作就像这样:
但是很不幸,真实的数据库中T1
和T2
的操作可能交替执行,与并发问题是一个道理的,比如这样:
如果按照上图中的执行顺序来进行两次转账的话,最终二哈的账户里还剩6
元钱,相当于只扣了5元钱,但是五郎的账户里却成了12
元钱,相当于多了10元钱,银行成为了妥妥的大冤种啊!
所以对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以原子性
的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则被称之为隔离性
。也就是事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
2.4、Durability持久性
这一特性很好理解,字面意思就已经进行解释了:事务一旦提交,它对数据库的改变就应该是永久性的。就比如我刚存了一元巨额进账户,当提示存款成功时那么证明操作是完成的了,那么我就可以把卡拔走继续送外卖了。如果在我走了之后银行把这次操作撤销,余额变回了存款之前的,那走的时候多少得带点血压。
当把现实世界的状态转换映射到数据库世界时,持久性
意味着该转换对应的数据库操作所修改的数据都应该在磁盘上保留下来,不论之后发生了什么事故,本次转换造成的影响都不应该被丢失掉。
3、聊回事务
3.1、概念
通过上面对ACID的描述,我们现在知道了事务
是一个抽象的概念,它其实对应着一个或多个需要保证原子性
、隔离性
、一致性
和持久性
的数据库操作,同时根据操作所执行的不同阶段,将事务分成了以下几个状态:
- 活动的(active)
- 事务对应的数据库操作正在执行过程中时,我们就说该事务处在
活动的
状态。
- 事务对应的数据库操作正在执行过程中时,我们就说该事务处在
- 部分提交的(partially committed)
- 当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在
部分提交的
状态。
- 当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在
- 失败的(failed)
- 当事务处在
活动的
或者部分提交的
状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的
状态。
- 当事务处在
- 中止的(aborted)
- 如果事务执行了半截而变为
失败的
状态,比如前面介绍的转账事务出现了错误,从而当前事务处在了失败的
状态,那么就需要把已经修改的账户余额调整为未转账之前的金额,换句话说,就是要撤销失败事务对当前数据库造成的影响。书面一点的话,我们把这个撤销的过程称之为回滚
。当回滚
操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的
状态。
- 如果事务执行了半截而变为
- 提交的(committed)
- 当一个处在
部分提交的
状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的
状态。
- 当一个处在
只有当事务处于
提交的或者中止的状态
时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。
随着事务对应的数据库操作执行到不同阶段,事务的状态也在不断变化,一个基本的状态转换图如下所示:
3.2、语法
3.2.1、开启事务
方式一:BEGIN [WORK]
:BEGIN
语句代表开启一个事务,后边的单词WORK
可有可无。
BEGIN;
方式二:START TRANSACTION
:START TRANSACTION
语句和BEGIN
语句有着相同的功效,都标志着开启一个事务。
START TRANSACTION;
不过与BEGIN
语句区别的是,可以在START TRANSACTION
语句后边跟随几个修饰符
:
READ ONLY
:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据;READ WRITE
:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据;WITH CONSISTENT SNAPSHOT
:启动一致性读。
如果是想在START TRANSACTION
后边跟随多个修饰符
的话,可以使用逗号将修饰符
分开,比如开启一个只读事务和一致性读:
START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT;
需要大家注意的一点是,READ ONLY
和READ WRITE
是用来设置所谓的事务访问模式
的,就是以只读还是读写的方式来访问数据库中的数据,一个事务的访问模式不能同时既设置为只读
的也设置为读写
的,所以我们不能同时把READ ONLY
和READ WRITE
放到START TRANSACTION
语句后边。另外,如果我们不显式指定事务的访问模式,那么该事务的访问模式就是读写
模式。
3.2.2、提交事务
COMMIT [WORK]
语句就代表提交一个事务,后边的WORK
可有可无。
COMMIT;
3.2.3、回滚事务
也可以称为手动终止事务。ROLLBACK [WORK]
语句就代表中止并回滚一个事务,后边的WORK
可有可无类似的。
ROLLBACK;
4、隔离级别
4.1、引入
我们知道MySQL
是一个客户端/服务器
架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为一个会话(Session
)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。
在ACID中提及到一个隔离性
的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样子的话对性能影响太大,我们既想保持事务的隔离性
,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,鱼和熊掌不可得兼,舍一部分隔离性
而取性能者也。
接下来的介绍将会用到一个新的表:
CREATE TABLE hero (
number INT COMMENT '主键',
name VARCHAR(100) COMMENT '姓名',
country varchar(100) COMMENT '国家',
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;
INSERT INTO hero VALUES(1, '刘备', '蜀');
4.2、并发问题
4.2.1、脏读
如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读(Dirty Read)
。
以下图为例,Session A
和Session B
各开启了一个事务,Session B
中的事务先将number
列为1
的记录的name
列更新为'关羽'
,然后Session A
中的事务再去查询这条number
为1
的记录,如果读到列name
的值为'关羽'
,而Session B
中的事务稍后进行了回滚,那么Session A
中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读
。
如果我们将Tn
视为第n个事务,wn[x]
视为Tn
对x的写操作,rn[x]
视为Tn
对x的读操作,cn
视为Tn
的提交(commit),an
视为Tn
的中止(abort),...
表示其他操作,那么我们可以得到一个脏读
的操作执行序列如下(默认开启了事务):
w1[x] ... r2[x] ... (a1 and c2 in any order)
也就是T1
先修改了数据项x的值,然后T2
又读取了未提交事务T1
针对数据项x修改后的值,之后T1
中止而T2
提交,这就意味着T2
读到了一个根本不存在的值,这就是脏读的严格解释。
4.2.2、不可重复读
如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读(Non-Repeatable Read)
以下图为例,我们在Session B
中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了number
列为1
的记录的列name
的值,每次事务提交之后,如果Session A
中的事务都可以查看到最新的值,这种现象也被称之为不可重复读
。
如果我们将Tn
视为第n个事务,wn[x]
视为Tn
对x的写操作,rn[x]
视为Tn
对x的读操作,cn
视为Tn
的提交(commit),an
视为Tn
的中止(abort),...
表示其他操作,那么我们可以得到一个不可重复读
的操作执行序列如下(默认开启了事务):
r1[x] ... w2[x] ... c2 ... r1[x] ... c1
也就是T1
先读取了数据项x的值,然后T2
又修改了未提交事务T1
读取的数据项x的值,之后T2
提交,然后T1
再次读取数据项x的值时会得到与第一次读取时不同的值,这就是不可重复读
的严格解释。
4.2.3、幻读
如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读
。
以下图为例,Session A
中的事务先根据条件number > 0
这个条件查询表hero
,得到了name
列值为'刘备'
的记录;之后Session B
中提交了一个隐式事务,该事务向表hero
中插入了一条新记录;之后Session A
中的事务再根据相同的条件number > 0
查询表hero
,得到的结果集中包含Session B
中的事务新插入的那条记录,这种现象也被称之为幻读
。
如果我们将Tn
视为第n个事务,wn[x]
视为Tn
对x的写操作,wn[y in p]
视为Tn
写入一些符合搜索条件p的记录y,rn[x]
视为Tn
对x的读操作,rn[p]
视为Tn
以搜索条件p进行读操作,cn
视为Tn
的提交(commit),an
视为Tn
的中止(abort),...
表示其他操作,那么我们可以得到一个幻读
的操作执行序列如下(默认开启了事务):
r1[p] ... w2[y in p] ... c2 ... r1[p] ... c1
也就是T1
先读取符合搜索条件p的记录,然后T2
写入了符合搜索条件p的记录,然后T2
提交,之后T1
再读取符合搜索条件p的记录时,会发现两次读取的记录是不一样的,这就是幻读
的严格解释。
4.2.4、三者区别
看到这里可能会有一点儿懵,三个好像有点类似又不太一样的,因此在这里对三个进行一个对比,便于大家伙们区别:
- 脏读的重点在于未提交。脏读应该是三个里面最好理解的,其定义很轻易便能理解,一个事务中读取了另外一个事务未提交的数据,是先修改再读;
- 不可重复读的重点在于对单条数据读取了两遍。
T1
先读取了一遍,而后T2
修改该数据并提交,最后T1
再次读取了该数据发现与之前的不同; - 幻读的重点在于针对一类条件对一系列数据读取了两边。比较特殊的点在于幻读是具备条件的查询,这种查询可能查出来的并不只有一条数据,而在两次查询过程中另外一个事务对查询的结果集中的某条数据进行了变动。
1️⃣ | 2️⃣ | 3️⃣ | 4️⃣ | 5️⃣ | 6️⃣ | 7️⃣ | 8️⃣ | 9️⃣ | |
---|---|---|---|---|---|---|---|---|---|
脏读 | w1[x] | … | r2[x] | … | a1 && c2 | ||||
不可重复读 | r1[x] | … | w2[x] | … | c2 | … | r1[x] | … | c1 |
幻读 | r1[p] | … | w2[y in p] | … | c2 | … | r1[p] | … | c1 |
在阅读不可重复读和幻读两个概念的时候,狗子我是有点儿懵的,因此去找到了一篇文章从另外一个角度对两者进行了区别,感兴趣的大家伙们可以去看一下->《不可重复读和幻读的区别》。其主要是从锁机制的角度出发对两者进行了不同的分析:不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
4.2.5、补充
在论文A Critique of ANSI SQL Isolation Levels(后文简称论文)中除了上述的三种并发问题,还引入了脏写、丢失更新、读偏斜、写偏斜等现象并且划分了更详细的隔离级别,这时SQL92标准中并没有指出的,其中的脏写在书籍《MySQL是怎样运行的:从根儿上理解 MySQL》中有做介绍。
与此同时,在论文中对幻读的描述也与SQL标准有所不同:
SQL标准
中只认为INSERT操作才会引起幻读现象;论文
中强调INSERT、DELETE、UPDATE
操作均可引起幻读现象。
4.3、四种隔离级别
根据上述的并发问题,根据可能导致一致性问题的严重性给这些现象排一下序:
脏写 > 脏读 > 不可重复读 > 幻读
其中脏写这个现象对一致性影响过于严重,因此无论是下面介绍的哪种隔离级别都不允许脏写的情况发生。
我们上面所说的舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,越严重的问题就越可能发生。因此在SQL标准
中设立了以下4个隔离级别
:
READ UNCOMMITTED
:未提交读。所有事务都可以看到其他未提交事务的执行结果;READ COMMITTED
:已提交读。一个事务只能看见已经提交事务所做的改变;REPEATABLE READ
:可重复读。确保了同一事务的多个实例在并发读取数据时,会看到同样的数据行;SERIALIZABLE
:可串行化。强制事务串行,并发效率很低。
注意的是,不同的数据库厂商对
SQL标准
中规定的4中隔离级别的支持是不一样的。其中MySQL
的默认隔离级别为REPEATABLE READ
,在该隔离级别下可以很大程度上禁止了幻读现象的发生。
下面表格展示了在SQL标准中规定的并发事务执行过程中可能发生的现象,其中✔️代表可能发生现象,❌代表不可能发生现象:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | ✔️ | ✔️ | ✔️ |
READ COMMITTED | ❌ | ✔️ | ✔️ |
REPEATABLE READ | ❌ | ❌ | ✔️ |
SERIALIZABLE | ❌ | ❌ | ❌ |