事务操作全解析:ACID特性与实战技巧

1. 本节目标

了解事务的概念与使用场景

掌握事务的ACID特性

掌握如何使用事务

掌握事务的隔离性与隔离级别

2. 什么是事务?

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

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

java 复制代码
# ================账户表====================
CREATE TABLE `bank_account` (
 `id` bigint PRIMARY KEY AUTO_INCREMENT,
 `name` varchar(255) NOT NULL, # 姓名
 `balance` decimal(10, 2) NOT NULL # 余额
);
INSERT INTO bank_account(`name`, balance) VALUES('张三', 1000);
INSERT INTO bank_account(`name`, balance) VALUES('李四', 1000);
# ================更新操作===================
# 张三余额减少100
UPDATE bank_account set balance = balance - 100 where name = '张三';
# 李四余额增加100
UPDATE bank_account set balance = balance + 100 where name = '李四';

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

  1. 张三的账户余额减少 100 ,变成 900 ,李四的账户余额增加了 100 ,变成 1100 ,不能出现张 三的余额减少而李四的余额没有增加的情况;

  2. 张三和李四在发生转账前后的总额不变,也就是说转账前张三和李四的余额总数为 1000+1000=2000 ,转账后他们的余额总数为 900+1100=2000

  3. 转账后的余额结果应当保存到存储介质中,以便以后读取

  4. 还有一点需要要注意,在转账的处理过程中张三和李四的余额不能因其他的转账事件而受到干扰

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

3. 事务的ACID特性

事务的ACID特性指的是 Atomicity (原子性), Consistency (一致性), Isolation (隔离 性)和 Durability (持久性)

**Atomicity (原子性):**一个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执 行了一半的情况,如果事务在执行过程中发生错误,会回滚( Rollback )到事务开始前的状 态,就像这个事务从来没有执行过一样

**Consistency (一致性):**在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表 示写入的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执行过程中服 务器崩溃后如何恢复

**Isolation (隔离性):**数据库允许多个并发事务同时对数据进行读写和修改,隔离性可以防止多 个事务并发执行时由于交叉执行而导致数据的不一致。事务可以指定不同的隔离级别,以权衡在不 同的应用场景下数据库性能和安全

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

4. 为什么要使用事务?

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

5. 如何使用事务

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

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

5.2 语法

通过以下语句可以完成对事务的控制

java 复制代码
# 开始一个新的事务
START TRANSACTION;
# 或
BEGIN;
# 提交当前事务,并对更改持久化保存
COMMIT;
# 回滚当前事务,取消其更改
ROLLBACK

START TRANSACTION 或 BEGIN 开始一个新的事务;

COMMIT 提交当前事务,并对更改持久化保存;

ROLLBACK 回滚当前事务,取消其更改

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

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

java 复制代码
# 开启事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
# 在修改之前查看表中的数据
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 张三余额减少100
mysql> UPDATE bank_accountset balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 李四余额增加100
mysql> UPDATE bank_accountset balance = balance + 100 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 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 回滚事务
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
# 再查询发现修改没有生效
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)

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

java 复制代码
# 开启事务
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
# 在修改之前查看表中的数据
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 张三余额减少100
mysql> UPDATE bank_accountset balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 李四余额增加100
mysql> UPDATE bank_accountset balance = balance + 100 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 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 提交事务
mysql> COMMIT;
Query OK, 0 rows affected (0.01 sec)
# 再查询发现数据已被修改,说明数据已经持久化到磁盘
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)

5.5 保存点

设置保存点,回滚时指定保存点可以把数据恢复到保存点的状态

java 复制代码
# 开启事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
# 在修改之前查看表中的数据
mysql> select * from bank_account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+------+---------+
2 rows in set (0.00 sec)
# 张三余额减少100
mysql> UPDATE bank_accountset balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 李四余额增加100
mysql> UPDATE bank_accountset balance = balance + 100 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 | 张三 | 800.00 |
| 2 | 李四 | 1200.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
# 设置保存点
mysql> SAVEPOINT savepoint1;
Query OK, 0 rows affected (0.01 sec)
# 再次执行,张三余额减少100
mysql> UPDATE bank_accountset balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 再次执行,李四余额增加100
mysql> UPDATE bank_accountset balance = balance + 100 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 | 张三 | 700.00 |
| 2 | 李四 | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
# 设置第二个保存点
mysql> SAVEPOINT savepoint2;
Query OK, 0 rows affected (0.00 sec)
# 插入一条新记录
mysql> insert into bank_account values (null, '王五', 1000);
Query OK, 1 row affected (0.01 sec)
# 查询结果,新记录写入成功
mysql> select * from bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 700.00 |
| 2 | 李四 | 1300.00 |
| 3 | 王五 | 1000.00 |
+----+--------+---------+
3 rows in set (0.00 sec)
# 回滚到第二个保存点
mysql> ROLLBACK TO savepoint2;
Query OK, 0 rows affected (0.00 sec)
# 回滚成功
mysql> select * from bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 700.00 |
| 2 | 李四 | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
# 回滚到第一个保存点
mysql> ROLLBACK TO savepoint1;
Query OK, 0 rows affected (0.00 sec)
# 回滚成功
mysql> select * from bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1200.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
# 回滚时不指定保存点,直接回滚到事务开始时的原始状态,事务关闭
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)
# 原始状态
mysql> select * from bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1100.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

5.6 自动/手动提交事务

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

查看当前事务是否自动提交可以使用以下语句

java 复制代码
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | # ON 表示自动提交开启
+---------------+-------+
1 row in set, 1 warning (0.04 sec)

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

java 复制代码
# 设置事务自动提交
mysql> SET AUTOCOMMIT=1; # 方式一
mysql> SET AUTOCOMMIT=ON; # 方式二
# 设置事务手动提交
mysql> SET AUTOCOMMIT=0; # 方式一
mysql> SET AUTOCOMMIT=OFF; # 方式二

注意:

只要使用 START TRANSACTION 或 BEGIN 开启事务,必须要通过 COMMIT 提交才会持久 化,与是否设置 SET autocommit 无关

手动提交模式下,不用显示开启事务,执行修改操作后,提交或回滚事务时直接使用 commit 或 rollback

已提交的事务不能回滚

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

6.1 什么是隔离性

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

6.2 隔离级别

事务具有隔离性,那么如何实现事务之间的隔离?隔离到什么程度?如何保证数据安全的同时也 要兼顾性能?这都是要思考的问题

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

READ UNCOMMITTED ,读未提交

READ COMMITTED ,读已提交

REPEATABLE READ ,可重复读(默认)

SERIALIZABLE,串行化

6.3 查看和设置隔离级别

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

(隔离级别分为全局和会话作用域",而是 "隔离级别可以在全局或会话作用域进行配置)

  • transaction → 事务(数据库中不可分割的操作单元)
  • isolation → 隔离(多个事务之间相互独立、互不干扰的特性)
java 复制代码
# 全局作用域
SELECT @@GLOBAL.transaction_isolation;
# 会话作用域
SELECT @@SESSION.transaction_isolation;
# 可以看到默认的事务隔离级别是REPEATABLE-READ(可重复读)
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ | # 默认是可重复读
+---------------------------------+
1 row in set (0.00 sec)

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

复制代码
# 通过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 # 表示事务是只读,不能对数据进行读写
}
# 示例
# 设置全局事务隔离级别为串行化,后续所有事务生效,不影响当前事务
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 设置会话事务隔离级别为串行化,当前会话后续的所有事务生效,不影响当前事务,可以在任何时候
执行
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 如果不指定任何作用域,设置只针对下一个事务,随后的事务恢复之前的隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 方式一
SET GLOBAL transaction_isolation = 'SERIALIZABLE';
# 注意使用SET语法时有空格要用"-"代替
SET SESSION transaction_isolation = 'REPEATABLE-READ';
# 方式二
SET @@GLOBAL.transaction_isolation='SERIALIZABLE';
# 注意使用SET语法时有空格要用"-"代替
SET @@SESSION.transaction_isolation='REPEATABLE-READ';

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

6.4.1 READ UNCOMMITTED - 读未提交与脏读

6.4.1.1 存在问题

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

6.4.1.2 问题重现

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

复制代码
# 设置隔离级别为READ UNCOMMITTED读未提交
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 查看设置是否生效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED | # 已生效
+--------------------------------+
1 row in set (0.00 sec)

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

复制代码
# 查看设置是否生效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED | # 已生效
+--------------------------------+
1 row in set (0.00 sec)

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

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

6.4.2 READ COMMITTED - 读已提交与不可重复读

6.4.2.1 存在问题

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

6.4.2.2 问题重现

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

复制代码
# 设置隔离级别为READ COMMITTED读未提交
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)

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

复制代码
# 查看设置是否生效
mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-COMMITTED | # 已生效
+--------------------------------+
1 row in set (0.00 sec)

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

6.4.3 REPEATABLE READ - 可重复读与幻读

6.4.3.1 存在问题

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

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

6.4.3.2 问题重现

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

复制代码
# 设置隔离级别为READ COMMITTED读未提交
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)

6.4.4 SERIALIZABLE - 串行化

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

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

相关推荐
熊文豪2 小时前
国产化替代浪潮下:金仓时序数据库的破局之路
数据库·时序数据库·金仓数据库
DBA小马哥2 小时前
时序数据库InfluxDB迁移替换:痛点剖析与解决方案
运维·数据库·时序数据库·dba
早日退休!!!2 小时前
数据库高并发技术:核心原理与工程实践
数据库
信创天地2 小时前
信创环境下数据库与中间件监控实战:指标采集、工具应用与告警体系构建
java·运维·数据库·安全·elk·华为·中间件
TDengine (老段)2 小时前
TDengine ODBC 连接器进阶指南
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
鱼跃鹰飞2 小时前
面试题:说一下Spring的事务传播特性
java·数据库·spring
菩提小狗2 小时前
Sqli-Labs Less4:双引号字符型 SQL 注入详解|靶场|网络安全
数据库·sql·web安全
努力进修2 小时前
国产化替代背景下Oracle与KingbaseES异构迁移技术全解析
数据库·oracle·kingbasees
一码归一码@3 小时前
Mysql进阶之事务原理
数据库·mysql