1.什么是事务?
事务把一组 SQL 语句打包成为,在这组 SQL 的执行过程中,要么全部成功,要么全部失败。这组 SQL 语句可以是一条也可以是多条。来看一个转账的例子:
MySQL 事务具备原子性、一致性、隔离性和持久性。InnoDB 引擎提供四种隔离级别,默认可重复读,通过 MVCC 和锁机制处理脏读、不可重复读和幻读。不同级别在并发性能与数据安全间存在权衡,开发者需根据具体业务场景选择合适的隔离级别以确保数据正确性。

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

create table account( id bigint primary key AUTO_INCREMENT, Bname VARCHAR(10) not null, balance decimal(10,2) not null );
insert into account(Bname ,balance) values ("罗峰",1000);
insert into account(Bname ,balance) values ("爱丽丝",1000);
select * from account;
update account set balance = balance - 100 where bname = "罗峰";
update account set balance = balance + 100 where bname = "爱丽丝";
如果未正确控制事务,此时执行两次转账的记录应该是 800,结果却是 900,违法了事务的隔离性。

以上这四点在事务的整个执行过程中必须要得到保证,这也就是事务的 ACID 特性。
事务的 ACID 特性指的是 Atomicity (原子性),Consistency (一致性),Isolation (隔离性) 和 Durability (持久性)。
事务具备的 ACID 特性,是我们使用事务的原因,在我们日常的业务场景中有大量的需求要用事务来保证。支持事务的数据库能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,在使用事务过程中,要么提交,要么回滚,不用去考虑网络异常,服务器宕机等其他因素,因此我们经常接触的事务本质上是数据库对 ACID 模型的一个实现,是为应用层服务的。
show engines; 语句查看:
通过以下语句可以完成对事务的控制:
# 开始一个新的事务 START TRANSACTION;
# 或 BEGIN;
#或者 begin transaction;
# 提交当前事务,并对更改持久化保存 COMMIT;
# 回滚当前事务,取消其更改 ROLLBACK;
START TRANSACTION 或 BEGIN 开始一个新的事务;COMMIT 提交当前事务,并对更改持久化保存;ROLLBACK 回滚当前事务,取消其更改;# 开启事务 START TRANSACTION;
# 在修改之前查看表中的数据 select * from bank_account;
# 张三余额减少 100 UPDATE bank_account set balance = balance - 100 where name = '张三';
# 李四余额增加 100 UPDATE bank_account set balance = balance + 100 where name = '李四';
# 在修改之后,提交之前查看表中的数据,余额已经被修改 select * from bank_account;
# 回滚事务 ROLLBACK;
# 再查询发现修改没有生效 select * from bank_account;
# 开启事务 BEGIN;
# 在修改之前查看表中的数据 SELECT * FROM bank_account;
# 张三余额减少 100 UPDATE bank_account SET balance=balance-100 WHERE name='张三';
# 李四余额增加 100 UPDATE bank_account SET balance=balance+100 WHERE name='李四';
# 在修改之后,提交之前查看表中的数据,余额已经被修改 SELECT * FROM bank_account;
# 提交事务 COMMIT;
# 再查询发现数据已被修改,说明数据已经持久化到磁盘 SELECT * FROM bank_account;
在事务执行的过程中设置保存点,回滚时指定保存点可以把数据恢复到保存点的状态。
# 开启事务 START TRANSACTION;
# 在修改之前查看表中的数据 SELECT * FROM bank_account;
# 张三余额减少 100 UPDATE bank_account SET balance=balance-100 WHERE name='张三';
# 李四余额增加 100 UPDATE bank_account SET balance=balance+100 WHERE name='李四';
# 余额已经被修改 SELECT * FROM bank_account;
# 设置第一个保存点 SAVEPOINT savepoint1;
# 再次执行,张三余额减少 100 UPDATE bank_account SET balance=balance-100 WHERE name='张三';
# 再次执行,李四余额增加 100 UPDATE bank_account SET balance=balance+100 WHERE name='李四';
# 余额已经被修改 SELECT * FROM bank_account;
# 设置第二个保存点 SAVEPOINT savepoint2;
# 插入一条新记录 INSERT INTO bank_account VALUES (null, '王五', 1000);
# 查询插入后的结果 SELECT * FROM bank_account;
# 回滚到第二个保存点 ROLLBACK TO savepoint2;
# 回滚成功后查询 SELECT * FROM bank_account;
注意:如果是没有写那个 rollback,回滚时不指定保存点,直接回滚到事务开始时的原始状态,事务关闭。
# 回滚时不指定保存点,直接回滚到事务开始时的原始状态,事务关闭 ROLLBACK;
# 查看原始状态 SELECT * FROM bank_account;

默认情况下,MySQL 是自动提交事务的,也就是说我们执行的每个修改操作,比如插入、更新和删除,都会自动开启一个事务并在语句执行完成之后自动提交,发生异常时自动回滚。
查看当前事务是否自动提交可以使用以下语句:
show variables like 'autocommit';

可以通过以下语句设置事务为自动或手动提交:
# 设置事务自动提交 SET AUTOCOMMIT=1;
# 方式一 SET AUTOCOMMIT=ON;
# 方式二
# 设置事务手动提交 SET AUTOCOMMIT=0;
# 方式一 SET AUTOCOMMIT=OFF;
# 方式二
INSERT/UPDATE/DELETE)。autocommit=ON):MySQL 默认将每一条 DML 语句(如 INSERT/UPDATE)作为一个独立事务,执行后自动提交。此时一个事务确实仅包含 1 条语句。autocommit=OFF):若设置 set autocommit=OFF,则执行的所有 DML 语句会默认加入同一个事务,直到 COMMIT/ROLLBACK 才结束。例如:set autocommit=OFF;
UPDATE bank_account SET balance=balance-100 WHERE name='张三'; -- 语句 1
UPDATE bank_account SET balance=balance+100 WHERE name='李四'; -- 语句 2
COMMIT; -- 提交事务,两条语句作为一个事务执行
autocommit=ON,若用 START TRANSACTION/BEGIN 显式开启事务,后续的多条 DML 语句会被合并为一个事务,直到 COMMIT/ROLLBACK 才结束。例如:START TRANSACTION; -- 显式开启事务(覆盖自动提交)
UPDATE bank_account SET balance=balance-100 WHERE name='张三'; -- 语句 1
UPDATE bank_account SET balance=balance+100 WHERE name='李四'; -- 语句 2
COMMIT; -- 提交事务,两条语句作为一个事务执行
autocommit 系统变量autocommit 是 MySQL 的系统变量,用于标识事务是否自动提交,默认值为 ON(开启自动提交)。show variables like 'autocommit'; 查看其状态。set autocommit = 0; 或 set autocommit = OFF; 均可关闭自动提交。autocommit 后,执行修改操作(如 DELETE)后,必须通过 COMMIT 提交或 ROLLBACK 回滚,才能结束事务。START TRANSACTION/BEGIN 显式开启事务,无论 autocommit 状态如何,都必须通过 COMMIT 提交才能持久化数据。COMMIT/ROLLBACK 即可。autocommit 会恢复为默认的自动提交状态;若需永久改为手动提交,需修改 MySQL 配置文件。MySQL 服务可同时被多个客户端访问,每个客户端的 DML 语句以事务为单位;当不同客户端修改同一张表的同一条数据时,可能相互影响。为保证事务执行过程互不干扰,事务之间需相互隔离,这种特性即为隔离性。
事务的隔离需考虑'如何实现隔离、隔离程度、兼顾数据安全与性能'等问题;事务间不同程度的隔离称为隔离级别,不同隔离级别在性能和安全上做了取舍(有的侧重并发、有的侧重安全、有的二者适中)。
MySQL 的 InnoDB 引擎中,事务隔离级别有四种:
READ UNCOMMITTED,读未提交
READ COMMITTED,读已提交
REPEATABLE READ,可重复读(默认)
SERIALIZABLE,串行化
SERIALIZABLE(串行化):事务串行执行(相当于单线程),完全隔离,解决所有并发问题,但并发性能极低,仅用于数据一致性要求极高的场景。
REPEATABLE READ(可重复读,MySQL 默认):同一事务内多次读取同一数据,结果一致,解决了'脏读''不可重复读',但仍可能出现'幻读'(InnoDB 通过 MVCC 等机制优化了幻读问题)。

READ COMMITTED(读已提交):事务只能读取其他事务已提交的数据,解决了'脏读',但仍存在不可重复读、幻读问题,是很多数据库的默认级别。

READ UNCOMMITTED(读未提交):事务能读取其他事务未提交的数据,并发性能高,但会出现'脏读'(读取到无效数据)、不可重复读、幻读问题。

事务隔离级别分全局作用域和会话作用域,查看方式:
# 全局作用域 SELECT @@GLOBAL.transaction_isolation;
# 会话作用域 SELECT @@SESSION.transaction_isolation;
默认隔离级别为 REPEATABLE-READ(可重复读)。
通过 GLOBAL|SESSION 指定作用域,语法:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level|access_mode;
REPEATABLE READ(可重复读)READ COMMITTED(读已提交)READ UNCOMMITTED(读未提交)SERIALIZABLE(串行化)READ WRITE:事务可读写数据READ ONLY:事务只读,不可写SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
如果执行 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;(不写 GLOBAL/SESSION):
| 设置方式 | 作用域 | 生效范围 |
|---|---|---|
GLOBAL | 所有新会话 | 新连接的会话后续事务 |
SESSION | 当前会话 | 当前会话的后续所有事务 |
| 不指定作用域 | 下一个事务 | 仅下一个事务,之后恢复原级别 |
在 READ UNCOMMITTED(读未提交)这个隔离级别下,事务读取数据没有限制,虽然并发效率高,但会出现数据安全问题:比如事务 A 执行了 INSERT 语句插入数据,但还没执行 COMMIT(提交事务),这时候事务 B 就能读到这条'还没确定保存'的数据;如果之后事务 A 执行了回滚操作(把刚才插入的数据撤销了),那事务 B 之前读到的这条数据就成了'无效数据'—— 相当于读了一个'不存在的、临时的脏数据',这种现象就叫'脏读'。
# 设置隔离级别为 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)
# 查看设置是否生效 mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED | # 已生效
+--------------------------------+
1 row in set (0.00 sec)
在不同客户端中执行事务。



由于 READ UNCOMMITTED 读未提交会出现'脏读'现象,在正常的业务中出现这种问题会产生非常危重后果,所以正常情况下应该避免使用 READ UNCOMMITTED 读未提交这种的隔离级别。
为了解决脏读问题,可以把事务的隔离级别设置为 READ COMMITTED,这时事务只能读到了其他事务提交之后的数据,但会出现不可重复读的问题,比如事务 A 先对某条数据进行了查询,之后事务 B 对这条数据进行了修改,并且提交(COMMIT)事务,事务 A 再对这条数据进行查询时,得到了事务 B 修改之后的结果,这导致了事务 A 在同一个事务中以相同的条件查询得到了不同的值,这个现象叫'不可重复读'。
# 设置隔离级别为 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)
# 查看设置是否生效 mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| READ-COMMITTED | # 已生效
+--------------------------------+
1 row in set (0.00 sec)
不同的客户端中执行事务。


为了解决不可重复读问题,可将事务隔离级别设置为 REPEATABLE READ,此时同一个事务中读取的数据在任何时候都是相同的结果,但仍会出现'幻读'问题:事务 A 查询一个区间的记录得到结果集 A,事务 B 向这个区间的间隙中写入一条记录并提交,事务 A 再次查询该区间时,会查到事务 B 新写入的记录得到结果集 B,两次查询的结果集不一致,这种现象即为'幻读'。
MySQL 的 InnoDB 存储引擎使用了 Next-Key 锁解决了大部分幻读问题。
由于 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)



把隔离级别设置为 REPEATABLE-READ 后,在 ID 的间隙中插入新数据观察现象,比如插入 ID=4 的记录。
串行化进一步提升事务的隔离级别到 SERIALIZABLE,此时所有事务串行执行,可以解决所有并发中的安全问题。

InnoDB 存储引擎事务隔离性以及相关的隔离级别是由锁和 MVCC 机制配合实现的,关于锁与 MVCC 的相关内容和原子性,持久性,一致性的相关实现原理可以参阅 MySQL 进阶中事务章节。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
在线格式化和美化您的 SQL 查询(它支持各种 SQL 方言)。 在线工具,SQL 美化和格式化在线工具,online
解析 INSERT 等受限 SQL,导出为 CSV、JSON、XML、YAML、HTML 表格(见页内语法说明)。 在线工具,SQL转CSV/JSON/XML在线工具,online
CSV 与 JSON/XML/HTML/TSV/SQL 等互转,单页多 Tab。 在线工具,CSV 工具包在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online