Mysql事务特性
参考:
图解MySQL介绍 | 小林coding | Java面试学习
1. 简单介绍
mysql事务的四大特性即ACID。
1.1 原子性(Atomicity)
一句话描述:事务是一个不可分割的工作单位,事务中的操作要么全部完成,要么全部不做;
如何保证:原子性是通过 undo log(回滚日志) 来保证的;
1.2 持久性(Durability)
一句话描述:事务一旦提交,其结果就是永久性的,即使发生系统故障也不会丢失;
如何保证:持久性是通过 redo log (重做日志)来保证的;
1.3 隔离性(Isolation)
一句话描述:一个事务的执行不能被其他事务干扰。多个并发事务之间彼此隔离;
如何保证:隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
1.4 一致性(Consistency)
一句话描述:事务必须使数据库从一个一致性状态转换到另一个一致性状态。在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏;
如何保证:一致性则是通过持久性+原子性+隔离性来保证;
2. 如何保证原子性?
我们在执⾏执⾏⼀条“增删改”语句的时候,虽然没有输⼊ begin 开启事务和 commit 提交事务,但 是 MySQL 会隐式开启事务来执⾏“增删改”语句的,执⾏完就⾃动提交事务的,这样就保证了执⾏ 完“增删改”语句后,我们可以及时在数据库表看到“增删改”的结果了。
但如果⼀个事务在执⾏过程中,在还没有提交事务之前,如果 MySQL 发⽣了崩溃,要怎么回滚到事务之前的数据呢?
实现这一机制的就是undo log。
每当 InnoDB 引擎对⼀条记录进⾏操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log ⾥,⽐如:
在删除⼀条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的 记录插⼊到表中就好了;
在更新⼀条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就 好了。
在发⽣回滚时,就读取 undo log ⾥的数据,然后做原先相反操作。⽐如当 delete ⼀条记录时, undo log 中会把记录中的内容都记下来,然后执⾏回滚操作的时候,就读取 undo log ⾥的数据, 然后进⾏ insert 操作。
通过undo log,每次事务没有执行完就发生了意外,就可以通过其回滚。
3. 如何保证持久性?
为了防止断电,导致之前更新的数据还在Buffer Pool中没有写入磁盘,当有一条记录需要更新的时候,InnoDB会先更新Buffer Pool中的数据(Buffer Poll就是一个缓存池,用来存取mysql中的热点数据,提高查询效率),然后将本次对页的修改用redo log记录下来。后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏⻚刷新到磁盘⾥,这就 是 WAL (Write-Ahead Logging)技术。
这里有一个问题,既然都要写入redo log(redo log本身又存储在磁盘中),那么为什么不直接更新Buffer Pool的数据到磁盘呢?
写⼊ redo log 的⽅式使⽤了追加操作, 所以磁盘操作是顺序写,⽽写⼊数据需要先找到写⼊位 置,然后才写到磁盘,所以磁盘操作是随机写。 磁盘的「顺序写 」⽐「随机写」 ⾼效的多,因此 redo log 写⼊磁盘的开销更⼩。
而且实际上,执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,redo log 也有⾃⼰的缓存—— redo log buffer,每当产⽣⼀条 redo log 时,会先写⼊到 redo log buffer,后续在持久化到磁盘。主要有以下几个时机进行刷盘,1.MySQL 正常关闭时; 2.当 redo log buffer 中记录的写⼊量⼤于 redo log buffer 内存空间的⼀半时,会触发落盘; 3.InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。4.每次事务提交时都将缓存在 redo log buffer ⾥的 redo log 直接持久化到磁盘。
通过redo log,事务提交之后发⽣了崩溃,重启后会通过 redo log 恢复事务。
4. 如何保证隔离性?
4.1 为什么事务要有隔离性?
脏读:
如果⼀个事务「读到」了另⼀个「未提交事务修改过的数据」,就意味着发⽣了「脏读」现象。
举例:A存了100w但是还没有提交,B此时读到了余额100w,此时A事务发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
不可重复读:
在⼀个事务内多次读取同⼀个数据,如果出现前后两次读到的数据不⼀样的情况,就意味着发⽣ 了「不可重复读」现象。
举例:A读取余额是100w,此时B又存了100w,A再次读取就是200w,发现前后两次读到的数据是不⼀致的,这种现象就被称为不可重复读。
幻读:
在⼀个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不 ⼀样的情况,就意味着发⽣了「幻读」现象。
举例:A查询自己手机号码下的账户有两个,此时B又用A的手机号码新建了一个,此时A再次读取发现自己手机号码下的账户有三个。
4.2 事务的隔离级别
1. 读未提交(read uncommitted),指⼀个事务还没提交时,它做的变更就能被其他事务看到;
2. 读已提交(read committed),指⼀个事务提交之后,它做的变更才能被其他事务看到;
3. 可重复读(repeatable read),指⼀个事务执⾏过程中看到的数据,⼀直跟这个事务启动时看到 的数据是⼀致的,MySQL InnoDB 引擎的默认隔离级别;
4. 串⾏化(serializable );会对记录加上读写锁,在多个事务对这条记录进⾏读写操作时,如果 发⽣了读写冲突的时候,后访问的事务必须等前⼀个事务执⾏完成,才能继续执⾏;
4.3 四种事务的隔离级别是怎么实现的?
1.读未提交:直接读最新的数据就行了
2.读已提交:每个语句执行前都生成一个Read View
3.可重复读:启动事务时生成一个快照,整个事务都用这个Read View
4.串行化:通过加读写锁的方式来避免并行访问
4.4 Read View在MVCC中如何工作?
先看看Read View是个什么东西
m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是⼀个 列表,“活跃事务”指的就是,启动了但还没提交的事务。
min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最⼩的事务, 也就是 m_ids 的最⼩值。
max_trx_id :这个并不是 m_ids 的最⼤值,⽽是创建 Read View 时当前数据库中应该给下⼀个 事务的 id 值,也就是全局事务中最⼤的事务 id 值 + 1;
creator_trx_id :指的是创建该 Read View 的事务的事务 id。
⼀个事务去访问记录的时候,除了⾃⼰的更新记录总是可⻅之外,还有这⼏种情况:
其实就是用查到的这条数据的trx_id和当前事务的Read View作比较
如果记录的 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 列表中,表⽰⽣成该版本记录的活跃事务已经被提交,所以 该版本的记录对当前事务可⻅。 这种通过「版本链」来控制并发事务访问同⼀个记录时的⾏为就叫 MVCC(多版本并发控制)。
4.5 总结
要解决脏读现象,就要将隔离级别升级到读已提交以上的隔离级别.
要解决不可重复读现象,就 要将隔离级别升级到可重复读以上的隔离级别。
⽽对于幻读现象,不建议将隔离级别升级为串⾏化,因为这会导致数据库并发时性能很差。 MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很⼤程度上避免幻读现象。
解决的⽅案有两种:
针对快照读(普通 select 语句),是通过 MVCC ⽅式解决了幻读,因为可重复读隔离级别下, 事务执⾏过程中看到的数据,⼀直跟这个事务启动时看到的数据是⼀致的,即使中途有其他事 务插⼊了⼀条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)⽅式解决 了幻读,因为当执⾏ select ... for update 语句的时候,会加上 next-key lock,如果有其他事务 在 next-key lock 锁范围内插⼊了⼀条记录,那么这个插⼊语句就会被阻塞,⽆法成功插⼊,所 以就很好了避免幻读问题。