一、MySQL事务
1.概念
事务是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
特点:
- 它是数据库最小的工作单元,是不可以再分的
- 它可能包含了一个或者一系列的 DML 语句,包括 insert delete update
2.事务四大特性
特性 | 说明 |
---|---|
原子性(Atomicity) | 事务是一个最小的工作单元,整个工作单元要么一起提交成功,要么全部失败回滚 |
一致性(Consistency) | 事务中操作的数据及状态改变是一致的,即写入资料的结果必须完全符合预设的规则,不会因为出现系统意外等原因导致状态的不一致 |
隔离型(Isolation) | 一个事务所操作的数据在提交之前,对其他事务的可见性设定(一般设定为不可见) |
持久性(Durability) | 事务所做的修改就会永久保存,不会因为系统意外导致数据的丢失 |
3.事务的并发问题
事务的并发问题都是关于读一致性的问题
假设有两个事务,一个是A,一个是B
3.1 脏读(不符合一致性)
- 在A事务里面,首先通过where id = 1查询一条数据,返回name = name1,age = 1的这条数据
- 然后B事务中,同样地是去操作id = 1的这行数据,它通过一个update把这行数据的 age 改成了 11,但是它没有提交。这个时候,在A事务里面,它再次去执行相同的查询操作,发现数据发生了变化,获取到的age变成了 11
这种在一个事务里面,读取了其他事务修改但没有提交的数据,而导致了前后两次读取数据不一致的情况,就称为脏读
3.2 不可重复读(不符合隔离性)
- A事务中,通过id = 1查询到一条数据
- 然后在B事务里面执行了一个update操作,然后以后commit提交了修改
- 然后A事务读取到了其他事务已提交的数据,导致在A事务中前后两次读取数据不一致的情况
在一个事务里,读取了其他事务修改但没有提交的数据,而导致了前后两次读取数据不一致的情况,就称为不可重复读
3.3 幻读(不符合隔离性)
- A事务执行了一个范围查询,假设这时只有3条数据符合条件
- 这时B事务执行了插入语句,新插入了一条数据并commit了
- A事务重新执行相同的查询语句,发现符合条件的数据变成了4条
在一个事务里,由于其他事务插入数据造成前后两次读取数据数据不一致的情况,就称为幻读
4.事务隔离级别
想要解决并发事务问题,就需要通过一定的事务隔离级别,SQL92标准定义了四个事务隔离级别,隔离级别越高,并发度就越低。
隔离级别 | 描述 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交(Read Uncommitted) | 一个事务可以读取到其他事务未提交的数据。没有解决任何问题 | 存在 | 存在 | 存在 |
读已提交(Read Committed) | 一个事务只能读取到其他事务已提交的数据,不能读取其他事务未提交数据。解决了脏读问题 | 不存在 | 存在 | 存在 |
可重复读(Repeatable Read) | 解决了不可重复读问题,MySQL默认级别 | 不存在 | 不存在 | 存在**(InnoDB中不存在)** |
串行化(Serializeble) | 所有事务串行化执行,因此也就不存在并发,所以解决了所有问题 | 不存在 | 不存在 | 不存在 |
注意!InnDB在RR级别就解决了幻读问题(通过临键锁和MVCC)! 这也是为什么InnoDB选择可重复读作为默认隔离级别的原因
二、实现事务隔离的方案
想要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,大体上有两类方案:
-
基于锁的并发控制(Lock Based Concurrency Control,LBCC)
如果仅仅基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而大多数应用都是读多写少的,这样会极大影响操作数据的效率
-
多版本并发控制(Multi Version Concurrency Control,MVCC)
利用快照,事务只能查看自身开始前就已经存在的数据,事务开始后新增的数据是看不到的
三、MVCC
1.基本思想
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。
2.基本特征
- 每行数据都存在一个版本,每次数据更新时都更新该版本。
- 修改时Copy出当前版本随意修改,各个事务之间无干扰。
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
3.InnoDB中MVCC的实现
InnoDB为每行记录都实现了两个隐藏字段
隐藏字段 | 长度 | 描述 |
---|---|---|
DB_TRX_ID | 6字节 | 可以理解为创建版本号,插入或更新该行的最后一个事务ID,事务ID是自动递增的 |
DB_ROLL_PTR | 7字节 | 记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息 |
4.几个基本概念
(1)版本链
每次对数据进行修改,都会在undo log中记录被修改前的数据版本,然后修改后的数据尾部使用DB_ROLL_PTR指向前一版本的记录,依次类推,最终形成链状结构,这就是版本链
每次对数据的修改都会在undo log中记录被修改前的数据版本,然后修改后的数据尾部使用DB_ROLL_PTR指向前一版本的记录,依次类推,最终形成链状结构,这就是版本链
(2)ReadView
- RR隔离级别下在第一次查询时创建read view
- RC隔离级别下会在每次查询时创建read view
ReadView中主要就是有个列表来存储系统中当前活跃着的读写事务,也就是begin了但还未提交的事务,通过这个列表来判断记录的某个版本是否对当前事务可见
假设当前列表里的事务id为[80,100]
- 如果要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的
- 如果要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就需要判断一下是否在列表内
- 如果在那就说明此事务还未提交,所以版本不能被访问
- 如果不在那说明事务已经提交,所以版本可以被访问
- 如果要访问的记录版本的事务id为110,比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问
这些记录都是去版本链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束
(3)快照读
读取的是记录数据的可见版本(可能是过期的数据),不用加锁
(4)当前读
读取的是记录数据的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不会再并发的修改这条记录
5.特点
- MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read)
- Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC,因为MVCC的创建版本和删除版本只有在事务提交后才会产生
- 串行化由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题
7.MySQL并非真正的MVCC
在 InnoDB 中,MVCC 和锁是协同使用的,而MVCC本身是一种乐观锁实现。乐观锁主要依靠版本控制,即消除锁,二者相互矛盾,所以从某种意义上来说,MySQL的MVCC并非真正的MVCC,只是借用MVCC的名号实现了读的非阻塞而已。
四、隔离级别实现原理
Read Uncommited
RU 隔离级别:没有任何操作,不解决任何问题
Serializable
Serializable 所有的 select 语句都会被隐式的转化为 select … in share mode,会和 update、delete 互斥。
RR 隔离级别
普通的 select 使用快照读(snapshot read),底层使用 MVCC 来实现。加锁的 select(select … in share mode / select … for update)以及更新操作update, delete 等语句使用当前读(current read),底层使用记录锁、或者间隙锁、临键锁。
RC 隔离级别
- 普通的 select 都是快照读,使用 MVCC 实现
- 加锁的 select 都使用记录锁,因为没有Gap Lock
五、MySQL如何实现ACID
事务特性 | 实现方式 |
---|---|
原子性 | undo log,失败回滚,保证事务原子性 |
隔离性 | 锁以及MVCC |
持久性 | redo log,故障后根据redo log恢复,保证持久性 |
一致性 | 通过隔离级别、回滚等保证并发下的一致性 |