一、认识事务
1、多线程访问数据库出现的问题
MySQL 是一款网络服务,必定存在多个客户端同时访问服务器的场景。假如有一个抢票系统,有两个客户端抢票,一个客户端抢票之后,数据库还没有及时更新,这张票又被另一个客户端抢了一次,一张票被卖了两次。这不符合逻辑! 于是,必须要对数据库的 CURD 操作进行一定的限制,才能解决上面的问题。
本文详细介绍了 MySQL 事务机制的核心概念。内容涵盖事务的四个属性(ACID):原子性、一致性、隔离性和持久性,解释了 InnoDB 引擎如何支持事务及提交方式(自动/手动)。重点阐述了四种事务隔离级别(读未提交、读已提交、可重复读、串行化)及其解决的并发问题(脏读、不可重复读、幻读),并分析了 ACID 特性如何共同维护数据一致性,强调了应用层逻辑的重要性。

MySQL 是一款网络服务,必定存在多个客户端同时访问服务器的场景。假如有一个抢票系统,有两个客户端抢票,一个客户端抢票之后,数据库还没有及时更新,这张票又被另一个客户端抢了一次,一张票被卖了两次。这不符合逻辑! 于是,必须要对数据库的 CURD 操作进行一定的限制,才能解决上面的问题。
事务就是一组 DML 语句组成,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。 事务主要用于处理操作量大,复杂度高的数据。比如要删除一个被开除的员工的所有历史信息以及和他相关的信息,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。
但是要知道,同一时刻并不是只有一个事务在运行,而是有多个事务同时在运行。如果大量的事务在不加保护的情况下访问同一个表数据,就一定会出现问题。因为事务由多条 sql 语句组成,一个事务执行了一半,突然 mysql 客户端挂了,那么这执行了一半的事务该怎么处理?
于是,MySQL 规定,一个完整的事务,不仅仅是简单的 sql 语句的组合,还要满足下面的 四个属性:
原子性(Atomicity,或称不可分割性): 一个事务(transaction)中的所有操作,要么 全部完成,要么 全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误(导致客户端挂了),执行了一半的事务会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
隔离性(Isolation,又称独立性): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
此外,事务本质上是为应用层服务的。因为我们并不是直接访问数据库的,而是通过上层的各种语言来间接访问数据库的,于是多条 sql 语句封装出来的事务可以简化上层的编程模型和逻辑。
使用指令查询:
mysql> SHOW ENGINES\G;
*************************** 1. row ***************************
Engine: ndbcluster
Support: NO
...
*************************** 3. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES XA: YES Savepoints: YES
...
11 rows in set (0.01 sec)
从查询结果可知,目前只有 InnoDB 引擎支持事务机制。
事务的 提交(commit) 方式有两种:
自动提交;
手动提交;
autocommit 变量是设置是否自动提交。
查看 autocommit 变量:
SHOW VARIABLES LIKE 'autocommit';
设置是否自动提交:
SET AUTOCOMMIT=1; # SET AUTOCOMMIT=1 开启自动提交
SET AUTOCOMMIT=0; # SET AUTOCOMMIT=0 禁止自动提交
当我们想要开始一个事务,需要指令:
BEGIN; # 或者 START TRANSACTION;
当我们想要提交一个事务,需要指令:
COMMIT;
而这两个指令之间的 sql 语句就是事务的内容。
为了方便后面的演示,这里我们创建一个表结构如下:
CREATE TABLE IF NOT EXISTS account (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL DEFAULT '',
balance DECIMAL(10,2) NOT NULL DEFAULT 0.0
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;
此时暂时把 全局隔离级别 设置为 读未提交【为了方便通过另一个客户端查看表中的数据】:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
查看隔离级别:
SELECT @@global.transaction_isolation AS '全局隔离级别', @@session.transaction_isolation AS '当前会话隔离级别';
重启 mysql,同步当前会话和全局的隔离级别,然后查看。
如果有两个客户端同时访问同一张表,同时启动了两个事务,一个客户端事务执行了一半,崩溃了,MySQL 会自动 回滚。
什么是回滚? 当我们开始一个事务之后,在事务执行的过程中,可以创建保存点,这个保存点可以理解为游戏中的存档点,如果对于最新的操作不满意,可以回滚(读取存档点):
SAVEPOINT s1; # 创建一个保存点 s1
首先,通过 begin;开启一个事务。 然后,我们在已经创建的表中插入数据。
此时设置一个 savepoint s1;
然后再次进行一些增删改操作(比如新插入一条数据):
但是发现在新进行的操作出错了,不想要了,于是可以 回滚(读取存档点 s1):
ROLLBACK TO s1;
结果:
于是,新插入的一条'haha'的数据就被删除了。这就是 回滚。
回到特性一的演示:证明:未 commit,客户端崩溃,MySQL 自动会回滚 [因为通过手动开始,autocommit 不会自动提交]: 启动两个事务,左侧插入一个新数据 lisi,右侧可以看到新插入的数据:
左侧事务的客户端被 ctrl+\ 被发送 abort 信号,进程被杀死,右侧客户端发现新插入的 lisi 被 MySQL 自动回滚了。
证明:commit 了,客户端崩溃,MySQL 数据不会再受影响,已经持久化。[因为通过手动开始,autocommit 不会自动提交]
启动两个事务,左侧客户端插入数据 jimmay,commit 之后被信号杀死,右侧查询可以找到新插入的数据:
证明:没有手动通过 begin;/start transaction;启动一个事务,autocommit 会对提交产生影响。
在 autocommit=1(开启)的情况下: 对于没有手动通过 begin;/start transaction;启动一个事务,一条 sql 语句就是一个事务,因为每一条语句都会被包装为一个事务。 即使一条语句被执行后,客户端直接崩溃,语句仍然被持久化:
在 autocommit=0(关闭)的情况下: 需要手动 commit,无论执行了多少操作,如果在客户端退出之前没有 commit,则所有的操作都会被回滚:
如果 commit 了,做的操作就会被持久化:
到这里,我们可以总结一下:
begin/start transaction,需要手动 commit 提交,才会持久化,不收到 autocommit 设置的影响。savepoint 手动回滚;当操作异常时,MySQL 会自动回滚。默认封装成事务,自动提交。(select 有特殊情况,因为 MySQL 有 MVCC)。从哪里体现隔离性?
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
SHOW VARIABLES LIKE 'transaction_isolation'; -- 输出示例:transaction_isolation = REPEATABLE-READ
-- 全局设置(重启后生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL + 隔离等级;
-- 当前会话设置
SET SESSION TRANSACTION ISOLATION LEVEL + 隔离等级;
MySQL InnoDB 存储引擎 支持四种标准的事务隔离级别,它们通过不同的 锁机制 和 多版本并发控制(MVCC) 实现事务之间的数据可见性和并发控制。
事务隔离级别定义了多个并发事务之间的数据可见性规则,核心目标是解决以下问题:
按隔离级别从低到高排序:
SELECT 生成一个独立的快照(一致性视图),基于当前已提交的数据。SELECT 生成一致性视图,后续读取沿用该视图。幻读的解决:
-- 事务 A
BEGIN;
SELECT * FROM users WHERE age > 20; -- 假设返回 3 条记录
-- 事务 B
INSERT INTO users (id, age) VALUES (4, 25); -- 提交
-- 事务 A 再次查询
SELECT * FROM users WHERE age > 20; -- 在可重复读下,仍然返回 3 条记录(避免幻读)
SELECT 自动转换为 SELECT ... FOR SHARE,使用共享锁,导致读写冲突时阻塞。| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁机制 | 性能 |
|---|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 行级写锁 | 最高 |
| 读已提交 | 无 | 可能 | 可能 | 行级写锁 + MVCC | 高 |
| 可重复读(InnoDB 默认) | 无 | 无 | 无* | 临键锁 + MVCC | 中等 |
| 串行化 | 无 | 无 | 无 | 表级锁或严格的行级锁 | 最低 |
*InnoDB 在可重复读级别下通过间隙锁基本消除幻读。
事务的 一致性(Consistency) 需要 原子性(Atomicity)、隔离性(Isolation)、持久性(Durability) 以及 应用层的正确逻辑(用户的配合)共同维护。
事务的 ACID 特性中,一致性(Consistency)是最终目标,而其他三个特性(原子性、隔离性、持久性)是实现一致性的技术手段。同时,应用层逻辑(用户代码)的合理设计是确保一致性的必要条件。
对一致性的贡献:防止部分操作失败导致数据处于 '半完成' 状态。
-- 示例:转账操作(原子性保证)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 扣款
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- 入账
COMMIT; -- 若任何一步失败,整个事务回滚,避免数据不一致。
对一致性的贡献:防止 脏读、不可重复读、幻读 等问题破坏数据逻辑。
-- 示例:隔离级别避免中间状态暴露
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 基于快照的一致性视图
-- 其他事务的修改不会影响当前事务的查询结果
COMMIT;
对一致性的贡献:确保提交后的数据在故障后仍可恢复,避免数据丢失导致不一致。
-- 提交后数据写入磁盘(通过 Redo Log 等机制保证)
COMMIT;
对一致性的贡献:数据库无法自动理解业务规则,需通过代码显式维护。
-- 示例:应用层必须检查转账的用户减去金额,转到的用户加上相应的金额。
BEGIN;
UPDATE accounts SET balance = balance - amount WHERE user_id = 1;
UPDATE accounts SET balance = balance + amount WHERE user_id = 2;
COMMIT;

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
在线格式化和美化您的 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
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online