CategoryResourceRepost/极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/15丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

254 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="15丨初识事务隔离隔离的级别有哪些它们都解决了哪些异常问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cf/62/cf23190e0286e17cfafe7c743acb7b62.mp3"></audio>
上一篇文章中我们讲到了事务的四大特性ACID分别是原子性、一致性、隔离性和持久性其中隔离性是事务的基本特性之一它可以防止数据库在并发处理时出现数据不一致的情况。最严格的情况下我们可以采用串行化的方式来执行每一个事务这就意味着事务之间是相互独立的不存在并发的情况。然而在实际生产环境下考虑到随着用户量的增多会存在大规模并发访问的情况这就要求数据库有更高的吞吐能力这个时候串行化的方式就无法满足数据库高并发访问的需求我们还需要降低数据库的隔离标准来换取事务之间的并发能力。
有时候我们需要牺牲一定的正确性来换取效率的提升也就是说我们需要通过设置不同的隔离等级以便在正确性和效率之间进行平衡。同时随着RDBMS种类和应用场景的增多数据库的设计者需要统一对数据库隔离级别进行定义说明这些隔离标准都解决了哪些问题。
我们今天主要讲解事务的异常以及隔离级别都有哪些,如果你已经对它们有所了解,可以跳过本次章节,当然你也可以通过今天的课程快速复习一遍:
1. 事务并发处理可能存在的三种异常有哪些?什么是脏读、不可重复读和幻读?
1. 针对可能存在的异常情况,四种事务隔离的级别分别是什么?
1. 如何使用MySQL客户端来模拟脏读、不可重复读和幻读
## 事务并发处理可能存在的异常都有哪些?
在了解数据库隔离级别之前我们需要了解设定事务的隔离级别都要解决哪些可能存在的问题也就是事务并发处理时会存在哪些异常情况。实际上SQL-92标准中已经对3种异常情况进行了定义这些异常情况级别分别为脏读Dirty Read、不可重复读Nonrepeatable Read和幻读Phantom Read
脏读、不可重复读和幻读都代表了什么我用一个例子来给你讲解下。比如说我们有个英雄表heros_temp如下所示
<img src="https://static001.geekbang.org/resource/image/66/dd/66cbeb736b5e92dc271f86b7152d46dd.png" alt=""><br>
这张英雄表,我们会记录很多英雄的姓名,假设我们不对事务进行隔离操作,那么数据库在进行事务的并发处理时会出现怎样的情况?
第一天,小张访问数据库,正在进行事务操作,往里面写入一个新的英雄“吕布”:
```
SQL&gt; BEGIN;
SQL&gt; INSERT INTO heros_temp values(4, '吕布');
```
当小张还没有提交该事务的时候,小李又对数据表进行了访问,他想看下这张英雄表里都有哪些英雄:
```
SQL&gt; SELECT * FROM heros_temp;
```
这时,小李看到的结果如下:
<img src="https://static001.geekbang.org/resource/image/1c/a8/1c4c7b7d7223739eb5346e3159bb34a8.png" alt=""><br>
你有没有发现什么异常?这个时候小张还没有提交事务,但是小李却读到了小张还没有提交的数据,这种现象我们称之为“脏读”。
那么什么是不可重复读呢?
第二天小张想查看id=1的英雄是谁于是他进行了SQL查询
```
SQL&gt; BEGIN;
SQL&gt; SELECT name FROM heros_temp WHERE id = 1;
```
运行结果:
<img src="https://static001.geekbang.org/resource/image/e3/74/e31d7f286ac5d77a82fb6ece2d1d6174.png" alt=""><br>
然而此时小李开始了一个事务操作他对id=1的英雄姓名进行了修改把原来的“张飞”改成了“张翼德”
```
SQL&gt; BEGIN;
SQL&gt; UPDATE heros_temp SET name = '张翼德' WHERE id = 1;
```
然后小张再一次进行查询同样也是查看id=1的英雄是谁
```
SQL&gt; SELECT name FROM heros_temp WHERE id = 1;
```
运行结果:
<img src="https://static001.geekbang.org/resource/image/e3/74/e3f6e70119376869f9a39d739c842674.png" alt=""><br>
这个时候你会发现,两次查询的结果并不一样。小张会想这是怎么回事呢?他明明刚执行了一次查询,马上又进行了一次查询,结果两次的查询结果不同。实际上小张遇到的情况我们称之为“不可重复读”,也就是同一条记录,两次读取的结果不同。
从这个例子中我们能看到小张和小李分别开启了两个事务针对客户端A和客户端B我用时间顺序的方式展示下他们各自执行的内容
<img src="https://static001.geekbang.org/resource/image/c3/b8/c3361eaa98016638fc47af65ce12edb8.png" alt="">
那什么是幻读呢?
第三天,小张想要看下数据表里都有哪些英雄,他开始执行下面这条语句:
```
SQL&gt; SELECT * FROM heros_temp;
```
<img src="https://static001.geekbang.org/resource/image/21/41/213f1601abb1629689d6d8477bd16641.png" alt=""><br>
这时当小张执行完之后,小李又开始了一个事务,往数据库里插入一个新的英雄“吕布”:
```
SQL&gt; BEGIN;
SQL&gt; INSERT INTO heros_temp values(4, '吕布');
```
不巧的是,小张这时忘记了英雄都有哪些,又重新执行了一遍查询:
```
SQL&gt; SELECT * FROM heros_temp;
```
<img src="https://static001.geekbang.org/resource/image/13/ab/134a542eae8b47b9c5da5b74aa96d3ab.png" alt=""><br>
他发现这一次查询多了一个英雄原来只有3个现在变成了4个。这种异常情况我们称之为“幻读”。
我来总结下这三种异常情况的特点:
1. 脏读:读到了其他事务还没有提交的数据。
1. 不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。
1. 幻读事务A根据条件查询得到了N条数据但此时事务B更改或者增加了M条符合事务A查询条件的数据这样当事务A再次进行查询的时候发现会有N+M条数据产生了幻读。
## 事务隔离的级别有哪些?
脏读、不可重复读和幻读这三种异常情况是在SQL-92标准中定义的同时SQL-92标准还定义了4种隔离级别来解决这些异常情况。
解决异常数量从少到多的顺序比如读未提交可能存在3种异常可串行化则不会存在这些异常决定了隔离级别的高低这四种隔离级别从低到高分别是读未提交READ UNCOMMITTED 、读已提交READ COMMITTED、可重复读REPEATABLE READ和可串行化SERIALIZABLE。这些隔离级别能解决的异常情况如下表所示
<img src="https://static001.geekbang.org/resource/image/b0/95/b07103c5f5486aec5e2daf1dacfd6f95.png" alt=""><br>
你能看到可串行化能避免所有的异常情况,而读未提交则允许异常情况发生。
关于这四种级别,我来简单讲解下。
读未提交,也就是允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读等情况。
读已提交就是只能读到已经提交的内容可以避免脏读的产生属于RDBMS中常见的默认隔离级别比如说Oracle和SQL Server但如果想要避免不可重复读或者幻读就需要我们在SQL查询的时候编写带加锁的SQL语句我会在进阶篇里讲加锁
可重复读保证一个事务在相同查询条件下两次查询得到的数据结果是一致的可以避免不可重复读和脏读但无法避免幻读。MySQL默认的隔离级别就是可重复读。
可串行化,将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。
## 使用MySQL客户端来模拟三种异常
我在讲解这三种异常的时候举了一个英雄数据表查询的例子你还可以自己写SQL来模拟一下这三种异常。
首先我们需要一个英雄数据表heros_temp具体表结构和数据你可以从[GitHub](https://github.com/cystanford/sql_heros_data)上下载heros_temp.sql文件。
你也可以执行下面的SQL文件来完成heros_temp数据表的创建。
```
-- ----------------------------
-- Table structure for heros_temp
-- ----------------------------
DROP TABLE IF EXISTS `heros_temp`;
CREATE TABLE `heros_temp` (
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of heros_temp
-- ----------------------------
INSERT INTO `heros_temp` VALUES (1, '张飞');
INSERT INTO `heros_temp` VALUES (2, '关羽');
INSERT INTO `heros_temp` VALUES (3, '刘备');
```
模拟的时候我们需要开两个MySQL客户端分别是客户端1和客户端2。
在客户端1中我们先来查看下当前会话的隔离级别使用命令
```
mysql&gt; SHOW VARIABLES LIKE 'transaction_isolation';
```
然后你能看到当前的隔离级别是REPEATABLE-READ也就是可重复读。
<img src="https://static001.geekbang.org/resource/image/fa/5f/faaf3196f842d3331e40364fa331925f.png" alt="">
现在我们把隔离级别降到最低设置为READ UNCOMMITTED读未提交
```
mysql&gt; SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
```
然后再查看下当前会话SESSION下的隔离级别结果如下
<img src="https://static001.geekbang.org/resource/image/25/33/25bc5c5e570315b9c711156cf87f4233.png" alt="">
因为MySQL默认是事务自动提交这里我们还需要将autocommit参数设置为0命令如下
```
mysql&gt; SET autocommit = 0;
```
然后我们再来查看SESSION中的autocommit取值结果如下
<img src="https://static001.geekbang.org/resource/image/8c/e3/8c584071af1c37cb2ea7835ea94489e3.png" alt=""><br>
接着我们以同样的操作启动客户端2也就是将隔离级别设置为READ UNCOMMITTED读未提交autocommit设置为0。
### 模拟“脏读”
我们在客户端2中开启一个事务在heros_temp表中写入一个新的英雄“吕布”注意这个时候不要提交。
<img src="https://static001.geekbang.org/resource/image/23/bb/2329718eb5e446a2e2e1e42420818abb.png" alt=""><br>
然后我们在客户端1中查看当前的英雄表
<img src="https://static001.geekbang.org/resource/image/a7/34/a74922819952a7afc93311c8e3f85834.png" alt="">
你能发现客户端1中读取了客户端2未提交的新英雄“吕布”实际上客户端2可能马上回滚从而造成了“脏读”。
### 模拟“不可重复读”
我们用客户端1来查看id=1的英雄
<img src="https://static001.geekbang.org/resource/image/39/17/39d6f01fbf926a83e28950bee720c917.png" alt="">
然后用客户端2对id=1的英雄姓名进行修改
<img src="https://static001.geekbang.org/resource/image/43/a6/43e24b5a2c9d861d4fde7ddc3195cda6.png" alt="">
这时用客户端1再次进行查询
<img src="https://static001.geekbang.org/resource/image/38/dd/38a638e9dcca39bc1cfb68279acdcbdd.png" alt="">
你能发现对于客户端1来说同一条查询语句出现了“不可重复读”。
### 模拟“幻读”
我们先用客户端1查询数据表中的所有英雄
<img src="https://static001.geekbang.org/resource/image/cd/32/cd47f77c15be231ea0b185c265d4cd32.png" alt="">
然后用客户端2开始插入新的英雄“吕布”
<img src="https://static001.geekbang.org/resource/image/13/1d/13ffd32b3654ff569e0eef6dbb2de51d.png" alt=""><br>
这时我们再用客户端1重新进行查看
<img src="https://static001.geekbang.org/resource/image/c3/3e/c3de2074d616530cafc6942e4a592b3e.png" alt=""><br>
你会发现数据表多出一条数据。
如果你是初学者那么你可以采用heros_temp数据表简单模拟一下以上的过程加深对脏读、不可重复读以及幻读的理解。对应的你也会更了解不同的隔离级别解决的异常问题。
## 总结
我们今天只是简单讲解了4种隔离级别以及对应的要解决的三种异常问题。我会在优化篇这一模块里继续讲解隔离级别以及锁的使用。
你能看到标准的价值在于即使是不同的RDBMS都需要达成对异常问题和隔离级别定义的共识。这就意味着一个隔离级别的实现满足了下面的两个条件
1. 正确性:只要能满足某一个隔离级别,一定能解决这个隔离级别对应的异常问题。
1. 与实现无关实际上RDBMS种类很多这就意味着有多少种RDBMS就有多少种锁的实现方式因此它们实现隔离级别的原理可能不同然而一个好的标准不应该限制其实现的方式。
隔离级别越低,意味着系统吞吐量(并发程度)越大,但同时也意味着出现异常问题的可能性会更大。在实际使用过程中我们往往需要在性能和正确性上进行权衡和取舍,没有完美的解决方案,只有适合与否。
<img src="https://static001.geekbang.org/resource/image/aa/fb/aa2ae6682a571676b686509623a2a7fb.jpg" alt=""><br>
今天的内容到这里就结束了,你能思考一下为什么隔离级别越高,就越影响系统的并发性能吗?以及不可重复读和幻读的区别是什么?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。