深入理解MySQL中事务隔离级别的实现原理

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"说到数据库事务,大家脑子里一定很容易蹦出一堆事务的相关知识,如事务的ACID特性,隔离级别,解决的问题(脏读,不可重复读,幻读)等等,但是可能很少有人真正的清楚事务的这些特性又是怎么实现的,为什么要有四个隔离级别。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天我们就先来聊聊MySQL中事务的隔离性的实现原理,后续还会继续出文章分析其他特性的实现原理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然MySQL博大精深,文章疏漏之处在所难免,欢迎批评指正。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"说明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MySQL的事务实现逻辑是位于引擎层的,并且不是所有的引擎都支持事务的,下面的说明都是以InnoDB引擎为基准。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"定义"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隔离性(isolation)指的是不同事务先后提交并执行后,最终呈现出来的效果是串行的,也就是说,对于事务来说,它在执行过程中,感知到的数据变化应该只有自己操作引起的,不存在其他事务引发的数据变化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"隔离性解决的是并发事务出现的问题"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"标准SQL隔离级别"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隔离性最简单的实现方式就是各个事务都串行执行了,如果前面的事务还没有执行完毕,后面的事务就都等待。但是这样的实现方式很明显并发效率不高,并不适合在实际环境中使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了解决上述问题,实现不同程度的并发控制,SQL的标准制定者提出了不同的隔离级别:未提交读(read uncommitted)、提交读(read committed)、可重复读(repeatable read)、序列化读(serializable)。其中最高级隔离级别就是序列化读,而在其他隔离级别中,由于事务是并发执行的,所以或多或少允许出现一些问题。见以下的矩阵表:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cda954d023dcf5a4bba3f4f71eb2342c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意,MySQL的InnoDB引擎在提交读级别通过MVCC解决了不可重复读的问题,在可重复读级别通过间隙锁解决了幻读问题,具体见下面的分析"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"实现原理"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"标准SQL事务隔离级别实现原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们上面遇到的问题其实就是并发事务下的控制问题,解决并发事务的最常见方式就是悲观并发控制了(也就是数据库中的锁)。标准SQL事务隔离级别的实现是依赖锁的,我们来看下具体是怎么实现的:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/45/45d7fd6f4cf9a594809cde62f425d31c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,在只使用锁来实现隔离级别的控制的时候,需要频繁的加锁解锁,而且很容易发生读写的冲突(例如在RC级别下,事务A更新了数据行1,事务B则在事务A提交前读取数据行1都要等待事务A提交并释放锁)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了不加锁解决读写冲突的问题,MySQL引入了MVCC机制,详细可见我以前的分析文章:"},{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000023332101","title":null},"content":[{"type":"text","text":"一文读懂数据库中的乐观锁和悲观锁和MVCC"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"InnoDB事务隔离级别实现原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在往下分析之前,我们有几个概念需要先了解下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"1、锁定读和一致性非锁定读"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"锁定读"},{"type":"text","text":":在一个事务中,主动给读加锁,如SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。分别加上了行共享锁和行排他锁。锁的分类可见我以前的分析文章:"},{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000025156465#","title":null},"content":[{"type":"text","text":"你应该了解的MySQL锁分类"}]},{"type":"text","text":")。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html","title":null},"content":[{"type":"text","text":"https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"一致性非锁定读"},{"type":"text","text":":InnoDB使用MVCC向事务的查询提供某个时间点的数据库快照。查询会看到在该时间点之前提交的事务所做的更改,而不会看到稍后或未提交的事务所做的更改(本事务除外)。也就是说在开始了事务之后,事务看到的数据就都是事务开启那一刻的数据了,其他事务的后续修改不会在本次事务中可见。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Consistent read是InnoDB在RC和RR隔离级别处理SELECT语句的默认模式。一致性非锁定读不会对其访问的表设置任何锁,因此,在对表执行一致性非锁定读的同时,其它事务可以同时并发的读取或者修改它们。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html","title":null},"content":[{"type":"text","text":"https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"2、当前读和快照读"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"当前读"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"读取的是最新版本,像"},{"type":"text","marks":[{"type":"strong"}],"text":"UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE"},{"type":"text","text":"这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"快照读"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"读取的是快照版本,也就是历史版本,像不加锁的"},{"type":"text","marks":[{"type":"strong"}],"text":"SELECT"},{"type":"text","text":"操作就是快照读,即不加锁的非阻塞读;"},{"type":"text","marks":[{"type":"strong"}],"text":"快照读的前提是隔离级别不是未提交读和序列化读级别,因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行,而序列化读则会对表加锁"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"3、隐式锁定和显式锁定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"隐式锁定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"InnoDB在事务执行过程中,使用两阶段锁协议(不主动进行显示锁定的情况):"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"显式锁定"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"InnoDB也支持通过特定的语句进行显示锁定(存储引擎层)"}]}]}]},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"select ... lock in share mode //共享锁\nselect ... for update //排他锁"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"MySQL Server层的显示锁定:"}]}]}]},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"lock table\nunlock table"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"了解完上面的概念后,我们来看下InnoDB的事务具体是怎么实现的(下面的读都指的是非主动加锁的select)"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a6/a69b29e8caa7eb2e1bfc68ce8d492d72.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一些常见误区"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"幻读到底包不包括了delete的情况?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可重复读:前后多次读取一行,数据内容不一致,针对其他事务的update和delete操作。为了解决这个问题,使用行共享锁,锁定到事务结束(也就是RR级别,当然MySQL使用MVCC在RC级别就解决了这个问题)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幻读:当同一个查询在不同时间生成不同的行集合时就是出现了幻读,针对的是其他事务的insert操作,为了解决这个问题,锁定整个表到事务结束(也就是S级别,当然MySQL使用间隙锁在RR级别就解决了这个问题)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网上很多文章提到幻读和提交读的时候,有的说幻读包括了delete的情况,有的说delete应该属于提交读的问题,那到底真相如何呢?我们实际来看下MySQL的官方文档(如下)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"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 "},{"type":"link","attrs":{"href":"https://segmentfault.com/a/1190000025156465#","title":null},"content":[{"type":"text","text":"SELECT"}]},{"type":"text","text":") is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html","title":null},"content":[{"type":"text","text":"https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,幻读针对的是结果集前后发生变化,所以看起来delete的情况应该归为幻读,但是我们实际分析下上面列出的标准SQL在RR级别的实现原理就知道,标准SQL的RR级别是会对查到的数据行加行共享锁,所以这时候其他事务想删除这些数据行其实是做不到的,所以在RR下,不会出现因delete而出现幻读现象,也就是幻读不包含delete的情况。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"MVCC能解决了幻读问题?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网上很多文章会说MVCC或者MVCC+间隙锁解决了幻读问题,实际上MVCC并不能解决幻读问题。如以下的例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"begin;\n\n#假设users表为空,下面查出来的数据为空\n\nselect * from users; #没有加锁\n\n#此时另一个事务提交了,且插入了一条id=1的数据\n\nselect * from users; #读快照,查出来的数据为空\n\nupdate users set name='mysql' where id=1;#update是当前读,所以更新成功,并生成一个更新的快照\n\nselect * from users; #读快照,查出来id为1的一条记录,因为MVCC可以查到当前事务生成的快照\n\ncommit;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到前后查出来的数据行不一致,发生了幻读。所以说只有MVCC是不能解决幻读问题的,解决幻读问题靠的是间隙锁。如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sql"},"content":[{"type":"text","text":"begin;\n\n#假设users表为空,下面查出来的数据为空\n\nselect * from users lock in share mode; #加上共享锁\n\n#此时另一个事务B想提交且插入了一条id=1的数据,由于有间隙锁,所以要等待\n\nselect * from users; #读快照,查出来的数据为空\n\nupdate users set name='mysql' where id=1;#update是当前读,由于不存在数据,不进行更新\n\nselect * from users; #读快照,查出来的数据为空\n\ncommit;\n\n#事务B提交成功并插入数据"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意,RR级别下想解决幻读问题,需要我们显式加锁,不然查询的时候还是不会加锁的"},{"type":"text","text":"。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章