【MySQL】事务

事务把⼀组SQL语句打包成为⼀个整体,在这组SQL的执行过程中,要么全部成功,要么全部失 败。这组SQL语句可以是⼀条也可以是多条。来看⼀个转账的例子,如图:

在这个例子中,涉及了两条更新语句:

账户表以及表中的数据:

更新操作:

如果转账成功,应该有以下结果

  1. 张三的账户余额减少 100 ,变成 900 ,李四的账⼾余额增加了100 ,变成1100 ,不能出现张三的余额减少而李四的余额没有增加的情况;
  2. 张三和李四在发生转账前后的总额不变,也就是说转账前张三和李四的余额总数为 1000+1000=2000 ,转账后他们的余额总数为 900+1100=2000 ;
  3. 转账后的余额结果应当保存到存储介质中,以便以后读取;
  4. 还有⼀点需要要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰;

以上这四点在事务的整个执行过程中必须要得到保证,这也就是事务的 ACID 特性。

1.事务的ACID特性

事务的ACID特性指的是 Atomicity (原⼦性)Consistency (⼀致性)Isolation (隔离性)Durability (持久性)

  • Atomicity (原子性):⼀个事务中的所有操作,++要么全部成功,要么全部失败++ ,不会出现只执行了⼀半的情况,如果事务在执行过程中++发生错误,会回滚( Rollback )到事务开始前的状态++,就像这个事务从来没有执行过⼀样;
  • Consistency (⼀致性):在事++务开始之前和事务结束以后,数据库的完整性不会被破坏++ 。这表 示写⼊的数据必须完全符合所有的预设规则(++事务执行完成之后,保证数据正确且符合预期++),包括数据的精度、关联性以及关于事务执行过程中服务器崩溃后如何恢复;
  • Isolation (隔离性):数据库++允许多个并发事务同时对数据进行读写和修改++ ,隔离性可以防止多 个事务并发执行时由于交叉执行而导致数据的不⼀致(++多个事务之间不能相互影响++)。事务可以指定不同的隔离级别,以权衡在不同的应用场景下数据库性能和安全;
  • Durability (持久性):事务处理结束后,对数据的修改将永久的写入存储介质,即便系统故障 也不会丢失。

事务最终要保证一致性:一致性是通过原子性、隔离性和持久性来实现。

(事务是保证数据安全的,索引是提升查询效率的)

为什么使用事务

事务具备的ACID特性,是我们使用事务的原因,在我们日常的业务场景中有大量的需求要同事务 来保证。支持事务的数据库能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,在使用事务过程中,要么提交,要么回滚,不⽤去考虑网络异常,服务器宕机等其他因素,因此,我们经常接触的事务本质上是数据库对 ACID 模型的⼀个实现,是为应用层服务的。

也就是说,在使用数据库的过程中,对于修改只要提交成功,就可以安全的保存,只要回滚就可以恢复到事务开始之初。

2.使用事务

要使用事务那么数据库就要支持事务,在MySQL中支持事务的存储引擎是InnoDB,可以通过 show engines; 语句查看:

语法

-- 开启事务

start transaction;

或者

begin;

-- 提交当前的事务并对更改持久化保存

commit;

-- 回滚当前事务,取消更改

rollback;

  • 无论提交还是回滚,事务都会关闭。

示例

开启一个事务,执行修改后回滚

首先开启事务,然后在事务中执行操作:

最后让其回滚,回到事务开始之初 / 修改被撤销(回滚执行后也代表着事务关闭):

开启一个事务,执行修改后提交

前面还是一样的操作,只是在最后时执行的是提交(提交也代表着事务关闭):

为了验证提交后,数据被持久化保存了,可以在打开一个命令行/会话去查询:

# 保存点

在事务执行的过程中设置保存点,回滚时指定保存点可以把数据恢复到保存点的状态。

语法

-- 设置保存点

savepoint 保存点名称;

-- 回滚到指定的保存点

rollback to 保存点名称;

  • 注意 rollback to 和 rollback 的区别:如果只是 rollback,那么数据就会回滚到事务最开始的状态,同时关闭事务;而 rollback to 是在事务中回滚到其中一个指定的保存点的位置。

示例

在事务中设置两个保存点,并回滚到第二个保存点

在设置完第二个保存点之后,往表中添加一行数据:

但是此时不想要这一记录了,可以回滚到保存点2,回到添加记录前,最后可以回滚,关闭事务,回到最初状态:

# 自动/手动提交事务

默认情况下,MySQL是自动提交事务的,也就是说我们执行的每个修改操作,比如插入、更新和删除,都会自动开启⼀个事务并在语句执行完成之后自动提交,发生异常时自动回滚。

也就是说,当不开启事务时,例如执行一条插入语句,该语句成功执行后自动提交,事务就关闭,且该事务中的内容就只有这一条语句:

当输出错误时,例如语法出错时,那么会执行失败,即会回滚,然后事务关闭:

以上就是默认情况下,MySQL事务提交的方式,即手动。我们可以通过

show variables like 'autocommit'; 语句 查看当前事务是否为自动提交

  • NO 表示自动提交开启(每个 SQL 语句后自动提交事务)。
  • OFF 表示自动提交关闭(需要手动执行 COMMIT 提交事务)。

可以通过以下语句设置事务为自动或手动提交

-- 设置事务自动提交

set autocommit = 1; # 方式1

set autocommit = NO; # 方式2

-- 设置事务手动提交

set autocommit = 1; # 方式1

set autocommit = OFF; # 方式2

注意:

  1. 只要使用 start transaction 或 begin 开启事务,必须要通过 COMMIT 提交才会持久化,与是否设置 set autocommit 无关。
  2. 手动提交模式下,不用显示开启事务,执行修改操作后,提交或回滚事务时直接使用 commit 或 rollback,才能持久化更改。
  3. 在自动提交模式下,一个事务只包含一条DML(增、删、改操作)语句。
  4. 已提交的事务不能回滚。

示例

当设置为手动提交时,如果没有执行 commit 或者 rollback,数据是不会自动保存的:

必须显示执行 commit 或者 rollback ,才能持久化保存,:

注意:如果我们设置了手动提交,但是在重启电脑之后又会变回自动提交,如果想要永久修改成手动提交,那么可以修改配置文件。

3.事务的隔离性和隔离级别

MySQL服务可以同时被多个客户端访问,每个客户端执行的DML语句以事务为基本单位,那么不 同的客户端在对同⼀张表中的同⼀条数据进行修改的时候就可能出现相互影响的情况,为了保证不同的事务之间在执行的过程中不受影响,那么事务之间就需要要相互隔离,这种特性就是隔离性

隔离级别

事务间不同程度的隔离,称为事务的隔离级别;不同的隔离级别在性能和安全方面做了取舍,有 的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中;在MySQL的InnoDB引擎中事务的隔离级别有四种,分别是:

  1. READ UNCOMMITTED ,读未提交
  2. READ COMMITTED ,读已提交
  3. REPEATABLE READ ,可重复读(默认)
  4. SERIALIZABLE ,串行化

以上四种级别从上到下安全性越来越高,性能越来越低。

查看和设置隔离级别

事务的隔离级别分为全局作用域和会话作用域,查看不同作用域事务的隔离级别,可以使用以下的 方式:

全局作用域 (查看全局默认隔离级别)

SELECT @@GLOBAL.transaction_isolation;

会话作用域 (查看当前会话的默认隔离级别(即未来事务将使用的级别))

SELECT @@SESSION.transaction_isolation;

查看当前事务实际使用的隔离级别(如果当前有事务)

SELECT @@transaction_isolation;

  • @@:表示查看系统变量。@@GLOBAL:全局系统变量,所有会话生效。@@SESSION:当前会话生效。
  • 全局与会话的意思与区别如以下举例:
  • 如下,未设置隔离级别时,都是默认值:

设置事务的隔离级别和访问模式,可以使用以下语法

sql 复制代码
# 通过GLOBAL|SESSION分别指定不同作⽤域的事务隔离级别
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode;

# 隔离级别
level: {
 REPEATABLE READ # 可重复读
 | READ COMMITTED # 读已提交
 | READ UNCOMMITTED # 读未提交
 | SERIALIZABLE # 串⾏化
}

# 访问模式
access_mode: {
 READ WRITE # 表⽰事务可以对数据进⾏读写
 | READ ONLY # 表⽰事务是只读,不能对数据进⾏读写
}

示例

sql 复制代码
set global transaction isolation level serializable;
# 或者
set global transaction_isolation = 'SERIALIZABLE'; -- 注意使⽤SET语法时有空格要⽤"-"代替
# 或者
set @@global.transaction_isolation='SERIALIZABLE';
  • 设置全局事务隔离级别为串行化,后续所有事务生效,不影响当前事务
sql 复制代码
set session transaction isolation level serializable;
# 或者
set session transaction_isolation = 'REPEATABLE-READ'; -- 注意使⽤SET语法时有空格要⽤"-"代替
# 或者
set @@session.transaction_isolation='REPEATABLE-READ';
  • 设置会话事务隔离级别为串行化,当前会话后续的所有事务生效,不影响当前事务,可以在任何时候执行。
sql 复制代码
set transaction isolation level serializable;
  • 如果不指定任何作用域,设置只针对下⼀个事务,随后的事务恢复之前的隔离级别。

不同隔离级别存在的问题

(1)READ UNCOMMITTED - 读未提交与脏读

  • 存在的问题:

出现在事务的 READ UNCOMMITTED 隔离级别下,由于在读取数据时不做任何限制,所以并发性能很高,但是会出现大量的数据安全问题,比如在事务A中执行了⼀条 INSERT 语句,在没有执行 COMMIT 的情况下,会在事务B中被读取到,此时如果事务A执行回滚操作,那么事务B中读取到事务A写入的数据将没有意义,我们把这个现象叫做 "脏读" 。

  • 问题重现:

在一个客户端A/会话A中先设置全局事务隔离级别为 READ UNCOMMITTED 读未提交:

打开另一个客户端B并确认生效:

在不同客户端执行事务:首先在客户端A中开启事务A,并在事务A中写入数据:

此时在事务A中,新记录已经写入,但是此时事务A并没有提交。在客户端B中,同样开始一个事务,在该事务中查询account表的结果,发现查到了事务A没有提交的数据:

在客户端A中对事务A进行回滚后,事务回到最初的状态,那么再在客户端B的事务中查询,发现王五的数据记录又不存在了:

以上的现象就是 " 脏读 "。

  • 由于 READ UNCOMMITTED 读未提交会出现"脏读"现象,在正常的业务中出现这种问题会产生非常危重后果,所以正常情况下应该避免使用 READ UNCOMMITTED 读未提交这种的隔离级别

(2)READ COMMITTED - 读已提交与不可重复读

  • 存在的问题:

为了解决脏读问题,可以把事务的隔离级别设置为 READ COMMITTED ,这时事务只能读到了其 他事务提交之后的数据,但会出现不可重复读的问题,比如事务A先对某条数据进行了查询,之后事务 B对这条数据进行了修改,并且提交( COMMIT )事务,事务A再对这条数据进行查询时,得到了事务B 修改之后的结果,这导致了事务A在同⼀个事务中以相同的条件查询得到了不同的值,这个现象要"不可重复读"。

  • 问题重现

在⼀个客户端A中先设置全局事务隔离级别为 READ COMMITTED 读未提交:

打开另一个客户端B并确认隔离级别生效:

现在会话B中写入一条新数据,然后开启事务B,查询表的记录:

然后在会话A中开启事务A,并对新数据进行修改,最后提交事务:

由于会话B中事务B并没提交,再次查询记录时,发现王五的余额变成了1000,与上一次查询的结果不一致:

以上的现象就是 " 不可重复读 "。

注意:读未提交和读已提交的主要区别在于一个事务能否读取到其他事务未提交的修改:

  • 读未提交:事务B可以读取到事务A尚未提交的数据修改,这可能导致脏读(读到可能回滚的中间数据)。
  • 读已提交:事务B只能读取到事务A已经提交的数据修改。但是,如果事务B在事务A提交之前就已开启,那么在事务A提交后,事务B内的后续查询会看到新的已提交数据,从而在同一个事务中可能出现不可重复读(即两次读取同一数据结果不一致)。

(3)REPEATABLE READ - 可重复读与幻读

  • 存在的问题:

为了解决不可重复读问题,可以把事务的隔离级别设置为 REPEATABLE READ ,这时同⼀个事 务中读取的数据在任何时候都是相同的结果,但还会出现⼀个问题,事务A查询了⼀个区间的记录得到结果集A,事务B向这个区间的间隙中写入了⼀条记录并提交,事务A再查询这个区间的结果集时会查 到事务B新写入的记录得到结果集B,两次查询的结果集不⼀致,这个现象就是"幻读"。

(MySQL的InnoDB存储引擎使用了Next-Key锁解决了大部分幻读问题)

  • 问题重现:

由于 REPEATABLE READ 隔离级别默认使用了 Next-Key 锁,++为了重现幻读问量,我们把隔离级回退到更新时只加了排他锁的 READ COMMITTED++

在会话A中设置隔离级别:

在会话B中开启事务B,更新新的王五的余额:

然后回到会话A中,开启事务A,在李四和王五之间的间隙插入一条赵六的记录,然后提交事务A:

在事务B中再次查询结果集,发现比上次查询的结果集多出了其他事务写入的数据:

以上的现象就是 " 幻读 "。

如果我们把全局隔离级别设置回REPEATABLE-READ后,在钱七和王五之间的间隙中插入新数据吴九:

在会话A中将全局事务隔离级别设置为可重复读:

打开会话B确认是否生效:

在会话B中开启事务B,查询表的结果集:

此时在会话A中开启事务A,在钱七和王五之间的间隙中插入新数据吴九,然后提交事务A,但是最后在会话B中事务B再次查询出来的结果集发现并没有吴九的数据记录:

以上的现象说明,在可重复读隔离级别下,通过 Next-Key锁 可防止幻读。

(4)SERIALIZABLE - 串行化

进⼀步提升事务的隔离级别到 SERIALIZABLE (最高级别的隔离),此时所有事务串行执行,可以解决所有并发中的安全问题。

  • 读未提交、读已提交、可重复读:这些隔离级别都允许事务并行执行,只是它们对并发可能产生的问题(脏读、不可重复读、幻读)设置了不同的容忍度。
  • 串行执行:一次只运行一个事务,前一个事务完全结束后,后一个事务才开始。这种方式绝对安全,不会出现任何并发问题(如脏读、不可重复读、幻读),但性能极低。
  • 并行执行:多个事务同时在执行(实际上是交替或并行使用资源),绝大多数数据库在实际运行时都是并行执行事务的,只是隔离级别决定了并行时的"混乱程度"能被容忍到何种地步。

4.不同隔离级别的性能与安全

相关推荐
努力进修2 小时前
智防SQL注入,金仓数据库SQL防护墙筑牢企业数据安全屏障
数据库·sql·安全
Seven972 小时前
MySQL使用全攻略:从命令大全到规范的最佳实践
mysql
布吉岛没有岛_2 小时前
MySQL 的学习
数据库·sql
gp3210262 小时前
mysql配置环境变量——(‘mysql‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件解决办法)
数据库·mysql·adb
SelectDB技术团队2 小时前
PostgreSQL + Apache Doris:构建用于实时分析的 HTAP 架构
数据库·postgresql·架构·实时数仓·湖仓一体·apache doris·selectdb
禁默2 小时前
金仓数据库SQL防火墙构建主动防御,让恶意SQL无处遁形
数据库·金仓
xgstb2 小时前
MySQL root用户密码忘记怎么办(Reset root account password)
数据库·mysql·adb
2401_873587822 小时前
MySQL——索引
数据库·mysql
core5122 小时前
深入浅出 Milvus 向量数据库:从核心原理到 Python 实战指南
数据库·python·milvus·向量数据库·语义检索