一文了解 MySQL 事务和隔离级别

一文了解 MySQL 事务和隔离级别_第1张图片

1. 什么是事务

事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位(不可再进行分割),由一个有限的数据库操作序列构成(多个DML语句,select语句不包含事务),要不全部成功,要不全部不成功。

如 A 给 B 要划钱,A 的账户-1000 元, B 的账户就要+1000 元,这两个 update 语句必须作为一个整体来执行,不然 A 扣钱了,B 没有加钱这种情况就是错误的。那么事务就可以保证 A 、B 账户的变动要么全部一起发生,要么全部一起不发生。

2. 事务特性

事务具有 4 个属性:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)。这四个属性通常称为 ACID 特性。

2.1 原子性

一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不能只执行其中的一部分操作。

比如: A 给 B 转钱,A 扣除 500 元 ,B 增加 500 元

整个事务的操作要么全部成功,要么全部失败,不能出现 A 扣钱,但是 B 不增加的情况。如果原子性不能保证,就会很自然的出现一致性问题。

2.2 一致性

一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。

即 A 给 B 转钱,A 扣除 500 元 ,B 增加 500 元,扣除的钱(-500) 与增加的钱(+500) 相加应该为 0。

2.3 隔离性

一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

如果隔离性不能保证,会导致什么问题?

假如 A 开始有 1000,B 没有钱,然后 A 给 B 转了 2 次钱,第一次转 500,第二次也转 500,如果我们把这两次转钱操作分别称为 T1 和 T2,则在正常情况下,应该是一次一次的来进行转账,但在数据库中可能出现交替的情况,如:

T1 T2
读:A = 1000
读:A = 1000
A - 500 = 500
B + 500 = 500
读:B = 500
A - 500 = 500
B + 500 = 1000

按照上面的方式 A 此时还剩余 500,而 B 已经为 1000,相当于多了 500。

2.4 持久性

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失。

3. 事务并发

我们知道 MySQL 是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称 之为一个会话(Session)。每个客户端都可以在自己的会话中向服务器发出请求 语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。

在上面我们说过事务有一个称之为隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据,这样的话并发事务的执行就变成了串行化执行。

但是对串行化执行性能影响太大,我们既想保持事务的一定的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,当我们舍弃隔离性的时候,可能会带来什么样的数据问题呢?

3.1 隔离级别

MySQL 具有四种事务隔离级别,隔离力度依次递增,高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。不同业务场景下使用不同的数据库事物隔离性,部分关键业务采用隔离性高的隔离级别,以保证数据正确性。

MySQL 四种事物隔离级别:

  • Read Uncommitted(读未提交):事务能读到不同事物没有提交(未commit)的数据结果,实际应用比较少,会产生脏读,事务已经读到其他事务未提交的数据,但数据被回滚,称为脏读
  • Read Committed(读已提交):事务读取其他事物已经提交的数据,读取到的是最新的数据,所以会出现在同一事务中 select 读取到的数据前后不一致,会出现不可重复读问题,不可重复读问题就是我们在同一个事务中执行完全相同的 select 语句时可能看到不一样的结果。
  • Repeatable Read(可重复读):mysql 默认事物隔离级别,在同一事务中多次读取同样的数据结果是一样的,解决了不可重复读的问题,此级别会出现幻读的问题,即当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的行。
  • Serializable(串行化):最高的事物隔离级别,串行化强制事物排序阻塞,避免事物冲突,解决了上述所有的问题,它使用了共享锁,执行效率低下,会导致大量的超时和锁切换竞争现象,实际开发应用很少。

注:MySQL 默认的事物隔离级别为可重复读(Repeatable Read)。

查看默认隔离级别:

SHOW VARIABLES LIKE 'transaction_isolation';
或
SELECT @@transaction_isolation;
复制代码

如何设置事务的隔离级别

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
复制代码

其中的 level 可选值有 4 个:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

设置事务的隔离级别的语句中,在 SET 关键字后可以放置 GLOBAL 关键字、SESSION 关键字或者什么都不放,这样会对不同范围的事务产生不同的影响,具体如下:

使用 GLOBAL 关键字(在全局范围影响):

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
复制代码

只对执行完该语句之后产生的会话起作用,当前已经存在的会话无效。

使用 SESSION 关键字(在会话范围影响):

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
复制代码

对当前会话的所有后续的事务有效 。

该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务。如果在事务之间执行,则对后续的事务有效。

上述两个关键字都不用(只对执行语句后的下一个事务产生影响):

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
复制代码

只对当前会话中下一个即将开启的事务有效。下一个事务执行完后,后续事务将恢复到之前的隔离级别。该语句不能在已经开启的事务中间执行,会报错的。

如果我们在服务器启动时想改变事务的默认隔离级别,可以修改启动参数 transaction-isolation 的值,比方说我们在启动服务器时指定了 --transaction-isolation=SERIALIZABLE,那么事务的默认隔离级别就从原来的 REPEATABLE READ 变成了 SERIALIZABLE。

3.2 事务基本语法

说明 操作
事务开始 begin
事务回滚 rollback
事务提交 commit

新建一张表来演示不同隔离级别产生的问题:

CREATE TABLE `account` (
  `id` bigint NOT NULL COMMENT '主键ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `money` decimal(12,2) DEFAULT NULL COMMENT '账户余额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
复制代码

初始数据如下:

一文了解 MySQL 事务和隔离级别_第2张图片

3.3 脏读

当一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读。

开启两个 session 会话,把隔离级别都设定为读未提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
复制代码

1、开启一个事务 T1,读取 A 的当前金额。

一文了解 MySQL 事务和隔离级别_第3张图片

2、开启另外一个事务 T2,把 A 的金额增加 500,但不提交。

一文了解 MySQL 事务和隔离级别_第4张图片

3、在去 T1 中查询数据为1500。

一文了解 MySQL 事务和隔离级别_第5张图片

4、此时如果 T2 回滚(ROLLBACK),则最终的金额为 1000,如果在 T2 回滚之前,T1拿到的是 1500 并做了操作,就会造成脏读现象。

T1 T2
BEGIN
SELECT money FROM account WHERE id = 1 BEGIN
UPDATE account SET money = money + 500 WHERE id = 1
SELECT money FROM account WHERE id = 1
ROLLBACK

3.4 不可重复读

当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。

开启两个 session 会话,把隔离级别都设定为读已提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
复制代码

1、开启一个事务 T1,读取 A 的当前金额。

一文了解 MySQL 事务和隔离级别_第6张图片

2、开启另外一个事务 T2,把 A 的金额增加 500,提交。

一文了解 MySQL 事务和隔离级别_第7张图片

3、此时对于 T1 来讲,A 的金额为1500,两次的查询结果不同。T2 对记录做了修改并提交成功,这意味着修改的记录对其他事务是可见的,因此 T1 两次读取的 money 值不同。

T1 T2
BEGIN
SELECT money FROM account WHERE id = 1 BEGIN
UPDATE account SET money = money + 500 WHERE id = 1
COMMIT
SELECT money FROM account WHERE id = 1
COMMIT

3.5 幻读

常规解释:

事务 T1 执行两次相同 select 操作得到不同的数据集,这其实并不是幻读,这是不可重复读的一种(删除同理),只会在 R-U、R-C 级别下出现,而在 MySQL 默认的 RR 隔离级别是不会出现的。

实际上:

并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

开启两个 session 会话,把隔离级别都设定为可重复读:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
复制代码

1、开启一个事务 T1,查询 name 为 A 的数据。

一文了解 MySQL 事务和隔离级别_第8张图片

2、开启另外一个事务 T2,新增一条 name 为 A 的数据并提交。

一文了解 MySQL 事务和隔离级别_第9张图片

3、然后再 T1 中再次查询发现只能查出一条数据,满足了可重复读,但如果在 T1 中也添加 T2 同样的数据,则会报错,在 T1 中查不到数据,但又被告知已经存在了,由此产生了幻读现象。

一文了解 MySQL 事务和隔离级别_第10张图片

另外的场景:

银行 A 开启了一个事务窗口,查询当前系统中有没有 "ayue" 用户,发现没有,银行 B 也开启了一个事务窗口,查询当前系统中也没有 "ayue" 用户,银行 A 先创建 "ayue" 用户并且提交,由于可重复读取,银行 B 在一次事务中必须保证查询的数据一致性,因此查询不到 "ayue",结果银行 B 窗口认为 "ayue" 没有被注册想注册 "ayue" 用户,就创建 "ayue" 用户结果发现系统提示 "ayue" 用户已经被注册",但是在本次事务中又查询不到 "ayue",就好像出现幻觉一样。

4. 总结

MySQL 四大隔离级别:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能发生 可能发生 可能发生
READ COMMITTED 解决 可能发生 可能发生
REPEATABLE READ 解决 解决 可能发生
SERIALIZABLE 解决 解决 解决

你可能感兴趣的:(数据结构,数据库,后端,mysql,数据库,java,数据结构,程序人生)