彻底搞懂数据库的并发问题以及事务的隔离级别(通俗易懂)
1. 前言
1.1 什么是数据库事务?
- 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
- 举例说明
即:事务在提交之前,可以回滚。一旦提交,就不能撤销,提交成功后其他用户才可以通过查询浏览数据的变化。
1.2 概述
- 数据库中的事务有四大特性(ACID),分别是原子性、一致性、隔离性和持久性。
- 针对隔离性,有四个等级的隔离级别:读未提交、读已提交、可重复读、串行化。设置事务的隔离级别是为了解决数据库并发产生的脏读、不可重复读、幻读问题。
- 具体的我们往下看……
2. 事务的四大特性(ACID)
-
① 原子性( Atomicity )
事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行(即:要么全部执行成功,要么全部执行失败)。 -
② 一致性( Consistency )
几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果相一致,即:一个事务执行之前和执行之后都必须处于一致性状态。
意思就是:事务执行后,数据库状态与其业务规则保持一致。用上述转账的例子来说就是:无论事务执行成功与否,也不管互相转账多少次,但是参与转账的两个账户余额之和是保持不变的。 -
③ 隔离性( Isolation )
一个事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
即:在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。(类似于多线程中的锁) -
④ 持久性( Durability )
对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
即:一个事务一旦提交,事务中对数据库的所有操作都被持久化到数据库中,即永久性的改变,即使提交后的下一秒遇到数据库崩溃,也不会对数据造成影响,在数据库重启后依然能通过某种机制恢复数据。
3. 数据库并发产生的问题
-
① 脏读
- 脏读就是读了未提交的新事务。
- 意思就是:对于 table1 表中的某条记录的某个字段 filed ,现在有两个事务,一个正在读的事务A 读到了另一个事务B 正在 update 但未提交的数据。
- 解释就是:A事务正在 查询字段 filed 的值,B事务正在修改 字段 filed的值,B事务修改之后还未提交,但是A事务读到了这个修改了但未提交的内容,这种情况就是脏读。因为A不应该读到B未提交的数据,因为B可能会回滚,那么对A来说未提交的数据就是脏数据,不应该读到。
-
② 不可重复读——针对update
- 读取了提交的新事务(指更新操作)。
- 意思就是:对于 table1 表中的某条记录的某个字段 filed ,现在有两个事务 A 和 B,A 事务正在读取这条记录的字段 filed 内容,B 事务正在 update 这条记录的字段 filed 内容,现在可能发生的情况是:第①步:A 事务第一次读取的时候 filed 内容为XXXX111(此时B事务未修改),第②步:B 事务将filed 内容 update为XXXX1222,并提交了事务,第③步:A 事务再次读取的时候读到了filed 内容为XXXX1222,即 A 事务如果多次读取数据,读取到的数据可能会不一致,不能重复读到 B 事务修改前的内容。
- 解释就是:站在数据库的存储原理以及开发的角度来看,这种情况是正常的,B 修改并且已经提交了,A 第二次读就应该读到新的数据,这很合理。但是站在某个业务的角度来看,我这是同一个事务,却发生第一次读是XXXX111,第二次读变成XXXX1222,或许第一次的内容正要记还没来得及数据变了,怎么不可重复读呢?对他们来说这就是不可重复读问题。这样解释的话应该就很明白什么是不可重复读问题了吧。
-
③ 幻读——针对插入、删除
-
读取了提交的新事务,指增删操作。
关于幻读的解释,看个人理解了,我这里列举了两种情况,第一种解释是最为常见的。也最好理解,但是我个人觉得第二种解释更为合理,都行吧,都算挺好理解的。无论哪种解释,需要了解的是:幻读都是可以接受的,所以一般不解决这种问题。
(1) 幻读的第一种解释
- 用两种场景来解释一下就是:
① 事务A正在根据条件筛选读取table1里符合条件的数据,然后事务B往表里批量插入很多新的数据,之后事务A第二次读取的时候,数据突然变多了,以为产生了幻觉。
② 事务A正在批量更新table1的某个字段,比如正在更新dog表里的【品种】字段,将品种都跟新为”边牧“,此时事务B也在操作dog表,只是事务B在批量插入品种为”金毛“的数据,然后事务A更新之后再次查的时候,发现居然不都是”边牧“还有”金毛“,以为出现了幻觉。
(2) 幻读的第二种解释
- 事务 A 根据条件查询得到了 N 条数据,但此时事务 B 新增 或者删除了 M 条符合事务 A 查询条件的数据,这样当事务 A 再次进行查询的时候真实的数据集已经发生了变化,但是A却查询不出来这种变化,因此产生了幻读。
-
4. 事务的隔离级别
4.1 数据库的4种隔离级别
- 事务的隔离级别就是为解决上面并发产生的脏读、不可重复读、幻读 问题存在的。
脏读 | 不可重复读 | 幻读 | 描述 | |
---|---|---|---|---|
读未提交(Read uncommitted)) | 问题存在 | 问题存在 | 问题存在 | 允许一个事务可以读取另一个未提交事务的数据 |
读已提交(Read committed) | × | 问题存在 | 问题存在 | 一个事务修改的数据必须提交后才能被其他事务读取到 |
可重复读(Repeatable red) | × | × | 问题存在 | 在事务结束前,都可以反复读取到事务刚开始时看到的数据 |
串行化(Serializable) | × | × | × | 最高的事务隔离级别,在该级别下,事务串行化顺序执行 |
-
我们再对上面表格简单说明一下:上面表格的 × 说明当前的隔离性解决了对应的并发问题。
① 读未提交(Read uncommitted)):隔离级别最低,脏读、不可重复读、幻读都没解决。所以,一般开发中不会用这个隔离级别。
② 读已提交(Read committed):可以理解为只允许事务A读取已被事务B提交后的数据,这样就避免了脏读,可重复读可幻读问题还存在。
③ 可重复读(Repeatable red):在事务结束前,都可以反复读取到事务刚开始时看到的数据。就是即便其他事务有修改操作,也允许事务A在多次读取数据的过程中,保证读取到的数据是一致的。解决了脏读、不可重复读问题。这个是 MySQL 默认的事务隔离级别。
④ 串行化(Serializable):事务的最高隔离级别,在一个事务在读取一张表的数据时,禁止其他事务对该表进行增删改查操作,解决了所有并发问题,但是性能十分低下,一般开发中也不会用这个隔离级别。
4.2 数据库的默认隔离级别
- Oracle 支持两种隔离级别:读已提交(Read committed)、串行化(Serializable)
默认的隔离级别是:读已提交(Read committed) - MySQL 四种隔离级别都支持。
默认的隔离级别是:可重复读(Repeatable red)
5. 解释说明事务的隔离级别
- 看完上面的一般应该都明白的差不多了,为了更形象的说明,我们用命令结合数据库数据再简单说一下,我下面用的是
MySQL
数据库来解释说明的。首先先看几个命令。
5.1 查看事务的隔离级别与设置事务自动提交模式
5.1.1 常用的命令
- ① 查看事务的隔离级别:
select @@tx_isolation;
select @@global.tx_isolation,@@tx_isolation;
(系统全局的隔离级别)
MySQL 的默认隔离级别:可重复读。 - ② 查看当前事务自动提交模式:
SHOW VARIABLES LIKE 'autocommit';
如果结果显示autocommit
的值是ON
,表示系统开启自动提交模式;如果autocommit
的值是OFF
,则表示未开启自动提交模式。 - ③ 修改事务自动提交的模式:
语法:
SET autocommit = 0 | 1 | true | false | ON | OFF;
- ④ 修改当前MySQL连接的隔离级别:
set session transaction isolation level serializable;
可选的参数为:read uncommitted, read committed, repetable read, serializable
- ⑤ 设置数据库系统的全局的隔离级别:
set global transaction isolation level serializable;
5.1.2 事务自动提交模式说明
-
上面我们说
autocommit
的值是ON
表示开启自动提交,否则没用开启。那么开启事务自动提交和关闭事务自动提交效果怎么体现的呢?① 如果开启自动提交,则每执行一条 SQL 语句,事务都会提交一次。
② 如果关闭自动提交,用户将会一直处于某个事务中,只有提交或回滚后才会结束当前事务,重新开始一个新事务。
5.2 测试事务的隔离级别
5.2.1 MySQL 的默认隔离级别下——Repeatable red
- 首先我们开启两个事务,在隔离级别为默认的隔离级别Repeatable red 下,设置事务不自动提交,设置之后检查一下:
- 然后,① 两个事务同时查询数据的某条数据,如图
② 接下来,左边的事务要进行update操作,修改dog_age为6,修改之后在没有提交的情况下,两边事务再进行查询一下,可以看到右边的事务查询不到左边未提交的事务。这里就解释了我们说的避免了脏读。
③ 接下来,左边事务提交一下,两边再重查一下:可以看到右边的事务读取到的还是原先事务开始读的数据。这里就解释了我们说的解决了“不可重复读”的问题。
这里,我们重新开启一个事务,即读取到左边事务提交后的数据
④ 接下来,我们让左边事务再插入一条数据,插入之前先让右边事务查一下,插入之后再查询一下,对比看看右边事务查的数据有没有发生变化:
我们看到,左边事务新插入了数据,已经改变了数据库的结果集,但是右边事务却没用查询出来(如果你用不可重复度解释,刚开始觉得有点意思,但是再想想觉得哪里不合适,因为是插入的数据没用读出来不只是影响你读的情况,而还会影响你接下来的插入操作),来看下图:
即:读不出来,又不让插入,导致右边的事务就产生了幻读。
5.2.2 修改隔离级别为 serializable——解决幻读
- 在上面的默认级别 Repeatable red 下,我们修改一下隔离级别再继续测试,请继续……
- ① 我们用命令
set global transaction isolation level serializable;
将隔离级别修改为最高级。修改完之后重新连接一下数据库,查看一下隔离级别,同时别忘了关闭数据库事务自动提交模式:
- ② 我们再重新让左边事务插入数据,右边事务重新读取数据,根据测试可以看除,修改成最高隔离级别后,右边事务如果不结束,左边事务是不能进行插入(可以说是不能进行增删改查操作)操作的,因为事务是串行的他们是按照一定的先后顺序执行,也可理解为多线程的同步监视器。所以这种情况下,肯定可以解决脏读,不可重复读以及幻读,但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
5.2.3 测试隔离级别 read uncommitted
- 都看到这里了,想必对隔离级别事务的隔离级别已经相当清楚了,怕你还有疑惑,再测试一个最低的隔离级别也不妨。
- ① 将隔离级别修改为:read uncommitted
set global transaction isolation level read uncommitted;
- ② 现在,我们让左边事务进行update操作,修改之前两个事务先进行查询,确保数据一致,且记得关闭自动提交事务:
- 现在左边事务去修改这条数据对应的狗狗年龄,在未提交之前,让右边事务查询看看结果:
我们把这种情况就叫做脏读,所以可见,事务的最低隔离级别(read uncommitted)没用解决脏读问题,我们上面测试的如果隔离级别为可重复读(Repeatable red)的话,可以解决脏读问题,除 读未提交(Read uncommitted))外的其他3个隔离级别都能解决脏读问题,测试到这里了,其他的应该没必要测试了,该懂得早懂了,不能懂得,我只能说你加油哦!
好了,就说到这了吧,这篇文章应该足够弄明白事务的隔离级别了!
6. 参考:
https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1/9744607?fr=aladdin.