这篇文章主要介绍“MySQL如何解决幻读问题”,在日常操作中,相信很多人在MySQL如何解决幻读问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”MySQL如何解决幻读问题”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
创新互联公司坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站设计、网站制作、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的德兴网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!
事务特性(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存储引擎是解决了幻读问题发生的,那他又是如何解决的呢?
在进入主题之前,我们先大致了解一下什么是行格式,这样有助于我们理解下面的MVCC,行格式是表中的行记录在磁盘的存放方式,Innodb
存储引擎总共有4种不同类型的行格式:compact
、redundant
、dynamic
、compress
;虽然很很多行格式,但是在原理上,大体都相同,如下,为compact
行格式: 从图中可以看出来,一条完整的记录其实可以被分为记录的额外信息
和记录的真实数据
两大部分,记录的额外信息
分别是变长字段长度列表
、NULL值列表
和记录头信息
,而记录的真实数据
除了我们自己定义的列之外,MySQL会为每个记录添加一些默认列,这些默认列又称为隐藏列
,具体列如下:
列名 | 长度 | 描述 |
---|---|---|
row_id | 6个字节 | 行ID,唯一标识一条记录 |
transaction_id | 6个字节 | 事务ID |
roll_pointer | 7个字节 | 回滚指针 |
隐藏列的值不用我们操心,InnoDB
存储引擎会自己帮我们生成的,画得再详细一点,compact
行格式如下:
transaction_id :事物id,当事物对行记录进行修改时,都会将本事物的事物id赋值到该列
roll_pointer:每次在对行记录进行改动的时候,都会把旧版本的数据写入undolog日志,然后将roll_pointer
指向该undolog
,所以该列相当于一个指针,通过该列,可以找到修改之前的信息
假设有一条记录如下:插入该记录的事务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
对于数据库的四种隔离级别: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 100 | trx 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如何解决幻读问题”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注创新互联网站,小编会继续努力为大家带来更多实用的文章!