事务是由 MySQL 的引擎实现的,我们常见的 InnoDB引擎时支持事务的。
不过并不是所有的引擎都支持事务,比如 MySQL原生的 MyISAM 引擎就不支持事务,正因为如此,大多数MySQL的引擎都是用 InnoDB。
事务的四个特性:
InnoDB 引擎通过什么来保证事务的这四个特性?
MySQL服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。
那么在同时处理多个事务的时候,就可能出现脏读、不可重复读、幻读的问题。
脏读
如果一个事务 [读到] 了另一个 [未提交事务修改过的数据] ,就意味着发生了 [脏读] 现象。
eg:
假设有 A 和 B这个事务同时在处理,事务 A 先开始从数据库中读取余额数据,然后再执行更新操作,如果此时事务 A 还没有提交事务,而此时正好事务 B 也从数据库中读取余额数据,那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事务。
因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,如果在上面这种情况事务A发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象被称为脏读。
不可重复读
一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,这就意味着发生了 [不可重复度] 现象。
eg:
假设有 A 和 B 两个事务同时在处理,事务 A 先开始从数据库中读取余额数据,然后继续执行代码处理逻辑,在这过程中如果事务 B 更新了这条数据,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读取的数据是不一致的,这种现象就被称为不可重复读。
幻读
在一个事务内多次查询某个符合条件的 [记录数量] , 如果出现前后两次查询到的记录数量不一样的情况,这就意味着发生了 幻读 现象。
eg:
假设有 A 和 B 两个事务同时在处理,事务 A 先开始从数据库查询账户余额大于 100 万的记录,发现共有 5 条 ,然后事务 B也按照相同的搜索条件也是查询出了 5 条记录。
接下来,事务 A 插入了一条余额超过 100 万的记录,并提交了事务,此时数据库超过 100 万余额的账号个数就变为 6 。
然后事务 B 再次查询账户余额大于 100 万的记录,此时查询到的记录数量有 6 条,发现和前一次读到的记录数量不一样了。就感觉发生了幻觉一样,这种现象就被称为 幻读。
前面提到,当多个事务并发执行时可能会遇到 [脏读、不可重复读、幻读] 的现象,这些现象会对事务的一致性产生不同程度的影响。
三个现象的严重性排序如下:
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
按照隔离水平高低排序:
针对不同的隔离级别,并发事务可能发生的现象也会不同。
也就是说:
所以,要解决脏读现象,就要升级到 [读提交] 以上的隔离级别;要解决不可重复读现象,就要升级到 [可重复读] 的隔离级别,要解决幻读现象不建议将隔离级别升级到 [串行化] 。
不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样,有的数据库只实现了其中的几种隔离级别,我们讨论的MySQL 虽然支持 4 种隔离级别,但是与 SQL 标准中规定的各级隔离级别允许发生的现象有些出入。
MySQL 在 [可重复读] 的隔离级别下,可以很大程度上避免幻读现象的发生,所以MySQL 并不会使用 [串行化] 隔离级别来避免幻读现象的发生,因为使用 [串行化] 隔离级别会影响性能。
MySQL InnoDB 引擎的默认级别虽然是 [可重复读] ,但是它很大程度上避免幻读现象,解决的方案有两种:
举个例子说明这四种隔离级别,有一张用户余额表,里面有一条账户余额为 100万的记录。然后有两个并发的事务,事务 A 只负责查询余额,事务 B 则会将余额改成 220万,下面是按照时间顺序执行两个事务的行为:
在不同的隔离级别下,事务 A 执行过程中查询到的余额可能会不同:
四种隔离级别如何实现?
PS:执行 [开始事务] 命令,并不意味着启动了事务。在 MySQL有两种开启事务的命令,分别是:
这两种开启事务的命令,事务的启动时机是不同的:
什么是 Read View ?
Read View 有四个重要的字段:
知道了 Read View 的字段,我们还需要了解聚簇索引记录中的两个隐藏列。
假设在账户余额表插入一条余额为 100 万的记录,然后把这两个隐藏列也画出来,该记录的整个示意图如下:
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录都包含下面两个隐藏列:
在创建 Read View 后,我们可以将记录中的 trx_id 划分为三种情况:
一个事务区访问记录的时候,除了自己的更新记录总是可见之外,还有几种情况:
min_trx_id
和 max_trx_id
之间,需要判断 trx_id 是否在 m_ids 列表中:
m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见这种通过 [版本链] 来控制并发事务访问同一个记录的行为就叫MVCC(多版本并发控制)。
可重复读隔离级别是启动事务时生成一个 Read View ,然后整个事务期间都在用这个 Read View。
假设事务 A (事务id 为 51)启动后,紧接着事务B(事务id为52)也启动了,那这两个事务创建的Read View 如下:
事务 A 和 事务B 的 Read View 具体内容如下:
接着,在可重复读隔离级别下,事务 A 和事务 B 按顺序执行了以下操作:
具体操作:
事务 B 第一次读取账户余额记录,在找到记录后,它会先看这条记录的 trx_id,此时发现 trx_id 为 50,比事务 B 的 Read view 中的 min_trx_id的值(51)小,这意味着修改这条记录的事务早已在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见,也就是事务 B 可以获取到这条记录。
接着,事务 A 通过 update 语句将这条记录修改了(还未提交事务),将余额改成了 200 万,这时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成 版本链,如下图:
可以在上图看到,由于事务 A 修改了记录,以前的记录juice变成了 旧版本的记录了,于是新记录的旧版本的记录通过链表的方式串起来
然后事务 B 第二次去读取该记录,发现这条记录的trx_id 为 51,在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间,则需要判断 trx_id 值是否在 m_ids 范围内,判断结果是在的,那么说明这条记录是被未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链表往下找旧版本的记录,直到找到 trx_id [小于] 事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 50 的记录,也就是余额为 100万的这条记录。
最后,当事务 A 提交事务后,由于隔离级别的 [可重复读] , 所以事务 B 再次被读取时,还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。所以,即使事务 A 将余额改成了 200 万并提交了事务,事务 B 第三次读取记录时,读取到的记录都是 100 万这条记录。
通过这种方式实现了,[可重复读] 隔离级别下在事务期间读到的记录都是事务启动前的记录。
读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View 。
这意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能在这期间另外一个事务修改了该记录,并提交了事务。
假设事务 A (事务id 为 51)启动后,紧接着 事务 B (事务id为 52)也启动了,接着按顺序执行了以下操作:
具体如何做到的呢?前两次事务B读取数据时创建的 Read View 如下图:
为什么事务 B 第二次读取数据时,读取不到 事务 A (还未提交事务)修改的数据呢?
事务 B 在找到这条记录时,会看记录的 trx_id 是 51,在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间,接下来需要判断trx_id值是否在 m_ids 范围内,判断结果在,那么说明这条记录是被未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链表往下找到旧版本的记录,直到找到 trx_id [小于] 事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 50 的记录,也就是余额为 100 的记录。
为什么事务 A提交后,事务 B 就可以读取到 事务 A 修改的数据?
在事务 A 提交后,由于隔离级别是 [读提取] ,所以事务 B 在每次读取数据的时候,会重新创建 Read View ,此时事务 B 第三次读取数据时创建的 Read View 如下;
事务 B 在找到这条记录时,会发现这条记录的 trx_id 是 51,比事务 B 的 Read View 中的 min_trx_id 还小,说明修改这条记录的事务早就在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。
正是因为在读提交隔离级别下,事务每次读数据时都重新创建 Read View ,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
事务是在 MySQL 引擎实现的,我们常见的 InnoDB 引擎时支持事务的,事务的四大特性是原子性、一致性、隔离性、持久性。
当多个事务并发执行的时候,会引发脏读,不可重复读,幻读这些问题,那为了避免这些问题,SQL提出了四种隔离级别,分别是 读未提交、读提交、可重复读、串行化、从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,InnoDB 引擎的默认隔离级别是可重复读。
要解决脏读现象,就需要将隔离级别升级到读提交以上的隔离级别,要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别。
而对于幻读现象,不建议将隔离级别升级为串行化,因为这会导致数据库并发时的性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是 [可重复读] ,但是它很大程度上避免幻读现象,解决的方案有两种:
对于 [读提交] 和 [可重复读] 隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于 创建 Read View 的时机不同:
这两个隔离级别实现是通过 [事务的 Read View 里的字段] 和 [记录中的两个隐藏列] 的对比,来控制并发事务访问同一个记录时的行为,这就叫做 MVCC (多版本并发控制)
在可重复读的隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁。而 select ... for update 语句就不是快照读了,而是当前读,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁