上一篇MySQL的文章讲了事务的特性和原理(
最详细的MySQL事务特性及原理讲解!(一)
) 作为基础,小伙伴们自行查看。
关于MySQL隔离级别的讲解,网上已经很多了。和大家一样,我也看过很多很多,但是发现看完之后还是晕晕乎乎,学习走了很多弯路。事务特性的原理是什么?什么隔离级别可以解决什么问题?具体什么原理?这些似乎都没有讲清楚,无非是抄来抄取,甚至都抄错了,我查了近10篇文章每篇都有各种各样的错误!比如下面这个举例:
作者说RR解决了幻读的问题,并且还不止一个人这样说。还有说未提交读级别下不加锁。
那么事实是这样吗?(阅读提醒:点收藏前先点「在看」,记忆效果翻倍!)
与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。隔离性追求的是并发情形下事务之间互不干扰。SQL标准定义了4类隔离级别:Read Uncommitted(读取未提交内容)、Read Committed(读取提交内容)、Repeatable Read(可重读)、Serializable(可串行化)。
对于隔离性的探讨,主要可以分为两个方面:
1.锁机制保证隔离性
2.MVCC保证隔离性。
严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。所以,共有四种隔离级别来对应不同的业务性能需求。然而正是由于不同的隔离级别,便产生了不同的问题。
我们不妨带着几个问题去继续阅读:
1.可重复读和幻读的区别是什么?
2.每个隔离级别解决了怎样的问题?怎么解决的?
顾名思义,该隔离级别下可以读取到另一个事务未提交的数据。严谨一点,我们给出官方定义:
The isolation level that provides the least amount of protection between transactions. Queries employ a locking strategy that allows them to proceed in situations where they would normally wait for another transaction. However, this extra performance comes at the cost of less reliable results, including data that has been changed by other transactions and not committed yet (known as dirty read). Use this isolation level with great caution, and be aware that the results might not be consistent or reproducible, depending on what other transactions are doing at the same time. Typically, transactions with this isolation level only do queries, not insert, update, or delete operations.
如文档所说,它提供了最低的隔离级别,查询时采用无需等待其他事务的锁策略。所以,在这个隔离级别下,基本上什么问题都会产生了。于是文档建议,通常具有此隔离级别的事务仅执行查询,而不执行插入,更新或删除操作。
但是,这并不代表他一无是处。其实RU隔离级别解决了一个问题——当A事务修改数据n,B事务也修改数据n,后执行的语句会把另一个事务的结果覆盖。如文章开头提到的反例,如果读未提交隔离级别下不加锁,这是不可能实现的。
为了解决丢失修改的写覆盖问题,未提交读规定:
1.事务A对当前被读取的数据不加锁,事务B读取也不加锁。
2.事务A开始更新一行数据时,必须先对其加排他锁,直到事务结束才释放。
如何证明呢?
事务A事务B
发生脏读 和 等待
图中左边为事务A,右边为事务B,可见事务A更新时加排他锁,事务未提交,所以事务B更新数据发生阻塞。但是B却可以读取到A修改后的数据,说明B在读取时并未加任何锁。
如图,证明两个事务在更新时发生了排他锁竞争。
那么未提交读会产生什么问题?刚才的例子依旧说明了,发生了脏读。如图:
右边事务读取到了左边事务更改后的数据 a=11,但左边事务还未提交。
我们依旧给出脏读的定义:
An operation that retrieves unreliable data, data that was updated by another transaction but not yet committed. It is only possible with the isolation level known as read uncommitted.
检索不可靠数据的操作,该数据是由另一个事务更新但尚未提交的数据。只有在隔离级别称为未提交读的情况下才有可能。
划重点!读出来的数据不可靠,是可以被其他事务修改后未提交的数据。脏读只可能出现在未提交读情况下。
2、读已提交(READ_COMMITTED)
读已提交和读未提交最大的区别如名字所说,可以读到另一个事务已提交读数据。官方定义如下:
An isolation level that uses a locking strategy that relaxes some of the protection between transactions, in the interest of performance. Transactions cannot see uncommitted data from other transactions, but they can see data that is committed by another transaction after the current transaction started. Thus, a transaction never sees any bad data, but the data that it does see may depend to some extent on the timing of other transactions.
事务无法看到来自其他事务的未提交的数据,但是它们可以看到在当前事务开始后另一个事务提交的数据。因此,一个事务永远不会看到任何不良数据,但是它所看到的数据可能在某种程度上取决于其他事务的时间安排。
实验前,更改数据库隔离级别为RC。
mysql> set session transaction isolation level read committed;Query OK, 0 rows affected (0.01 sec)
mysql> SELECT @@tx_isolation;+----------------+| @@tx_isolation |+----------------+| READ-COMMITTED |+----------------+1 row in set, 1 warning (0.00 sec)
事务A
事务B
脏读被解决
不可重复读出现
如上表AB事务可知,在B事务更改数据 a=11 且事务未提交时,A事务读取到的还是旧数据 a=9,可见RC隔离级别很好的解决了脏读的问题。
但是紧接着在B事务提交后,A事务再次读取数据,a=11。在同一事务中两次查询出现了结果不一样的情况,这就是不可重复读。官方定义:
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).
查询检索数据,而同一事务中的前后查询检索应为相同的数据,但实际查询返回不同的结果(数据同时被其他提交的事务更改)。
很关键的一点,不可重复读是同一事务前后同样的查询返回了不同的数据!一定有人有这样的疑惑,那脏读不也是前后不一致吗,怎么区分这两者?很多文章只甩出了几个脏读、不可重复读、幻读的例子,却并没有解释清楚到底什么是什么。
现在我可以肯定的说,脏读的条件不可重复读一定满足,但是如果是不可重复读,脏读却不一定满足,脏读属于不可重复读。可以这么理解,RC级别下,相当于只解决了一部分不可重复读的问题。
他是如何解决脏读的呢,锁机制:
1.事务A对当前被读取的数据加共享锁,一旦读完该行,立即释放该共享锁(注意是读完立即释放)
2.事务A在更新某行数据时,必须对其加上排他锁,直到事务结束才释放(注意是事务结束才释放)
听起来,可重复读解决了不可重复读的问题。事实是这样。官方定义:
The default isolation level for InnoDB. It prevents any rows that are queried from being changed by other transactions, thus blocking non-repeatable reads but not phantom reads. It uses a moderately strict locking strategy so that all queries within a transaction see data from the same snapshot, that is, the data as it was at the time the transaction started.
InnoDB的默认隔离级别。它可防止查询的任何行被其他事务更改,从而阻止不可重复的读取,但不会阻止幻读。它使用中等严格的锁定策略,以便事务中的所有查询都可以查看来自同一快照的数据,即事务开始时的数据。
看重点!RR隔离级别是InnoDB默认隔离级别,他可以解决不可重复读,但不能解决幻读!这就是为什么我说很多文章错了,官方已经给出了明确定义。
mysql> set session transaction isolation level repeatable read;Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@tx_isolation;+-----------------+| @@tx_isolation |+-----------------+| REPEATABLE-READ |+-----------------+1 row in set, 1 warning (0.00 sec)
如表格所示,不可重复读是被解决了,但很明显还有一个问题,没有解决幻读。
事务A事务B
已解决不可重复读
此时我们再来一次,回到B事务提交前但状态,执行一条insert语句。
回到更新前状态。
在B事务执行 insert 后再次查询
B事务提交后出现了奇怪的情况。A事务查询到的数据前后一致,但是在UPDATE条件下符合的数据却变成两条。
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.
在查询的结果集中出现的行,但在较早的查询的结果集中没有出现。例如,如果一个查询在一个事务中运行两次,同时,另一个事务将在插入新行或更新一行以使其与查询的WHERE子句匹配之后提交。
请看上方幻读定义。重点是,同样的条件同一事务前后结果中出现了和之前不一样的行。与不可重复读的区别是什么:幻读的条件也符合不可重复读,但是不可重复读不一定符合幻读。前后检索出不一样的行当然查询前后查询结果不同,但是查询结果不同不一定有新的行。
为什么很多文章错误的认为RR可以解决幻读呢?我也不知道,可能是被快照读迷惑了?MVCC能解决快照读下的幻读问题,但是没法保证当前读下的幻读问题(这个我们下一篇细说,内容太多了)。但是幻读确实在一定条件下解决,这个条件就是 next-key locks。
By default, InnoDB operates in REPEATABLE READ transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows
默认情况下,InnoDB以REPEATABLE READ事务隔离级别运行。在这种情况下,InnoDB使用next-key locks 锁定进行搜索和索引扫描,从而防止幻读
使用效果如下:
select count(*) from test where id>=9 lock in share mode;
执行 insert 发生阻塞,如此便使用了 next-key locks 防止幻读。
锁原理:
1.事务A在读取某数据时,必须先对其加共享锁,直到事务结束才释放(注意是事务结束才释放)
2.事务A在更新某数据时,必须先对其加排他锁,直到事务结束才释放(注意是事务结束才释放)
The isolation level that uses the most conservative locking strategy, to prevent any other transactions from inserting or changing data that was read by this transaction, until it is finished. This way, the same query can be run over and over within a transaction, and be certain to retrieve the same set of results each time. Any attempt to change data that was committed by another transaction since the start of the current transaction, cause the current transaction to wait.
最严格的隔离级别,防止其他任何事务插入或更改此事务读取的数据,直到完成为止。这样,可以在一个事务中一遍又一遍地运行相同的查询,并确保每次都检索相同的结果集。自当前事务开始以来,任何尝试更改另一事务提交的数据的尝试都会导致当前事务等待。
这个我就简单演示下,因为不会出现任何问题,但是效率也最低。
如图,执行串行读级别下,insert 发生阻塞,隔离级别最严格。
锁机制:
1.事务在读取数据时,必须先对其加表级共享锁(注意这里是表级) ,直到事务结束才释放。
2.事务在更新数据时,必须先对其加表级排他锁(注意这里是表级) ,直到事务结束才释放。
到这里,我们之前的问题也有了答案。
1.不同隔离级别解决怎样的问题:
隔离级别脏读不可重复读幻读读未提交发生发生发生读已提交✅发生发生可重复读✅✅可以在 next-key lock下解决
串行读✅✅✅
2.脏读、不可重复读、幻读关系