开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
事务特性
InnoDB支持事务,这也是InnoDB和MyISAM的一个区别之一,MyISAM不支持事务。
事务的特性,相信大多数人都能记住,ACID-原子性、一致性、隔离性、持久性,下面只做简单的描述:
- 原子性:事物中的操作,要么全部完成,要么全部不执行,不会出现执行一半就停止的情况。在事务执行期间发生错误,系统也会根据undo日志回滚到事务开始前的状态。
- 一致性:事务操作前后,数据满足完整性约束,数据库保持一致性的状态。举个银行账户转账的例子,A向B转帐600元,事务满足一致性,就不会出现A账号减600元,而B账户没有收到的情况。一致性的保证比较复杂,需要在原子性、隔离性和持久性都实现的情况下,也就是AID保证C。
- 隔离性:数据库允许多个事务并发执行,但事务之间互不干扰,也就是隔离性,这样就不会产生多个事务并发执行导致数据不一致的情况。
- 持久性:即将数据写入磁盘永久保存。MySql中引入redo log实现持久性,只将日志写入磁盘,将数据缓存在内存中,一定时间后再进行更新。
并发事务引发的问题
MySql允许多个事务并发执行,在同时处理事务的时候,就不可避免的出现一些并发带来的问题,在MySql中经常出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)三种问题。
接下来我们通过举例看这三种问题发生的场景,从而能够更好理解后面的事务隔离级别。
脏读
脏读,即一个事务读取到了另一个事务修改但未提交的数据,后续如果另一个事务出现错误回滚,当前数据就读取了错误数据。
假设有事务A、B。事务A开启事务,准备从数据库中读取余额,而此时事务B也开启了事务,并且在事务A之前修改余额为200万,事务A读取到了200万,但事务B因为某些原因发生回滚,此时数据库中余额仍为100万,但是事务A读取到了200万,这就是脏读,如下图所示:
不可重复读
不可重复读,即事务内多次读取数据时,前后两次读取到的数据不一致,这就是发生了不可重复读。
事务A第一次读取数据,读取到了100万,此时事务B在事务A第二次读取数据之前修改数据并提交,事务A再次读数据,读取到了200万,前后两次读到的数据不一致,这就发生了不可重复读。
幻读
幻读,即一个事务内多次读取某个范围的数据,如果出现前后两次读取到的记录数不同,就意味着发生了幻读。幻读与不可重复读的区别在于,一个是单个记录内容发生变化,一个是记录数量发生变化。
事务A第一次查询余额大于200万的数据返回5条记录,此时事务B向数据库中插入一条余额大于200万的数据并提交事务,事务A第二次查询时就会返回六条数据。前后两次读取,记录数量不一样,就像人眼花多读一条数据,这就是幻读。
事务隔离级别
针对事务并发产生的脏读、不可重复读和幻读三种问题,数据库设计了四种隔离级别来解决这些问题:
- 读未提交(read uncommitted):一个事务未提交时,其他事务可以看到它做的变更。
- 读已提交(read committed):只有事务提交后,它做的变更才能被其他事务看到。
- 可重复读(repeatable read):事务在执行过程中读取到数据一直和事务启动时能够看到的数据内容保持一致。是InnoDB默认的隔离级别。
- 串行化(serializable):会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
| 隔离级别 | 并发事务引发的问题 |
|---|---|
| 读未提交 | 脏读 不可重复读 幻读 |
| 读已提交 | 不可重复读 幻读 |
| 可重复读 | 幻读 |
串行化隔离级别最高,不会出现脏读、不可重复读和幻读问题,但相应并发度降低,效率也就低了。
Mysql中MVCC多版本控制实现
首先需要了解Read View,它就相当于一个事务中监控其他事务的监控器,可以保证事务读取的数据是自己可见的。
Read View有四个属性:
| 属性 | 描述 |
|---|---|
| creator_trx_id | 指创建该 Read View 的事务的事务 id。 |
| m_ids | 指的是在创建 Read View 时,当前数据库中启动了但还没提交的事务 id 列表 |
| min_trx_id | 创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务。 |
| creator_trx_id | 创建 Read View 时当前数据库中应该给下一个事务的 id 值。 |
在保存每条记录时,还有三个隐藏列row_id、transaction_id、roll_pointer。
- trx_id:当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
事务创建Read View后,记录中的trx_id就可以划分为三种情况,对应着事务对记录的可见性。
- 如果记录的 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 在
最后事务就可以根据Read View 解决不可重复读和幻读问题。