MySQL事务
事务的4个特性,ACID
- 原子性(Atomicity)
要么都完成,要么都不完成。
通过undo log(回滚日志)来保证的。
- 一致性(Consistency)
与现实世界的逻辑一致。比如钱不能是负的;转账时双方一个钱减少,一个增多。
一致性是最终目的,通过另外三种性质保证,即原子性、隔离性和持久性都是为了保证一致性而存在的。
- 隔离性(Isolation)
并发执行的事务之间不能相互影响
通过 MVCC(多版本并发控制)或锁机制来保证的
- 持久性(Durability)
保证内存(buffer)中的数据都能写进磁盘
通过 redo log (重做日志)来保证的
隔离性保证
事务在并行执行时不可避免的会出现多个事务同时读或者写同一条数据的情况。这里的同时,并不是严格的同一时刻,而是两个事务在完成之前的时间段内同时访问或更改了某一条数据。
其实有一种很简单的办法来保证隔离性,消除并发的影响。那就是不并发,完全地串行化执行。在一个事务执行完毕之前,所有其他事务都不许动,都不许执行,给我等着(也不一定这么严格的串行,可以通过加锁来达到同样的效果)。显然,由于效率问题,我们不能采用这种方案。
又到了最爱的又当又立环节,我既要速度,又要稳定(不产生矛盾)。
并行事务可能会面临的问题:
- 脏读(dirty read):读到其他事务未提交的数据
- 不可重复读(non-repeatable read):前后读取的数据不一致
- 幻读(phantom read):前后读取的记录数量不一致
严重性:脏读>不可重复读>幻读
脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
如图事务B读取了未提交事务A修改过的数据。其实未提交不是主要问题,因为如果你最后提交的确实是这个数据,那问题也不大,就相当于提前知道结果了呗。但是,上面这张图,他发生了回滚,也就是前面的操作都不算数了,那个被事务B读到的自己曾经修改过的数据也不算数了,这就产生问题了。因为事务的原子性,所以一个事务没有提交时,就处于一种量子态,薛定谔的猫,要么成功提交,要么回滚,前面的所有操作都不算数。所以,一个未提交的事务尽管可能暂时对数据进行了修改,但这个修改是否能持续到事务提交是我们要考虑到问题。
总之就是,当我们读一个未提交的被修改过的数据时,我们无法保证那个事务不回滚,无法保证那个修改不被撤销,无法保证我们读取的数据还是对的,还能保持一致性。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
如图,事务A先后两次读取了同一条数据,但是这条数据在这两次读取之间被另一个事务修改了,进而导致事务A两次读取的结果不一样。
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
见鬼了,刚才不是5条吗,现在怎么成6条了。幻读的幻,也可以认为是幻影,看到了5条数据和1条幻影。
四种隔离级别
- 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
- 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到; (使用Read View 数据快照来实现,在每个语句执行前都会重新生成一个Read View)
- 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的(只能看见启动事务时的数据),MySQL InnoDB 引擎的默认隔离级别; (使用Read View 数据快照来实现,启动事务时生成一个Read View,然后整个事务期间都在用这个Read View)
- 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
三种并发可能出现的问题,四种隔离级别,就像四棵树有三个间距,每个间距解决一种问题。如下图
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了)
InnoDB如何解决幻读问题
面试被问到。
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
MVCC
Multi-Version Concurrency Control,多版本并发控制
Read View
- creator_trx_id :指的是创建该 Read View 的事务的事务 id。
- m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
- min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务id 最小的事务,也就是 m_ids 的最小值。
- max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;(所以事务id是递增的)
下面这张图直观地解释了上面的几个字段的含义。注意m_ids并不是min_trx_id和max_trx_id之间的所有值,而是两者之间的活跃事务。
注意,读提交隔离级别是在每条语句都创建Read View(所以在事务执行过程中能洞察到是否存在其他事务的提交行为,每次创建Read View会更新这四个字段,进而洞悉外面其他事务的提交情况);可重复读隔离级别只在事务开始时创建Read View。Read View就是这四个字段形成的一个元组。
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
-
如果记录的 trx_id 值小于 Read View 中的
min_trx_id
值,表示这个版本的记录是在创建 Read View前已经提交的事务生成的,所以该版本的记录对当前事务可见。 -
如果记录的 trx_id 值大于等于 Read View 中的
max_trx_id
值,表示这个版本的记录是在创建 Read View后才启动的事务生成的,所以该版本的记录对当前事务不可见。 -
如果记录的 trx_id 值在 Read View 的
min_trx_id
和max_trx_id
之间,需要判断 trx_id 是否在 m_ids 列表中:- 如果记录的 trx_id在
m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 - 如果记录的 trx_id不在
m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
- 如果记录的 trx_id在
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
用锁保证隔离性
锁的类型
- X锁:排他锁,Exclusive Lock,事务对数据加上X锁时,只允许此事务读取和修改此数据,并且其它事务不能对该数据加任何锁;
- S锁:共享锁,Shared Lock,加了S锁后,该事务只能对数据进行读取而不能修改,并且其它事务只能加S锁,不能加X锁;
- IX锁,意向排他锁,Intention Exclusive Lock,一个事务在获得某个数据行对象的 X 锁之前,必须先获得整个表的 IX 锁;
- IS锁,意向共享锁,Intention Shared Lock,一个事务在获得某个数据行对象的 S 锁之前,必须先获得整个表的 IS 锁或更强的锁;
意向锁的好处:如果一个事务想要对整个表加X锁,就需要先检测是否有其它事务对该表或者该表中的某一行加了锁,这种检测非常耗时。有了意向锁之后,只需要检测整个表是否存在IX/IS/X/S锁就行了
三级封锁协议
- 一级封锁协议:事务在修改数据之前必须先对其加X锁,直到事务结束才释放。可以解决丢失修改问题(两个事务不能同时对一个数据加X锁,避免了修改被覆盖);
- 二级封锁协议:在一级的基础上,事务在读取数据之前必须先加S锁(别的事物想读也要先加S锁),读完后释放。可以解决脏读问题(如果已经有事务在修改数据,就意味着已经加了X锁,此时想要读取数据的事务并不能加S锁,也就无法进行读取,避免了读取脏数据);
- 三级封锁协议:在二级的基础上,事务在读取数据之前必须先加S锁,直到事务结束才能释放。可以解决不可重复读问题(避免了在事务结束前其它事务对数据加X锁进行修改,保证了事务期间数据不会被其它事务更新)
简单概括:
封锁等级 | 解决问题 | 实现方法 |
---|---|---|
一级 | 解决丢失修改问题 | 写数据先加X锁,事务结束才释放 |
二级 | 解决脏读问题 | 写数据先加X锁,事务结束才释放;读之前先加S锁,读完释放 |
三级 | 解决不可重复读问题 | 写数据先加X锁,事务结束才释放;读之前先加S锁,事务结束才释放 |
隔离性的实现方法比较
- 读未提交:什么都不做
- 读提交:MVCC,二级封锁
- 可重复读:MVCC,三级封锁
- 串行化:强制事务串行执行
读未提交会发生脏读,读提交解决脏读;读提交可能产生不可重复读,可重复读解决不可重复读;可重复读可能产生幻读,串行化解决幻读。幻读的解决方法上面有提到,使用MVCC快照或者next-key lock
参考链接
事务隔离级别是怎么实现的? | 小林coding
Waking-Up/Database.md at master · wolverinn/Waking-Up