一、ACID
事务的四个特性:原子性(Atomicity)、一致性(Consistent)、隔离性(Isalotion)、持久性(Durable),简称为ACID。原子性、隔离性、持久性都是为了保障一致性而存在的,一致性也是最终的目的。
1.原子性
原子性是指事务的原子性操作,对数据的修改要么全部执行成功,要么全部失败,实现事务的原子性,是基于日志的Redo/Undo机制。
Redo log用来记录某数据块被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据;Undo log是用来记录数据更新前的值,保证数据更新失败能够回滚。
假如数据库在执行的过程中,不小心崩了,可以通过该日志的方式,回滚之前已经执行成功的操作,实现事务的一致性。举个例子,假如某个时刻数据库崩溃,在崩溃之前有事务A和事务B在执行,事务A已经提交,而事务B还未提交。当数据库重启进行 crash-recovery 时,就会通过Redo log将已经提交事务的更改写到数据文件,而还没有提交的就通过Undo log进行roll back。
2.一致性
一致性是指执行事务前后的状态要一致,可以理解为数据一致性。
比如银行转账,100个人互相乱七八糟的转账,但最后总额肯定是固定的。原来总值100万,经过一顿互相转账后,总额一定还是100万,不能因为几重了多钱,也不能因为bug少钱。
3.隔离性
隔离性侧重指事务之间相互隔离,不受影响,这个特性与事务设置的隔离级别有密切的关系。下面会重点说明这一特性。
4.持久性
持久性则是指在一个事务提交后,这个事务的状态会被持久化到数据库中,也就是事务提交,对数据的新增、更新将会持久化到数据库中。即使数据库崩溃,写入的任何数据都不会丢失。
二、数据库的隔离性
数据库的隔离性非常重要,单独拿出来重点说明。
1.需要了解的一些基本概念
脏读
脏读指读取到了其他事务未提交的数据,
未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了不一定最终存在的数据,这就是脏读。
可重复读
可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。
不可重复读
对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
幻读
幻读是指某一次的select操作得到的结果所表征的数据状态无法支撑后续的业务操作。需要与不可重复读区分开。同一个事务中执行两次一样的select操作,得到的结果不一样,这不是幻读,是不可重复读的一种,只会出现在RU、RC隔离级别。由于MVCC机制,RR隔离级别下,事务开始后读的都是同一个快照,也就是快照读,所以不可能同一事务中两次读取的结果不一样。
那到底什么情况才叫幻读?
打个形象点的比方,查看一张桌子某个座位有没有人,发现没人后我想坐下,结果我发现我坐不下去,有人已经在座位上了。我不信,我又查看了一遍有没有人,发现还是没有人。见鬼了,我出现幻觉了。当然这两次查询,一次插入的操作需要在同一个事务里。
这就是幻读。
2.数据库的四个隔离等级
数据库有四个隔离等级:读未提交(Read Uncommitted,RU)、读已提交(Read Committed,RC)、可重复读(Repeatable Read,RR)、序列化(Serializable)。
从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
RU | 可能 | 可能 | 可能 |
RC | 不可能 | 可能 | 可能 |
RR | 不可能 | 不可能 | 可能 |
Serializable | 不可能 | 不可能 | 不可能 |
如何修改数据库的隔离级别?
set [作用域] transaction isolation level [事务隔离级别]
即
SET {SESSION | GLOBAL} TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
1)读未提交
MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。但有利就有弊,这基本上就相当于裸奔啊,所以它连脏读的问题都没办法解决。
RU隔离等级下,任何事务对数据的修改都会第一时间暴露给其他事务,即使事务还没有提交,所以实际情况中基本上不会使用该隔离等级。
2)读已提交
读已提交事务隔离级别是大多数流行数据库的默认事务隔离界别,比如 Oracle。
RC隔离级别下,select采用的方式是快照读,即每个select语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的。
读已提交解决了脏读的问题,但是无法做到可重复读,也没办法解决幻读。
3)可重复读
可重复是对比不可重复而言的,相较于RC,RR的快照是建立在事务开始的基础上的,事务中每一次select操作都是用的同一个快照,所以不会读到其他事务对已有数据的修改,及时其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,可重复读依旧没有解决幻读问题。
4)串行化
串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。
3.MySQL如何实现事务隔离
为了实现可重复读,MySQL 采用了 MVCC (多版本并发控制)的方式。
我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:
当前事务内的更新,可以读到;
版本未提交,不能读到;
版本已提交,但是却在快照创建后提交的,不能读到;
版本已提交,且是在快照创建前提交的,可以读到。
再次强调,RC和RR主要的区别就是在快照的创建上,RR仅在事务开始是创建一次,而RC每次执行语句的时候都要重新创建一次。
三、MySQL的RR隔离级别到底会不会发生幻读?
先说结论:MySQL的RR隔离级别会发生幻读。
网上有文章说MySQL解决了RR级别的幻读,因为间隙锁(Gap Lock)或者说Next-key锁的作用就是用来解决幻读的,并且举了这样一个例子:
假设有一张person表,结构如下:
id name age
1 Bob 10
其中id是自增主键,age是索引。有两个并发事务:
transaction_A:
begin;
select * from person;
update person set age=20 where age=10;
select * from person;
commit;transaction_B:
begin:
insert into person values(null, 'Alice', 10);
commit;事务A第一次select查询,查到Bob,然后把所有年龄是10的记录更新成年龄为20;这个时候事务B想插入一条记录,因为事务A更新操作会获取到(-∞,10], (10, +∞]两个Next-key Lock,所以事务B只能等待锁释放;事务A完成更新操作后,事务B插入。
最后的结果是
id name age
1 Bob 20
2 Alice 10
所以解决了幻读。
我们再来看看幻读的定义:幻读是指某一次的select操作得到的结果所表征的数据状态无法支撑后续的业务操作。
还是上面的例子,如果事务B的insert操作发生在事务A的update之前呢?
事务A第一次select,没问题,只有一个Bob。
事务B插入了Alice这条记录,然后提交了,事务B结束。
事务A把所有年龄是10的记录改成了20,然后又用select查了一遍(这个时候事务A看到的还是只有年龄为20的Bob,因为快照是在事务一开始时生成的),然后提交,事务A结束。
最后结果是什么样呢?
id | name | age |
---|---|---|
1 | Bob | 20 |
2 | Alice | 20 |
事务A全程只查到Bob,认为自己只更新了Bob的年龄;事务B说我插入的Alice这条记录年龄明明是10啊,怎么变20了,谁给我改了。结果就是数据出异常了,还是发生了幻读。
至于“间隙锁(Gap Lock)或者说Next-key锁的作用就是用来解决幻读”这种说法,我觉得是偷换概念,间隙锁确实有防止幻读的作用,因为有间隙锁,其他事务就不能再往这个间隙里面insert数据,自然就不会发生幻读。但是在上面我举的这个例子里面,事务B在insert的时候,事务A只做了一个select操作,没有加任何锁,当然可以往里插数据。
所以,RR隔离级别下,要完全避免幻读,应该是在有更新或插入操作的事务里,使用当前读。
select … lock in share mode 给查询的记录和范围加S锁
select … for update 给查询的记录和范围加X锁。
这样才能真正杜绝幻读。
四、MVCC
MVCC主要有以下特点:
1.MySQL 中 InnoDB 引擎支持 MVCC
2.应对高并发事务, MVCC 比单纯的加行锁更有效, 开销更小
3.MVCC 在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用
4.MVCC 既可以基于乐观锁又可以基于悲观锁来实现
1.MVCC实现原理
InnoDB 中 MVCC 的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列DB_ROW_ID)。
DATA_TRX_ID
记录最近更新这条行记录的事务 ID,大小为 6 个字节
DATA_ROLL_PTR
表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。
DB_ROW_ID
行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header)里都有一个专门的 bit(deleted_flag)来表示当前记录是否已经被删除。
事务 A 对值 x 进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务 ID 为 100,事务 A 的 ID 为 200,该行的隐藏主键为 1。
undo log链事务 A 的操作过程为:
1.对 DB_ROW_ID = 1 的这行记录加排他锁
2.把该行原本的值拷贝到 undo log 中,DB_TRX_ID 和 DB_ROLL_PTR 都不动
3.修改该行的值这时产生一个新版本,更新 DATA_TRX_ID 为修改记录的事务 ID,将 DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录,这样就能通过 DB_ROLL_PTR 找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATE,Undo Log 会组成一个链表,遍历这个链表可以看到这条记录的变迁
4.记录 redo log,包括 undo log 中的修改
那么 INSERT 和 DELETE 会怎么做呢?其实相比 UPDATE 这二者很简单,INSERT 会产生一条新纪录,它的 DATA_TRX_ID 为当前插入记录的事务 ID;DELETE 某条记录时可看成是一种特殊的 UPDATE,其实是软删,真正执行删除操作会在 commit 时,DATA_TRX_ID 则记录下删除该记录的事务 ID。
2.补充说明:Undo log
Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log 里,当事务进行回滚时可以通过undo log 里的日志进行数据还原。
Undo log 的用途
(1)保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
(2)用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
除了Undo log以外,MySQL还有Redo log。
Redo log用来记录某数据块被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据;Undo log是用来记录数据更新前的值,保证数据更新失败能够回滚。当数据库重启进行 crash-recovery 时,就会通过Redo log将已经提交事务的更改写到数据文件,而还没有提交的就通过Undo log进行回滚。
网友评论