事务是对数据库独立的一组操作,操作中可能涉及到对数据库的多次读写,一个事务中的操作要么全部执行成功,要么都不执行
一、事务的四个特性
1. 原子性
一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。Innodb引擎事务的原子性是基于undolog版本链表实现的,会将修改前的值插入链表然后再进行修改操作,若事务执行失败则进行回滚,根据undolog日志恢复数据,这样就实现了事务的原子性。
2. 一致性
是指事务操作前和操作后,数据满足完整性约束,符合预期逻辑,保持一致性状态,而不会出现随机的、混乱的情况。一致性是基于原子性、隔离性、持久性实现的,只有这三个特性达成了,才能满足一致性。
3. 隔离性
是指多个并行事务在执行时不会相互干扰,不能查看彼此未提交的数据。Innodb引擎中事务的隔离性是基于锁和MVCC机制实现的,默认隔离级别是可重复读。
4. 持久性
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。故障分为两种,一种是断电导致Buffer pool中还未落盘的数据丢失,Innodb引擎是基于Redolog日志解决的。还有一种故障是落盘失败,是通过双写缓冲区解决的。
二、并行事务会引发什么问题?
MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。那么在同时处理多个事务的时候,就可能出现脏读、不可重复读、幻读的问题。
本质就是一个事务执行过程中,由于另外一个并行事务修改了数据,导致第一个事务读取了错误数据或者数据前后不一致的问题
1. 脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
例:事务A修改了数据还未提交,事务B此时读取了新数据后,事务A回滚了,又把数据撤回原来的值了,事务B就是脏读了
2. 不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
例:事务A读取数据后,事务B修改了数据并提交了事务,事务A再次读取数据就不一样了,这就是在事务A中发生了「不可重复读」现象
3. 幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
例:事务B读取了查询了某个记录是5条,然后事务A插入了一条记录并提交了事务,事务B再进行相同的查询发现变成了6条,这就是幻读。
三、事务的四种隔离级别
当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象,SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低
1.读未提交
指一个事务还没提交时,它做的变更就能被其他事务看到;
2. 读已提交
指一个事务提交之后,它做的变更才能被其他事务看到;可防止脏读
3.可重复读
指一个事务提交之后,它做的变更才能被其他事务看到;一个事务执行过程中看到的一个数据值,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;可防止脏读和不可重复读
4.串行化
会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;可防止「脏读、不可重复读、幻读」
四、InnoDB 可重复读是如何实现的
InnoDB 引擎的默认隔离级别是可重复读,并且在一定程度上解决了幻读问题。访问数据方式分为快照读和当前读
快照读(普通 select 语句):普通 select 语句是快照读,不会对记录加锁。在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,这样就避免了可重复读和幻读问题。
当前读(select … for update 等语句):MySQL 里除了普通查询是快照读,其他都是当前读(select … for update、update、delete),这些语句执行前都会对记录加临键锁(根据实际在能避免幻读的情况下又可能会退化为记录锁或者间隙锁),并查询最新版本的数据,然后再做进一步的操作,事务结束再释放锁,因此在事务执行期间其他事务是无法修改锁范围内的数据的,避免了可重复读和幻读问题。
五、MVCC多版本并发控制机制(Read View + Undolog版本链)
每条记录都有两个隐藏字段
- trx_id:最新修改这条记录的事务id
- roll_pointer:undolog版本链指针,每当事务修改这条记录时会先将旧的记录插入undolog版本链,然后再修改记录并将roll_pointer指向版本链。
在快照读时创建的ReadView中记录了当前活跃事务的id集合m_ids以及其中的最小id,还有下一个待开启事务的id,在访问数据记录时若其undolog版本链中最新记录的trx_id小于min_trx_id则可直接访问,否则沿着undolog版本链找到小于max_trx_id且不在m_ids中的数据记录进行读取即可。即快照读能够读到已提交(RC:当前已提交。RR:第一次查询前已提交)事务数据 + 事务自身修改的数据。
- 在RC隔离级别中,事务中的每次查询都会创建一个ReadView,每次都能访问到最新已提交的数据,实现读已提交;
- 在RR隔离级别中,事务中的第一次查询时创建一个ReadView,后续查询复用这个ReadView,因此每次查询都一样,实现可重复读;
六、MySQL 可重复读隔离级别,完全解决幻读了吗?
在以下两种特殊场景中依然会幻读
1. 场景1:事务A快照读 事务B插入提交 事务A定向更新插入的记录然后再读
2. 场景2:事务A快照读 事务B插入提交 事务A当前读
要避免这两类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。
七、MySQL日志
1. undolog日志
undolog是用于回滚的日志,Innodb引擎的事务回滚机制就是基于undolog日志实现的,保证了事务的原子性
undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。
buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。
2. redolog日志
redolog日志是为了防止断电故障导致Buffer pool中缓存的还未落盘的数据丢失。
redo log 是物理日志,记录了某个数据页做了什么修改,每当执行一个事务就会产生这样的一条或者多条物理日志。在事务提交时将 redo log 持久化到磁盘。
innodb_flush_log_at_trx_commit 参数可取的值有:0、1、2,默认值为 1
- 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。由innoDB后台线程每秒从 redo log buffer刷盘一次。
- 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
- 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,Page Cache 是专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存。由操作系统每秒从Page Cache刷盘一次,相比参数0,MySQL进程崩溃不会导致redolog的丢失。
redo log 和 undo log 区别
这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于:
- redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;
- undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;
事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务
redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?
写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。
磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。
3. binlog日志
前面介绍的 undo log 和 redo log 这两个日志都是 Innodb 存储引擎生成的。binlog日志是Server层生成的
MySQL 在完成一条更新操作后(查询操作不会记录),Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 物理文件。
binlog日志和redolog日志的区别
- 实现层面:binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
redo log 是 Innodb 存储引擎实现的日志; - 文件格式:binlog日志的格式为STATEMENT、ROW或者二者的混合,redolog日志是直接记录表的某个数据页做了什么修改
- 写入方式:binlog 是追加写,写满一个文件,就创建一个新的文件继续写
redo log 是循环写,日志空间大小是固定,全部写满就从头开始 - 用途:binlog 用于备份恢复、主从复制;
redo log 用于掉电等故障恢复。