MySQL中的“事务”

目录

一、概念

二、事务的ACID特性

[2.1 A 原子性](#2.1 A 原子性)

[2.2 C 一致性](#2.2 C 一致性)

[2.3 I 隔离性](#2.3 I 隔离性)

[2.4 D 持久性](#2.4 D 持久性)

三、使用事务

[3.1 查看支持事务的存储引擎](#3.1 查看支持事务的存储引擎)

[3.2 使用事务流程](#3.2 使用事务流程)

四、使用事务示例

[4.1 开启一个事务,执行修改后回滚](#4.1 开启一个事务,执行修改后回滚)

[4.2 开启一个事务,执行修改后提交](#4.2 开启一个事务,执行修改后提交)

五、保存点

[六、自动 / 手动提交事务](#六、自动 / 手动提交事务)

七、事务的隔离级别

[7.1 隔离级别](#7.1 隔离级别)

[7.2 查看和设置隔离级别](#7.2 查看和设置隔离级别)

[7.2.1 设置全局作用域](#7.2.1 设置全局作用域)

[7.2.2 设置会话作用域](#7.2.2 设置会话作用域)

[7.2.3 无作用域指定](#7.2.3 无作用域指定)

[7.2.4 设置隔离级别综合示例比较](#7.2.4 设置隔离级别综合示例比较)

[7.3 不同隔离级别存在的问题](#7.3 不同隔离级别存在的问题)

[7.3.1 read uncommit - 读未提交与脏读](#7.3.1 read uncommit - 读未提交与脏读)

[7.3.2 read committed - 读已提交与不可重复读](#7.3.2 read committed - 读已提交与不可重复读)

[7.3.3 repeatable read - 可重复读与幻读](#7.3.3 repeatable read - 可重复读与幻读)

[7.3.4 serializable - 串行化](#7.3.4 serializable - 串行化)

[7.4 不同隔离级别的性能与安全](#7.4 不同隔离级别的性能与安全)


一、概念

事务把一组SQL语句打包成为一个集合,在这组SQL的执行过程中,要么全部成功,要么全部失败。这组SQL语句可以只有一条,也可以是多条。

事务是按照一定逻辑顺序执行的任务序列,既可以由用户手动执行,也可以由某种数据库程序自动执行,在MySQL中默认开启事务。


如,张三和李四各自的账户中都有10000块。张三要向李四转账1000,如果在转账的中途出现网络异常/黑客入侵/服务器宕机等突发情况,有可能出现张三的账款减少,但李四的账款没有增加的情况。而如果开启事务,就能避免上述的情况。

如果成功转账,应该是下面的结果:

1、张三账户余额减少1000,变成9000,李四账户余额变成11000;------>对应 原子性

2、张三和李四两人账户总金额不变,原来是20000,转账后还是20000;------> 对应 一致性

3、转账处理过程中张三和李四的余额不能因其他的转账事件而受到干扰;------> 对应 隔离性

4、转账后的余额结果保存到存储介质,方便以后读取。------> 对应 持久性

二、事务的ACID特性

Atomicity**(原子性)、Consistency一致性)、Isolation隔离性)、Durability持久性)**

2.1 A 原子性

一个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执行了一半的情况。如果在执行过程中发生错误,系统会自动回滚Rollback)到事务开始前的状态;

2.2 C 一致性

在事务开始之前和事务结束之后,数据状态保持一致。这表示写入的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服务器奔溃后如何恢复。

2.3 I 隔离性

事务在并发执行时,相互之间不会干扰。MySQL 通过锁机制来保证隔离性,防止多个事务同时修改同一数据,导致数据不一致。事务可以指定不同的隔离级别,以权衡在不同应用场景下数据库的性能和安全。

2.4 D 持久性

事务处理结束后,对数据的修改将永久的写入存储介质,即便系统故障也不会丢失。

三、使用事务

3.1 查看支持事务的存储引擎

3.2 使用事务流程

1、START TRANSACTION 或 BEGIN 开始⼀个新的事务;

2、执行必要的数据库操作;

3、COMMIT 提交当前事务,确保操作成功并对更改持久化保存;

4、ROLLBACK 回滚当前事务,在发生错误时取消其更改;

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

四、使用事务示例

背景:

sql 复制代码
# 创建账户表
mysql> create table bank_account(
    -> id bigint primary key auto_increment,
    -> name varchar(255) not null,
    -> balance decimal(10,2) not null
    -> );
Query OK, 0 rows affected (0.26 sec)


# 插入张三的初始账户余额
mysql> insert into bank_account(name, balance) values('张三',10000);
Query OK, 1 row affected (0.08 sec)

# 插入李四的初始账户余额
mysql> insert into bank_account(name, balance) values('李四',10000);
Query OK, 1 row affected (0.06 sec)

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

sql 复制代码
# 开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)


# 查看账户表中的原始数据,张三、李四初始余额都是1万
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   | 10000.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 张三余额减少1000
mysql> update bank_account set balance = balance -1000 where name = '张三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0


# 李四余额增加1000
mysql> update bank_account set balance = balance +1000 where name = '李四';
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0


# 查询修改表中的数据,两人的余额都被修改
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  9000.00 |
|  2 | 李四   | 11000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 回滚事务
mysql> rollback;
Query OK, 0 rows affected (0.06 sec)


# 再次查询表中数据,发现之前的修改没有生效
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   | 10000.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

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

sql 复制代码
# 开启事务
mysql> start transaction;;
Query OK, 0 rows affected (0.05 sec)


# 张三余额减少1000
mysql> update bank_account set balance = balance -1000 where name = '张三';
Query OK, 1 row affected (0.07 sec)
Rows matched: 1  Changed: 1  Warnings: 0


# 李四余额增加1000
mysql> update bank_account set balance = balance +1000 where name = '李四';
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0


# 修改后立即查看表中的数据,余额已被修改
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  9000.00 |
|  2 | 李四   | 11000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 提交事务
mysql> commit;
Query OK, 0 rows affected (0.03 sec)


# 再次查看表中的数据
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  9000.00 |
|  2 | 李四   | 11000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

* commit 提交之后再回滚,是无法退回到一开始双方余额都是10000的情况。因为事务一旦提交,数据就落盘了。

但如果想要回到最初的状态则需要在最开始设置"保存点"。

五、保存点

在事务执行的过程中使用 savepoint 指定名称; 设置保存点,再使用 rollback to 保存点名称; 回滚到指定保存点可以把数据恢复到保存点的状态。

sql 复制代码
# 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  9000.00 |
|  2 | 李四   | 11000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 将上一张表设置为保存点1
mysql> savepoint point1;
Query OK, 0 rows affected (0.05 sec)

# 修改数据
mysql> update bank_account set balance = balance -1000 where name = '张三';
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update bank_account set balance = balance +1000 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.01 sec)


# 回滚到保存点1
mysql> rollback to point1;
Query OK, 0 rows affected (0.06 sec)

mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  9000.00 |
|  2 | 李四   | 11000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

# 再次修改数据
mysql> update bank_account set balance = balance -1000 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update bank_account set balance = balance +1000 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 设置保存点2
mysql> savepoint point2;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 向表中插入数据
mysql> insert into bank_account (name,balance) values ('王五', 10000);
Query OK, 1 row affected (0.06 sec)


# 插入成功
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  8000.00 |
|  2 | 李四   | 12000.00 |
|  3 | 王五   | 10000.00 |
+----+--------+----------+
3 rows in set (0.00 sec)


# 回滚到保存点2
mysql> rollback to point2;
Query OK, 0 rows affected (0.00 sec)

# 上一条插入语句失效
mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  8000.00 |
|  2 | 李四   | 12000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)


# 回滚到本例最开始的数据
mysql> rollback to point1;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from bank_account;
+----+--------+----------+
| id | name   | balance  |
+----+--------+----------+
|  1 | 张三   |  9000.00 |
|  2 | 李四   | 11000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

直接 rollback;rollback to 保存点; 的区别:

六、自动 / 手动提交事务

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

show variables like 'autocommit'; 查看当前事务是否为自动提交,on 表示开启"自动提交"

sql 复制代码
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set, 1 warning (0.12 sec)

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

sql 复制代码
# 设置事务自动提交
mysql> set autocommit = 0;  # 方式一
Query OK, 0 rows affected (0.05 sec)

mysql> set autocommit = on; # 方式二
Query OK, 0 rows affected (0.00 sec)


# 设置事务手动提交
mysql> set autocommit = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> set autocommit = off;
Query OK, 0 rows affected (0.00 sec)

注意:

  • 只要使用 start transaction 或 begin 开启事务,必须通过 commit 提交数据才会持久化,与是否设置 set autocommit 无关;
  • 手动提交模式下,不用显示开启事务,执行修改操作后,提交或回滚事务时直接用 commit 或 rollback
  • 即使当前设置为手动提交,重启程序之后还是会恢复为自动提交

七、事务的隔离级别

上文中我们知道了什么是隔离性,那么如何实现事务之间的隔离?隔离到什么程度?如何保证数据安全的同时也要兼顾性能?这是需要考虑的问题。

7.1 隔离级别

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

7.2 查看和设置隔离级别

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

sql 复制代码
# 查看全局作用域
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+
1 row in set (0.00 sec)


# 会话作用域
mysql> select @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ                 |
+---------------------------------+
1 row in set (0.00 sec)

* 可以看到默认 的事务隔离级别是 REPEATABLE-READ "可重复读"

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

7.2.1 设置全局作用域

作用范围:影响之后所有新的会话连接,对当前已存在的会话无效。

示例:

sql 复制代码
-- 会话A(管理员)
-- 设置全局事务隔离级别为串行化
transaction isolation level sertalizable;
-- 或
set global transaction isolation level serializable;

-- 会话B(已存在的会话)不受影响,仍使用之前的隔离级别

-- 会话C(新建立的连接)自动使用serializable隔离级别

7.2.2 设置会话作用域

作用范围:只影响当前会话中后续的所有事务,不影响其他会话。

示例:

sql 复制代码
-- 会话A
-- 当前隔离级别:repeatable read (可重复读)

-- 设置会话隔离级别为串行化
set session transaction isolation level serializable;

-- 事务1(使用serializable)
start transaction;
select * from bank_account;
commit;

-- 事务2(仍使用serializable)
start transaction;
update bank_account set balance = balance + 1000 where id = 1;
commit;

-- 其余打开的会话完全不受影响,使用默认隔离级别

7.2.3 无作用域指定

作用范围:只影响紧接者的下一个事务,之后的事务恢复之前的隔离级别。

示例:

sql 复制代码
-- 会话A
-- 当前隔离级别:repeatable read (可重复读)

-- 设置仅下一个事务使用 serializable
set transaction isolation level serializable;

-- 事务1(使用serializable)
start transaction;
select * from bank_account where balance > 10000;  -- 需要严格一致性检查
commit;

-- 事务2(自动恢复为 repeatable read)
start transaction;
update bank_account set balance = balance - 1000 where id = 1;
commit;

7.2.4 设置隔离级别综合示例比较

sql 复制代码
-- 假设默认隔离级别是 REPEATABLE READ

-- 情况1:全局设置(需要管理员权限)
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 影响:所有新建立的连接都会使用SERIALIZABLE

-- 情况2:会话设置
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 影响:当前会话中所有后续事务都使用SERIALIZABLE

-- 情况3:仅下一个事务
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 影响:只有紧接着的下一个事务使用SERIALIZABLE
START TRANSACTION;
-- 串行化事务操作...
COMMIT;
-- 后续事务恢复为REPEATABLE READ

实际使用场景:

  1. 全局设置:当整个应用都需要最高级别的一致性保证时;
  2. 会话设置:当某个用户会话需要执行一系列关键操作时;
  3. 仅下一个事务:当只有某个特定操作需要严格一致性,其他操作可以使用较低隔离性别提升性能时。

7.3 不同隔离级别存在的问题

7.3.1 read uncommit - 读未提交与脏读

存在问题:

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

* 正常业务中应该避免使用 read uncommitted 读未提交这种隔离级别。

7.3.2 read committed - 读已提交与不可重复读

存在问题:

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

7.3.3 repeatable read - 可重复读与幻读

存在问题:

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

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


不可重复读和幻读的核心区别:不可重复读是针对同一行数据的修改,幻读则是针对数据集中行的数量的变化(新增或者删除)。

不可重复读示例:

1、在一个客户端A中先设置全局事务隔离级别为 read committed 读已提交:

sql 复制代码
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+
1 row in set (0.00 sec)

2、打开另一个客户端B并确认隔离级别

3、在不同的客户端中执行事务:

幻读示例:

由于 repeatable read 隔离级别在 MySQL 中默认使用了 Next-Key 锁,所以在设置隔离级别为 repeatable read 的时候是无法看到幻读的。因此我们把隔离级别回退到更新时只加了排他锁的 read committed:

1、在一个客户端A中先设置全局事务隔离级别为 read committed 读已提交:

sql 复制代码
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+
1 row in set (0.00 sec)

2、打开另一个客户端B并确认隔离级别

3、在不同的客户端中执行事务:

7.3.4 serializable - 串行化

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

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

InnoDB 存储引擎事务隔离性以及相关的隔离级别是由锁和 MVCC 机制配合实现的。

相关推荐
xmjd msup15 小时前
mysql的分区表
数据库·mysql
Lyyaoo.15 小时前
【JAVA Spring面经】Spring 事务失效情况
java·数据库·spring
MeAT ITEM15 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
dovens15 小时前
PostgreSQL 中进行数据导入和导出
大数据·数据库·postgresql
IOT.FIVE.NO.115 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库
Rick199316 小时前
mysql 慢查询怎么快速定位
android·数据库·mysql
科技小花1 天前
全球化深水区,数据治理成为企业出海 “核心竞争力”
大数据·数据库·人工智能·数据治理·数据中台·全球化
X56611 天前
如何在 Laravel 中正确保存嵌套动态表单数据(主服务与子服务)
jvm·数据库·python
虹科网络安全1 天前
艾体宝干货|数据复制详解:类型、原理与适用场景
java·开发语言·数据库
2301_771717211 天前
解决mysql报错:1406, Data too long for column
android·数据库·mysql