程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • MySQL基础

  • MySQL高级

    • 第01章_Linux下安装MySQL
    • 第02章_MySQL的数据目录
    • 第03章_用户与权限管理
    • 第04章_逻辑架构
    • 第05章_存储引擎
    • 第06章_索引的数据结构
    • 第07章_InnoDB数据结构
    • 第08章_索引的创建与设计
    • 第09章_性能分析工具
    • 第10章_索引优化与优化
    • 第11章_数据库的设计规范
    • 第12章_数据库调优策略
    • 第13章_事务基础知识
      • 1.数据库事务概述
        • 1.1.存储引擎支持情况
        • 1.2.基本概念
        • 1.3.事务的 ACID 特性
        • 1. 原子性(atomicity)
        • 2. 一致性 (consistency)
        • 3. 隔离型 (isolation)
        • 4. 持久性 (durability)
        • 1.4.事务的状态
      • 2.如何使用事务
        • 2.1.显式事务
        • 步骤 1:start transaction 或者 begin
        • 步骤 2:一系列事务中的操作(主要是 DML,不含DDL)
        • 步骤 3:提交事务或中止事务(即回滚事务)
        • 2.2.隐式事务
        • 2.3.隐式提交数据的情况
        • 2.4.使用举例 1:提交与回滚
        • 2.5.使用举例 2:测试不支持事务的 Engine
        • 2.6.使用举例 3:savepoint
      • 3.事务隔离级别
        • 3.1.数据准备
        • 3.2.数据并发问题
        • 3.2.1.脏写 (Dirty Write)
        • 3.2.1.脏读 (Dirty Read)
        • 3.2.1.不可重复读 (Non-Repeatable Read)
        • 3.2.1.幻读 (Phantom)
        • 3.3.SQL 中的四种隔离级别
        • 3.4.MySQL 支持的四种隔离级别
        • 3.5.如何设置事务的隔离级别
        • 3.6.不同隔离级别举例
        • 3.6.1.数据准备
        • 3.6.2.读未提交之脏读问题
        • 3.6.3.读已提交解决脏读问题
        • 3.6.4.可重复读解决不可重复读问题
        • 3.6.5.可重复读存在的幻读问题
      • 4.事务的常见分类
        • 4.1.扁平事务 (Flat Transactions)
        • 4.2.带有保存点的扁平事务 (Flat Transactions with Savepoints)
        • 4.3.链事务 (Chained Transactions)
        • 4.4.嵌套事务 (Nested Transactions)
        • 4.5.分布式事务 (Distributed Transactions)
    • 第14章_MySQL事务日志
    • 第15章_锁
    • 第16章_多版本并发控制
    • 第17章_其他数据库日志
    • 第18章_主从复制
    • 第19章_数据库备份与恢复
    • 第20章_Docker实现主从复制
  • MySQL
  • MySQL高级
scholar
2023-08-29
目录

第13章_事务基础知识

友情提示

转载须知: 以下所有文章整理于B站宋红康老师的《MySQL数据库入门到大牛》。MySQL (opens new window)

# 1.数据库事务概述

事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

# 1.1.存储引擎支持情况

SHOW ENGINES 命令来查看当前 MySQL 支持的存储引擎都有哪些,以及这些存储引擎是否支持事务。

image-20240308120832182 能看出在 MySQL 中,只有 InnoDB 是支持事务的。

# 1.2.基本概念

事务: 一组逻辑操作单元,使数据从一种状态变换到另一种状态。

事务处理的原则: 保证所有事务都作为 一个工作单元来执行,即使出 现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交 (commit),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改 ,整个事务回滚 (rollback) 到最初状态。

# 案例:AA 用户给 BB 用户转账 100
update account set money = money - 180 where name = 'AA';
# 服务器宕机
update account set money = money + 100 where name = 'BB';
1
2
3
4

# 1.3.事务的 ACID 特性

# 1. 原子性(atomicity)

原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。即要么转账成功,要么转账失败,是不存在中间的状态。如果无法保证原子性会怎么样?就会出现数据不一致的情形,A 账户减去100元,而 B 账户增加 100元 操作失败,系统将无故丢失 100 元。

# 2. 一致性 (consistency)

一致性保证事务从一个一致的状态转移到另一个一致的状态。事务执行前后,数据库的完整性约束没有被破坏。例如,如果数据库中有一个规则是账户余额不得为负,那么任何一次事务,无论是存款还是取款,都必须确保这一规则在事务开始和结束时仍然成立。

那什么是合法的数据状态呢? 满足预定的约束的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。

举例

  1. 举例 1:A 账户有 200 元,转账 300 元出去,此时 A 账户余额为 -100 元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须 >= 0。
  2. 举例 2:A 账户有 200 元,转账 50 元给 B 账户,A 账户的钱扣了,但是 B 账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求 A + B 的总余额必须不变。
  3. 举例 3:在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。

# 3. 隔离型 (isolation)

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

如果无法保证隔离性会怎么样?假设 A 账户有 200 元,B 账户 0 元。A 账户往 B 账户转账两次,每次金额为 50 元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形:

UPDATE accounts SET money = money - 50 WHERE NAME = 'AA'; 
UPDATE accounts SET money = money + 50 WHERE NAME = 'BB';
1
2

在这里插入图片描述

# 4. 持久性 (durability)

持久性是指一个事务一旦被提交,它对数据库的修改就是永久性的,即使在系统发生故障的情况下也不会丢失。

持久性是通过事务日志来保证的。日志包括了重做日志和回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

书生小结

ACID 是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是目的。数据库事务,其实就是数据库设计者为了方便起见,把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称为一个事务。

# 1.4.事务的状态

我们现在知道事务是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL 根据这些操作所执行的不同阶段把事务大致划分成几个状态:

① 活动的 (active):事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的状态。

② 部分提交的 (partially committed):当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在部分提交的状态。

③ 失败的 (failed):当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态。

④ 中止的 (aborted):如果事务执行了一部分而变为失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚 。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的状态。

⑤ 提交的 (committed):当一个处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的状态。

一个基本的状态转换图如下所示:

在这里插入图片描述

由图可知,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。


# 2.如何使用事务

使用事务有两种方式,分别为显式事务和隐式事务。

# 2.1.显式事务

# 步骤 1:start transaction 或者 begin

(1)该步骤的作用是显式开启一个事务。

begin; 
# 或者 
start transaction;
1
2
3

(2)start transaction 语句相较于 begin 特别之处在于,后边能跟随以下几个修饰符:

  • read only:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。

补充

只读事务中只是不允许修改那些其他事务也能访问到的表中的数据,对于临时表来说(我们使用 CREATETMEPORARY TABLE 创建的表),由于它们只能在当前会话中可见,所以只读事务其实也是可以对临时表进行增、删、改操作的。

  • read write:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
  • with consistent snapshot:启动一致性读。

(3)比如

# 开启一个只读事务
start transaction read only;
# 开启只读事务和一致性读
start transaction read only, with consistent snapshot;
# 开启读写事务和一致性读
start transaction read write, with consistent snapshot;
1
2
3
4
5
6

(4)注意:read only 和 read write 是用来设置所谓的事务访问模式的,就是以只读还是读写的方式来访问数据库中的数据,一个事务的访问模式不能同时既设置为只读的也设置为读写的,所以不能同时把 read only 和 read write 放到 start transaction 语句后边。如果我们不显式指定事务的访问模式,那么该事务的访问模式就是读写模式。

# 步骤 2:一系列事务中的操作(主要是 DML,不含DDL)

# 例如下面 2 个操作
UPDATE account SET balance = balance - 10 WHERE id = 1; 
UPDATE account SET balance = balance + 10 WHERE id = 2;
1
2
3

# 步骤 3:提交事务或中止事务(即回滚事务)

# 提交事务,当提交事务后,对数据库的修改是永久性的
commit;
# 回滚事务。即撤销正在进行的所有没有提交的修改
rollback;
# 将事务回滚到某个保存点
rollback to [savepoint]
1
2
3
4
5
6

其中关于savepoint相关操作有:

# 在事务中创建保存点,方便后续针对保存点进行回滚,一个事务中可以存在多个保存点
savepoint 保存点名称;
# 删除某个保存点
release savepoint 保存点名称;
1
2
3
4

# 2.2.隐式事务

(1)MySQL中有一个系统变量 autocommit:

# 查看事务的自动提交状态
show variables like 'autocommit';
1
2

image-20240308144630827

默认情况下,如果我们不显式的使用 start transaction 或者 begin语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。也就是说,不以 start transaction 或者 begin语句显式的开启一个事务,那么下边这两条语句就相当于放到两个独立的事务中去执行:

UPDATE account SET balance = balance - 18 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;
1
2

(2)当然,如果我们想关闭这种自动提交的功能,可以使用下边两种方法之一:

  • 显式地的使用 start transaction 或者 begin语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
  • 把系统变量 autocommit 的值设置为 OFF ,就像这样:
# 针对于 DML 操作是有效的,对 DDL 操作是无效的
SET autocommit = OFF; 
# 或 
SET autocommit = 0;
1
2
3
4

这样的话,我们写入的多条语句就算是属于同一个事务了,直到我们显式的写出 CONMIT 语句来把这个事务提交掉,或者显式的写出 ROLLBACK 语句来把这个事务回滚掉。

补充:Oracle 默认不自动提交,需要手写 COMMIT 命令,而 MySQL 默认自动提交。

# 2.3.隐式提交数据的情况

  • 数据定义语言 (Data definition language, DDL)

数据库对象,指的就是数据库、表、视图、存储过程等结构。当我们使用 CREATE、ALTER、DROP 等语句去修改数据库对象时,就会隐式地提交前边语句所属于的事务。例如:

begin;
select ... # 事务中的一条语句
update ... # 事务中的一条语句...
# 事务中的其它语句

create table ... # 此语句会隐式地提交前边语句所属于的事务
1
2
3
4
5
6
  • 隐式使用或修改 MySQL 数据库中的表

当我们使用 ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD 等语句时也会隐式地提交前边语句所属于的事务。

  • 事务控制或关于锁定的语句

① 当我们在一个事务还没提交或者回滚时就又使用 start transaction 或者 begin 语句开启了另一个事务时,会隐式地提交上一个事务。即:

begin;

select ... # 事务中的一条语句
update ... # 事务中的一条语句...
# 事务中的其它语句

begin;    # 此语句会隐式地提交前边语句所属于的事务
1
2
3
4
5
6
7

② 当前的 autocommit 系统变量的值为 OFF,我们手动把它调为 ON 时,也会隐式地提交前边语句所属的事务。

③ 使用 LOCK TABLES、UNLOCK TABLES 等关于锁定的语句也会隐式地提交前边语句所属的事务。

  • 加载数据的语句

使用 LOAD DATA 语句来批量往数据库中导入数据时,也会隐式地提交前边语句所属的事务。

  • 关于 MySQL 复制的一些语句

使用 START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO 等语句时会隐式地提交前边语句所属的事务。

  • 其它的一些语句

使用 ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、LOAD INDEX INTO CACHE 、OPTIMIZE TABLE、REPAIR TABLE、 RESET 等语句也会隐式地提交前边语句所属的事务。

# 2.4.使用举例 1:提交与回滚

我们看下在 MySQL 的默认状态下,下面这个事务最后的处理结果是什么。

(1)情况 1

use atguigudb2;
create table user3(name varchar(15) primary key);

begin;
insert into user3 values('张三'); 	# 此时不会自动提交数据
commit;								# 提交事务之后,上面的添加的'张三'这条记录就会写入到磁盘中
						
begin; 								# 开启一个新的事务
insert into user3 values('李四'); 	# 此时不会自动提交数据
insert into user3 values('李四'); 	# 受主键的影响,不能添加成功
rollback;							# 回滚之后,上面添加'李四'的这条记录会被撤回

select * from user3;				# 此时只有'张三'这一条记录
1
2
3
4
5
6
7
8
9
10
11
12
13

image-20240308150717873

(2)情况 2

truncate table user3;  				# 清除 user3 表中的数据,ddl 操作会自动提交数据,不受 autocommit 变量的影响

begin;
insert into user3 values('张三'); 	# 此时不会自动提交数据
commit;

insert into user3 values('李四'); 	# 默认情况下(即 autocommit 为 true),dml 操作也会自动提交数据
insert into user3 values('李四'); 	# 事务的失败的状态

rollback;							# 此时有'张三'、'李四'这两条记录
1
2
3
4
5
6
7
8
9
10

image-20240308151026815

(3)情况 3

truncate table user3;

select @@completion_type;
set @@completion_type = 1;

begin;
insert into user3 values('张三'); 
commit;

insert into user3 values('李四');
insert into user3 values('李四'); 

rollback;							# 此时只有'张三'这一条记录
1
2
3
4
5
6
7
8
9
10
11
12
13

image-20240308151241760

你能看到相同的 SQL 代码,只是在事务开始之前设置了下面的语句,结果就和我们第一次处理的一样,只有“张三”这一条记录。

set @@completion_type = 1
1

这是为什么呢?这里介绍一下 MySQL中 completion_type 参数的作用,实际上这个参数有 3 种可能:

  1. completion = 0,这是默认情况。当我们执行 commit 的时候会提交事务,在执行下一个事务时,还需要使用 start transaction 或者 begin 来开启。
  2. completion = 1,这种情况下,当我们提交事务后,相当于执行了 commit and chain,也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务。
  3. completion = 2,这种情况下 commit = commit and release,也就是当我们提交后,会自动与服务器断开连接。

知识点总结

  • 当我们设置 autocommit = 0 时,不论是否采用 start transaction 或者 begin 的方式来开启事务,都需要用 commit 进行提交,让事务生效,使用 rollback 对事务进行回滚。
  • 当我们设置 autocommit = 1 时,每条 sql 语句都会自动进行提交。不过这时,如果你采用 start transaction 或者 begin 的方式来显式地开启事务,那么这个事务只有在 commit 时才会生效,在 rollback 时才会回滚。

# 2.5.使用举例 2:测试不支持事务的 Engine

# 举例2:体会 innodb 和 myisam 的区别
create table test1(i int) engine = innodb;
create table test2(i int) engine = myisam;

begin;
insert into test1 values (1);
rollback;

select * from test1;			# 回滚后没有记录

# 针对于 myisam 表:不支持事务
begin;
insert into test2 values (1);
rollback;

select * from test2;			# 回滚后仍然有记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image-20240308154145485

# 2.6.使用举例 3:savepoint

use atguigudb3;

create table user3(name varchar(15),balance decimal(10, 2));

begin;
insert into user3(name,balance) values('张三', 1000);
commit;

select * from user3;

begin;
update user3 set balance = balance - 100 where name = '张三';
update user3 set balance = balance - 100 where name = '张三';

savepoint s1;			# 设置保存点

update user3 set balance = balance + 1 where name = '张三';

rollback to s1;     # 回滚到保存点 s1,此时张三的余额为 800

select * from user3;

rollback;         # 事务只是到了保存点 s1,但是还未提交,此时进行回滚操作,张三的余额会变为初始的 1000

select * from user3;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

image-20240308160205837

image-20240308160400752


# 3.事务隔离级别

MySQL 是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session )。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。事务有隔离性的特性,理论上在某个事务 某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,那就看二者如何权衡取舍了。

# 3.1.数据准备

# 创建 student 表
create table student ( 
studentno int, 
name varchar(20), 
class varchar(20), 
primary key (studentno) 
) engine=innodb charset=utf8;

# 插入一条数据
insert into student values(1, '小谷', '1班');

# 查看当前表的所有数据
select * from student;

mysql> select * from student; 
+-----------+--------+-------+
| studentno | name 	 | class | 
+-----------+--------+-------+
| 1 		| 小谷 	 | 1班   | 
+-----------+--------+-------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.2.数据并发问题

针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在不保证串行执行(也就是执行完一个再执行另一个)的情况下可能会出现哪些问题。

# 3.2.1.脏写 (Dirty Write)

对于两个事务 Session A、Session B,如果 Session A 修改了另一个未提交事务 Session B 修改过的数据,那就意味着发生了脏写。

  • 脏写(Dirty Write) 是指当一个事务写入了某些数据,而这些数据在该事务提交前被另一个事务覆写的情况。脏写的发生意味着一个事务可以覆盖另一个事务的修改,而不考虑第一个事务是否已经提交或回滚,这会导致数据的不一致性和丢失重要的修改。

  • 通过锁机制和事务隔离级别来阻止脏写的发生。当一个事务正在修改某些数据时,这些数据会被加锁,直到事务提交或回滚,其他事务就无法修改这些正在被第一个事务修改的数据。因此,在正常操作下,脏写在这些系统中是不会发生的。

在这里插入图片描述 Session A 和 session B 各开启了一个事务,Session B 中的事务先将 studentno 列为 1 的记录的 name 列更新为’李四’,然后 Session A 中的事务接着又把这条 studentno 列为 1 的记录的 name 列更新为’张三’。如果之后 Session B 中的事务进行了回滚,那么 Session A 中的更新也将不复存在,这种现象就称之为脏写。这时 Session A 中的事务就没有效果了,明明把数据更新了,最后也提交事务了,最后看到的数据什么变化也没有。这里大家对事务的隔离级比较了解的话,会发现默认隔离级别下,上面 SessionA 中的更新语句会处于等待状态。

# 3.2.1.脏读 (Dirty Read)

(1)对于两个事务 Session A、Session B,Session A 读取了已经被 Session B 更新但还没有被提交的字段。之后若 Session B 回滚 ,Session A 读取的内容就是临时且无效的。

脏读发生在一个事务读取了另一个事务未提交的数据。如果这个未提交的数据被回滚(Rollback),那么第一个事务读到的数据就是“脏”的,因为它读到了实际上从未被提交的数据。

举例:事务A修改了一条记录,但未提交;事务B在此时读取了同一记录,读到了A的修改。如果A回滚,B读到的数据就是脏数据。

在这里插入图片描述

(2)Session A 和 Session B 各开启了一个事务,Session B 中的事务先将 studentno 列为 1 的记录的 name 列更新为’张三’,然后 Session A 中的事务再去查询这条 studentno 为 1 的记录,如果读到列 name 的值为’张三’,而 Session B 中的事务稍后进行了回滚,那么Session A 中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读 。

# 3.2.1.不可重复读 (Non-Repeatable Read)

(1)对于两个事务 Session A、Session B,Session A 读取了一个字段,然后 Session B 更新了该字段。 之后 Session A 再次读取同一个字段, 值就不同了。那就意味着发生了不可重复读。

不可重复读描述的是在同一个事务内,多次读取同一数据集合时,由于其他事务的修改提交,导致两次读取的结果不一致的情况。这主要是因为其他事务在此期间对这些数据进行了更新并提交。

举例:事务A读取了一条记录,事务B更新了这条记录并提交;当事务A再次读取同一记录时,发现记录已经改变。这里,事务A两次读到的数据不一致,发生了不可重复读。

在这里插入图片描述

(2)我们在 Session B 中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了 studentno 列为 1 的记录的列 name 的值,每次事务提交之后,如果 Session A 中的事务都可以查看到最新的值,这种现象也被称之为不可重复读。

# 3.2.1.幻读 (Phantom)

(1)对于两个事务 Session A、Session B, Session A 从一个表中读取了一个字段,然后 Session B 在该表中插入了一些新的行。 之后,如果 Session A 再次读取同一个表,就会多出几行。那就意味着发生了幻读。

幻读与不可重复读类似,但它指的是在一个事务内,执行两次相同的查询,第二次查询结果中出现了第一次查询中未出现的数据行。这通常是因为另一个并发事务在这两次查询之间插入了新的数据行。

举例:事务A根据某一条件查询出一组记录,事务B在此期间插入了一些新的记录,这些新记录符合A的查询条件。当事务A再次执行相同的查询时,会发现有之前未见过的“幻”记录。

在这里插入图片描述

(2)Session A 中的事务先根据条件 studentno > 0 这个条件查询表 student,得到了 name 列值为’张三’的记录;之后 Session B 中提交了一个隐式事务,该事务向表 student 中插入了一条新记录;之后 Session A 中的事务再根据相同的条件 studentno > 0 查询表 student,得到的结果集中包含 Session B 中的事务新插入的那条记录,这种现象也被称之为幻读。我们把新插入的那些记录称之为幻影记录。

  • 注意 1:有的读者可能会有疑问,那如果 Session B 中删除了一些符合 studentno > 0 的记录而不是插入新记录,那 Session A 之后再根据studentno > 0 的条件读取的记录变少了,这种现象算不算幻读呢?这种现象不属于幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。

  • 注意 2:那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。

# 3.3.SQL 中的四种隔离级别

上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:

脏写 > 脏读 > 不可重复读 > 幻读

我们愿意舍弃一部分隔离性来换取一部分性能,在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。 SQL标准中设立了 4 个隔离级别:

read uncommitted :读未提交,在该隔离级别,所有事务都可以看到 其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。

read committed :读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。

repeatable read :可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。

serializable :可串行化,确保事务可以从一个表中读取相同的行。在这个事务续期间,禁止 其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。

SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:

在这里插入图片描述

脏写怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4 种事务隔离级别与并发性能的关系如下:

在这里插入图片描述

# 3.4.MySQL 支持的四种隔离级别

不同的数据库厂商对 SQL 标准中规定的四种隔离级别支持不一样。比如,Oracle 就只支持 read comnitted(默认隔离级别)和 serializable 隔离级别。MySQL 虽然支持 4 种隔离级别,但与 SQL 标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL 在 repeatable read 隔离级别下,是可以禁止幻读问题的发生的,禁止幻读的原因在多版本并发控制中会介绍。

MySQL 的默认隔离级别为 repeatable read,我们可以手动修改一下事务的隔离级别。

# 查看隔离级别,MySQL 5.7.20 的版本之前:
mysql> SHOW VARIABLES LIKE 'tx_isolation'; 
+---------------+-----------------+
| Variable_name | Value 		  | 
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ | 
+---------------+-----------------+
1 row in set (0.00 sec) 

# MySQL 5.7.20版本之后,引入 transaction_isolation 来替换 tx_isolation 
# 查看隔离级别,MySQL 5.7.20 的版本及之后: 
mysql> SHOW VARIABLES LIKE 'transaction_isolation'; 
+-----------------------+-----------------+
| Variable_name 	    | Value 		  | 
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ | 
+-----------------------+-----------------+
1 row in set (0.02 sec) 

# 或者不同 MySQL 版本中都可以使用的:
SELECT @@transaction_isolation;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.5.如何设置事务的隔离级别

(1)通过下面的语句修改事务的隔离级别:

set [global|session] transaction isolation level 隔离级别; 
# 其中隔离级别格式: 
> read uncommitted 
> read committed 
> repeatable read 
> serializable
1
2
3
4
5
6

或者:

set [global|session] transaction_isolation = '隔离级别' 
# 其中隔离级别格式: 
> read-uncommitted 
> read-committed 
> repeatable-read 
> serializable
1
2
3
4
5
6

(2)关于设置时使用 GLOBAL 或 SESSION 的影响:

  • ① 使用 GLOBAL 关键字(在全局范围影响):
set global transaction isolation level serializable; 
# 或 
set global transaction_isolation = 'serializable';
1
2
3

当前已经存在的会话无效,如下图所示:

image-20240308232120154

并且只对执行完该语句之后产生的会话起作用

  • ② 使用 SESSION 关键字(在会话范围影响):
set session transaction isolation level serializable; 
#或
set session transaction_isolation = 'serializable';
1
2
3
  1. 对当前会话的所有后续的事务有效;
  2. 如果在事务之间执行,则对后续的事务有效;
  3. 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务;

image-20240308231849903

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

知识点小结

数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

# 3.6.不同隔离级别举例

# 3.6.1.数据准备

use atguigudb3;

CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(15),
balance DECIMAL(10, 2)
);

INSERT INTO account(NAME, balance) VALUES ('张三', 100), ('李四', 0);
1
2
3
4
5
6
7
8
9

# 3.6.2.读未提交之脏读问题

打开两个会话,均设置隔离级别为读未提交:

set session transaction_isolation = 'read-uncommitted';
1

image-20240309011200799

脏读引发的另一种情况如下:

在这里插入图片描述

为什么会阻塞?

当事务1修改了一条数据但还未提交时,尽管它的修改在READ UNCOMMITTED级别下对其他事务是可见的,MySQL仍然会对这条数据加上排他锁(X锁)。排他锁确保了在事务1提交或回滚之前,其他任何事务都不能修改这条数据。这是因为,不管隔离级别如何,写操作都需要保证数据的完整性和一致性。如果允许多个事务同时修改同一条数据,将很难保证这一点。

# 3.6.3.读已提交解决脏读问题

# 仍然使用上面的 account 表,在演示本例之前,先恢复初始的数据
truncate account;

insert into account(name, balance) values ('张三', 100), ('李四', 0);

# 两个会话均设置隔离级别为读已提交
set session transaction_isolation = 'read-committed';
1
2
3
4
5
6
7

下载 (1)

# 3.6.4.可重复读解决不可重复读问题

MVCC解决了不可重复读问题

可重复读问题指的是在一个事务中,多次读取同一数据集合时,中间如果有其他事务修改了这些数据,当前事务的后续读取会看到这些变化,这违背了可重复读的定义。MVCC通过为每个事务提供一个数据的一致性快照来解决这个问题。在REPEATABLE READ隔离级别下,事务第一次读取数据时,InnoDB会创建一个快照,之后该事务的所有读取操作都会访问这个快照,因此即使其他事务提交了更新,当前事务也不会看到这些变化。

# 在上面数据的基础上,两个会话均设置隔离级别为可重复读
set session transaction_isolation = 'repeatable-read';
1
2

image-20240309063134076

# 3.6.5.可重复读存在的幻读问题

# 两个会话均设置隔离级别为可重复读
set session transaction_isolation = 'repeatable-read';
1
2

image-20240309065703244

MySQL 的幻读并非什么读取两次返回结果集不同,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。


# 4.事务的常见分类

从事务理论的角度来看,可以把事务分为以下几种类型:

# 4.1.扁平事务 (Flat Transactions)

  • 扁平事务是事务类型中最简单的一种,但是在实际生产环境中,这可能是使用最频繁的事务,在扁平事务中,所有操作都处于同一层次,其由 BEGIN WORK 开始,由 COMMIT WORK 或 ROLLBACK WORK 结束,其间的操作是原子的,要么都执行,要么都回滚,因此,扁平事务是应用程序成为原子操作的基本组成模块。扁平事务虽然简单,但是在实际环境中使用最为频繁,也正因为其简单,使用频繁,故每个数据库系统都实现了对扁平事务的支持。扁平事务的主要限制是不能提交或者回滚事务的某一部分,或分几个步骤提交。

  • 扁平事务一般有三种不同的结果:

  1. 事务成功完成。在平常应用中约占所有事务的 96%。
  2. 应用程序要求停止事务。比如应用程序在捕获到异常时会回滚事务,约占事务的 3%。
  3. 外界因素强制终止事务。如连接超时或连接断开,约占所有事务的 1%。

# 4.2.带有保存点的扁平事务 (Flat Transactions with Savepoints)

带有保存点的扁平事务除了支持扁平事务支持的操作外,还允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销太大。

保存点 (Savepoint) 用来通知事务系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。对于扁平的事务来说,隐式的设置了一个保存点,然而在整个事务中,只有这一个保存点,因此,回滚只能会滚到事务开始时的状态。

# 4.3.链事务 (Chained Transactions)

1、链事务是指一个事务由多个子事务链式组成,它可以被视为保存点模式的一个变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。

2、链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务,前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。其工作方式如下:

在这里插入图片描述

3、链事务与带有保存点的扁平事务的不同之处体现在:

  • 带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。
  • 对于锁的处理,两者也不相同,链事务在执行 COMMIT 后即释放了当前所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

# 4.4.嵌套事务 (Nested Transactions)

嵌套事务是一个层次结构框架,由一个顶层事务 (Top-Level Transaction) 控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务 (Subtransaction),其控制着每一个局部的变换,子事务本身也可以是嵌套事务。因此,嵌套事务的层次结构可以看成是一棵树。

# 4.5.分布式事务 (Distributed Transactions)

分布式事务通常是在一个分布式环境下运行的扁平事务,因此,需要根据数据所在位置访问网络中不同节点的数据库资源。例如,一个银行用户从招商银行的账户向工商银行的账户转账 1000 元,这里需要用到分布式事务,因为不能仅调用某一家银行的数据库就完成任务。

公众号封面

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
第12章_数据库调优策略
第14章_MySQL事务日志

← 第12章_数据库调优策略 第14章_MySQL事务日志→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式