mysql可重复读和幻读的理解

本文深入探讨了MySQL中InnoDB引擎的幻读与不可重复读现象,详细解释了两者的定义与区别,以及MySQL如何通过不同的事务隔离级别解决这些问题。

mysql可重复读和幻读的理解

很多教程和书籍对mysql的可重复读和幻读的解释都比较含糊,本文结合原理和其他的考证,深入分析下。
这里讨论的引擎是常用的InnoDB。

可重复读和幻读的定义

先看下《高性能MySQL(第三版)》对隔离级别的描述。
在这里插入图片描述在这里插入图片描述
这里对幻读的描述为

所谓幻读,指的是当某个事物在读取某个范围内的记录时,另外一个事物又在该范围内插入了新的记录,当之前的事务再次读取该范围内的记录时,会产生幻行(Phantom Row)。

实际上这里的解释不太准确,先看下官网上对这些的定义。

phantom
A row that appears in the result set of a query, but not in the result set of an earlier query. For example, if a query is run twice within a transaction, and in the meantime, another transaction commits after inserting a new row or updating a row so that it matches the WHERE clause of the query.
This occurrence is known as a phantom read. It is harder to guard against than a non-repeatable read, because locking all the rows from the first query result set does not prevent the changes that cause the phantom to appear.
Among different isolation levels, phantom reads are prevented by the serializable read level, and allowed by the repeatable read, consistent read, and read uncommitted levels.

这里说明了一个事务里查询的结果集里的一行数据,但是这一数据并不在更早一点的查询结果集里,原因是另外一个事务里对数据进行了新增、修改操作。这种现象被称之为幻读,总之就是多读出一条数据。而且RR也不能完全解决幻读问题(手动加锁应该是可以的),只有更低的事务级别serializable read可以完全避免。

non-repeatable read
The situation when a query retrieves data, and a later query within the same transaction retrieves what should be the same data, but the queries return different results (changed by another transaction committing in the meantime).
This kind of operation goes against the ACID principle of database design. Within a transaction, data should be consistent, with predictable and stable relationships.
Among different isolation levels, non-repeatable reads are prevented by the serializable read and repeatable read levels, and allowed by the consistent read, and read uncommitted levels.

介绍了不可重复读的现象,和幻读非常相似。再看下Phantom Rows的解释。

Phantom Rows
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.

不可重复读的现象,和幻读非常相似,甚至可以觉得不可重复读就是幻读的一个特例。个人觉得美团上面的解释最到位,摘录如下,

不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

Mysql解决不可重复读用的MVCC多版本控制,或者叫做snapshot解决的,但RR也只解决了部分的幻读问题,官网定义如下

REPEATABLE READ
This is the default isolation level for InnoDB. Consistent reads within the same transaction read the snapshot established by the first read. This means that if you issue several plain (nonlocking) SELECT statements within the same transaction, these SELECT statements are consistent also with respect to each other. See Section 15.7.2.3, “Consistent Nonlocking Reads”.
For locking reads (SELECT with FOR UPDATE or FOR SHARE), UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition.
For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it.
For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range. For information about gap locks and next-key locks, see Section 15.7.1, “InnoDB Locking”.

这里还介绍了间隙锁,暂不展开了。
也就是说在RR下,select不会有phantom,因为读的是当前事开始时的快照;可对于select FOR UPDATE or FOR SHARE这种语句,如果前一次是select读快照(non-locking read),在RR下是可能会有幻像的,这因为select FOR UPDATE or FOR SHARE按照Mysql的定义是获取当前最新的快照。

session1session2
begin
select * from test_lock where t.id=6begin
insert into(id,value) valus(6,‘def’);
commit;
select * from test_lock where t.id=6 //不会幻读
select * from test_lock for update //会幻读

最后总结

总的来说,不可重复读和幻读都是违反了事务的隔离性。其实大可不必纠结字面的含义,重要的还是要搞明白内部的实现原理,并且能在工作当中避免。本质来说如果业务涉及到check-and-do的场景,必然会有数据一致性问题,因为check-and-do并非原子性的(当然redis lua脚本可以做成原子性)。如果要完全解决幻读问题,熟悉Java的并发编程的话,就知道最好的办法就是加锁,顺序执行,但是这样会很影响并发的性能,在实际使用过程中要具体分析,在并发和满足业务场景的同时选择最优解。

参考资料

  1. https://www.zhihu.com/question/47007926/answer/222348887
  2. https://www.zhihu.com/question/372905832
  3. https://tech.meituan.com/2014/08/20/innodb-lock.html
  4. http://mysql.taobao.org/monthly/2017/06/07/
  5. 高性能MySql(第三版)
  6. MySql技术内幕InnoDB存储引擎(第二版)
  7. https://www.zhihu.com/question/38507762/answer/968486962
<think>嗯,用户这次的问题是关于数据库事务中脏、不可重复区别。之前我已经解释过事务传播行为隔离级别的区别,现在用户想深入理解这些并发问题。用户引用了多篇关于数据库事务的文章,内容很专业。从引用[1][3][4][5]来看,用户主要关注三种现象的具体表现差异。特别是引用[5]强调与数据行数变化的关系,这点需要重点说明。用户可能正在学习数据库事务,或者遇到了实际的并发问题。ta特意要求区分这三个概念,说明之前可能混淆过它们。需要特别注意用生活化的类比帮助理解。脏最危险,就像偷看别人未提交的草稿。引用[1][3]都强调这是取未提交数据。不可重复更像是同一事务内数据"变脸",引用[3]用"两次查询不一致"描述得很准确。最特别,引用[5]指出关键在于"行数变化",就像魔术师凭空变出鸽子。 表格对比会很有用,但要注意引用[4]的隔离级别对照表不能直接复制,需要转化。隔离级别部分要简明,引用[2]提到"SERIALIZABLE_READ能解决所有问题"这点值得强调。最后用"文件草稿/合同金额/员工名单"的类比收尾,既呼应引用内容,又能让用户记住本质区别。相关问题的设计要延续事务主题,特别是隔离级别选择这种实用问题。</think>以下是数据库事务中脏、不可重复的详细区别及对比: --- ### 一、核心概念对比 | **现象** | **本质问题** | **触发场景** | **示例** | |----------------|----------------------------|------------------------------------------|------------------------------------------| | **脏** | 取**未提交**的数据 | 事务A取事务B**修改中但未提交**的数据 | 事务B修改金额未提交,事务A到错误金额[^1][^3] | | **不可重复** | 同数据**多次取结果不同** | 事务A两次取之间,事务B**修改并提交**了数据 | 事务A首次金额100,事务B改为200提交后,事务A第二次金额变为200[^1][^3] | | **** | 同查询**返回行数不同** | 事务A两次查询之间,事务B**新增/删除**了数据 | 事务A首次查10条记录,事务B新增1条提交后,事务A第二次查得11条记录[^3][^5] | > **关键区别**: > 脏关注**数据是否提交**,不可重复关注**数据值是否变化**,关注**数据行数是否变化**[^2][^5]。 --- ### 二、隔离级别的解决能力 | **隔离级别** | 脏 | 不可重复 | | |----------------------|------|------------|------| | **READ UNCOMMITTED** | ❌ | ❌ | ❌ | | **READ COMMITTED** | ✅ | ❌ | ❌ | | **REPEATABLE READ** | ✅ | ✅ | ❌* | | **SERIALIZABLE** | ✅ | ✅ | ✅ | > *注:MySQL的`REPEATABLE READ`通过间隙锁可避免,但标准SQL中仍可能存在[^3][^4]。 --- ### 三、技术原理与示例 #### 1. **脏** ```sql -- 事务A BEGIN; UPDATE accounts SET balance = 200 WHERE id = 1; -- 未提交 -- 事务B(READ UNCOMMITTED) BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 到200(脏数据) ``` > 事务B取到事务A的**中间状态**,若事务A回滚,数据将不一致[^1][^3]。 #### 2. **不可重复** ```sql -- 事务A BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 返回100 -- 事务B提交更新 UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT; -- 事务A再次查询 SELECT balance FROM accounts WHERE id = 1; -- 返回200(值变化) ``` > 同一事务内**相同查询**结果不同,因其他事务修改了数据[^3][^5]。 #### 3. **** ```sql -- 事务A(查询年龄<30的用户) BEGIN; SELECT * FROM users WHERE age < 30; -- 返回10条 -- 事务B新增用户并提交 INSERT INTO users (name, age) VALUES ('Bob', 25); COMMIT; -- 事务A再次查询 SELECT * FROM users WHERE age < 30; -- 返回11条(行数变化) ``` > 重点在于**数据行数变化**(新增/删除),而非单行数据修改[^5]。 --- ### 四、解决方式 1. **提升隔离级别** - `READ COMMITTED` 解决脏 - `REPEATABLE READ` 解决不可重复 - `SERIALIZABLE` 彻底解决所有问题(但性能最低)[^2][^4] 2. **MySQL特殊优化** 通过**间隙锁**(Gap Lock)在`REPEATABLE READ`级别阻止,例如锁定`age < 30`的范围阻止新增[^4]。 --- ### 五、生活化类比 - **脏** 👉 偷看别人未保存的文档草稿 - **不可重复** 👉 两次查看合同时发现金额被修改 - **** 👉 两次统计员工名单时发现人数变化 > 三者本质都是**事务间可见性控制不足**导致的数据一致性问题[^1][^3]。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值