共计 3828 个字符,预计需要花费 10 分钟才能阅读完成。
这篇文章主要介绍“MySQL 如何解决幻读问题”,在日常操作中,相信很多人在 MySQL 如何解决幻读问题问题上存在疑惑,丸趣 TV 小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”MySQL 如何解决幻读问题”的疑惑有所帮助!接下来,请跟着丸趣 TV 小编一起来学习吧!
事务特性(ACID):原子性(Atomicity)、隔离性(Isolation)、一致性(Consistency)和持久性
隔离级别:读取未提交(READ UNCOMMITTED),读取已提交(READ COMMITTED),可重复读(REPEATABLE READ),可串行化(SERIALIZABLE)
而每一种隔离级别导致的问题有:
READ UNCOMMITTED 隔离级别下,可能发生脏读、不可重复读和幻读问题
READ COMMITTED 隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题
REPEATABLE READ 隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题
SERIALIZABLE 隔离级别下,各种问题都不可以发生
对于 MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读),从上面的 SQL 标准的四种隔离级别定义可知,REPEATABLE-READ(可重复读) 是不可以防止幻读的,但是我们都知道,MySQL InnoDB 存储引擎是解决了幻读问题发生的,那他又是如何解决的呢?
1. 行格式
在进入主题之前,我们先大致了解一下什么是行格式,这样有助于我们理解下面的 MVCC,行格式是表中的行记录在磁盘的存放方式,Innodb 存储引擎总共有 4 种不同类型的行格式:compact、redundant、dynamic、compress;虽然很很多行格式,但是在原理上,大体都相同,如下,为 compact 行格式:从图中可以看出来,一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分,记录的额外信息分别是变长字段长度列表、NULL 值列表和记录头信息,而记录的真实数据除了我们自己定义的列之外,MySQL 会为每个记录添加一些默认列,这些默认列又称为隐藏列,具体列如下:
列名长度描述 row_id6 个字节行 ID,唯一标识一条记录 transaction_id6 个字节事务 IDroll_pointer7 个字节回滚指针
隐藏列的值不用我们操心,InnoDB 存储引擎会自己帮我们生成的,画得再详细一点,compact 行格式如下:
transaction_id:事物 id,当事物对行记录进行修改时,都会将本事物的事物 id 赋值到该列
roll_pointer:每次在对行记录进行改动的时候,都会把旧版本的数据写入 undolog 日志,然后将 roll_pointer 指向该 undolog,所以该列相当于一个指针,通过该列,可以找到修改之前的信息
2. MVCC 详解 2.1 版本链
假设有一条记录如下:插入该记录的事务 id 为 80,roll_pointer 指针为 NULL(为了便于理解,读者可理解为指向为 NULL,实际上 roll_pointer 第一个比特位就标记着它指向的 undo 日志的类型,如果该比特位的值为 1 时,就代表着它指向的 undo 日志类型为 insert undo)
假设之后两个事务 id 分别为 100、200 的事务对这条记录进行 UPDATE 操作:
-- 事务 id=100
update person set grade =20 where id =1;
update person set grade =40 where id =1;
-- 事务 id=200
update person set grade =70 where id =1;
每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id
2.2 ReadView
对于数据库的四种隔离级别:1)read uncommitted;2) read committed;3) REPEATABLE READ;4)SERIALIZABLE;来说,READ UNCOMMITTED,每次读取版本链的最新数据即可;SERIALIZABLE,主要是通过加锁控制;而 read committed 和 REPEATABLE READ 都是读取已经提交了的事物,所以对于这两个隔离级别,核心问题是版本链中,哪些事物是对当前事物可见;为了解决这个问题,MySQL 提出了 read view 概念,其包含四个核心概念:
m_ids:生成 read view 时候,活跃的事物 id 集合
min_trx_id:m_ids 的最小值,既生成 read view 的时候,活跃事物的最小值
max_trx_id:表示生成 read view 的时候,系统应该分配下一个事物 id 值
creator_trx_id:创建 read view 的事物 id,即当前事物 id。
有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
当记录的事物 id 等于 creator_trx_id 的时候,说明当前事物正在访问自己修改的记录,所以该版本可见
如果被访问的版本事物 id 小于 min_trx_id 的时候,则说明,在创建 read view 的时候,该事物已经提交,该版本,对当前事物可读
如果被访问的版本事物 id 大于或等于 max_trx_id,则说明创建该 read view 的时候,该说明生成该版本记录的事物 id 在生成 Read view 之后才开启,所以该版本不能被当前事物可读
如果被访问的版本事物 transaction_id 在 m_ids 集合中,说明生成 Read view 的时候,该事物还是活跃的,还没有被提交,则该版本不可以被访问;如果不在,则说明创建 ReadView 时生成该版本的事务已经被提交,可以被访问
注:读事物的事物 id 为 0
在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同:
READ COMMITTED —— 每次读取数据前都生成一个 ReadView
REPEATABLE READ —— 在第一次读取数据时生成一个 ReadView
下面我们通过详细例子来说明,两者有何不同:
时间编号
trx 100trx 200①BEGIN;
②
BEGIN;BEGIN;③
update person set grade =20 where id =1;
④
update person set grade =40 where id =1;
⑤SELECT * FROM person WHERE id = 1;
⑥
COMMIT;
⑦
update person set grade =70 where id =1;⑧SELECT * FROM person WHERE id = 1;
⑨
COMMIT;?COMMIT;
在时间④中,因事务 trx 100 执行了事务的提交,id= 1 行记录的版本链如下:
在时间⑥中,因事务 trx 200 执行了事务的提交,id= 1 行记录的版本链如下:
在时间⑤,事务 trx 100 执行 select 语句时会先生成一个 ReadView,ReadView 的 m_ids 列表的内容就是 [100, 200],min_trx_id 为 100,max_trx_id 为 201,creator_trx_id 为 0,此时,从版本链中选可见的记录,版本链从上到下遍历:因为 grade=40,trx_id 值为 100,在 m_ids 里,所以该记录不可见,同理,grade=20 的也不见。继续往下遍历,grade=20,trx_id 值为 80, 小于小于 ReadView 中的 min_trx_id 值 100,所以这个版本符合要求,返回给用户的是等级为 10 的记录。
在时间⑧中,如果事务的隔离级别是 READ COMMITTED,会单独又生成一个 ReadView,该 ReadView 的 m_ids 列表的内容就是 [200],min_trx_id 为 200,max_trx_id 为 201,creator_trx_id 为 0,此时,从版本链中选可见的记录,版本链从上到下遍历:因为 grade=70,trx_id 值为 200,在 m_ids 里,所以该记录不可见,继续往下遍历,grade=40,trx_id 值为 100,小于 ReadView 中的 min_trx_id 值 200,所以这个版本是符合要求的,返回给用户的是是等级为 40 的记录。
在时间⑧中,如果事务的隔离级别是 REPEATABLE READ,在时间⑧中,不会单独生成一个 ReadView,而是沿用时间 5 的 ReadView,所以返回给用户的等级是 10。前后两次 select 得到的是一样的,这就是可重复读的含义。
到此,关于“MySQL 如何解决幻读问题”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注丸趣 TV 网站,丸趣 TV 小编会继续努力为大家带来更多实用的文章!