事务入门:确保数据的一致性与持久性

在前一篇文章中,我们学习了索引如何提升查询性能。但在真实业务场景中,一个操作往往包含多个 SQL 语句,这些语句要么全部成功,要么全部失败,不能出现"只执行了一半"的情况。比如银行转账:A 账户扣款和 B 账户加款必须同时生效,否则资金就凭空消失或凭空产生了。这就是数据库事务要解决的问题。

本文将带你系统地学习 MySQL 事务的核心知识,内容包括:

  • 什么是事务,为什么需要事务
  • 事务的四大特性:ACID
  • 显式事务操作:START TRANSACTIONCOMMITROLLBACK
  • 自动提交模式 autocommit 的作用
  • 通过银行转账案例动手体验事务
  • 并发访问下的问题初探:脏读、不可重复读、幻读

读完本文,你将能正确地在应用中使用事务来保证数据的一致性,并为后续深入理解隔离级别和锁机制打下基础。


1. 什么是事务?

事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,它由一组有限的数据库操作序列构成。事务具有"要么全部执行,要么全部不执行"的原子特性。

用转账的例子来说:从账户 A 转 100 元到账户 B,包含两步:

  1. UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
  2. UPDATE accounts SET balance = balance + 100 WHERE id = 'B';

如果第一步执行成功后系统突然崩溃,第二步没有执行,那么 A 的钱被扣除了,B 却没收到。这 100 元就凭空消失了。如果将这两个操作放在一个事务中,数据库就能保证要么两步都执行成功,要么两步都不执行(回滚到执行前的状态)。


2. 事务的四大特性:ACID

ACID 是衡量事务正确性的四个基本要素:

  • 原子性 (Atomicity):事务中的所有操作作为一个整体,要么全部完成,要么全部不做。由 Undo Log 实现,后面原理篇会详解。
  • 一致性 (Consistency):事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。例如转账前后总金额不变。一致性由原子性、隔离性和持久性共同保证,也需要业务逻辑本身正确。
  • 隔离性 (Isolation):多个事务并发执行时,彼此之间不能互相干扰。隔离级别越高,并发度越低。MySQL InnoDB 通过锁和 MVCC 实现隔离性。
  • 持久性 (Durability):一旦事务提交,其对数据库的修改就是永久的,即使系统崩溃也不会丢失。由 Redo Log 保证。

我们可以用一个简单的比喻来记忆:A (原子)是事务的"不可分割",C (一致)是"规则不被打破",I (隔离)是"互不干扰",D(持久)是"说到做到"。


3. 事务的基本操作:开启、提交、回滚

MySQL 中,InnoDB 存储引擎支持事务。默认情况下,MySQL 为每条单独的 SQL 语句自动开启一个事务并自动提交(autocommit 模式)。如果需要显式控制事务边界,可以手动开启事务。

3.1 显式事务语法

sql 复制代码
-- 开启事务
START TRANSACTION;
-- 或者使用 BEGIN;  (START TRANSACTION 是标准 SQL)

-- 执行一系列 SQL 操作
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';

-- 如果一切正常,提交事务,使修改永久生效
COMMIT;

-- 如果中间出现问题,回滚事务,撤销所有修改
ROLLBACK;

3.2 autocommit 模式

通过以下命令查看当前会话的自动提交设置:

sql 复制代码
SHOW VARIABLES LIKE 'autocommit';
  • autocommit = 1(默认):每条 SQL 语句自动成为一个事务,执行后立即提交。
  • autocommit = 0:需要显式使用 COMMITROLLBACK 来结束事务,否则连接断开时自动回滚。

你可以临时关闭自动提交来体验事务:

sql 复制代码
SET autocommit = 0;

UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
-- 此时如果发现误操作,可以回滚
ROLLBACK;

SET autocommit = 1;  -- 恢复默认

建议在应用程序中始终使用 START TRANSACTION 明确开启事务,而不要依赖 autocommit = 0,因为后者容易遗漏提交或回滚,造成长事务锁表。


4. 实战:银行转账案例

我们来创建一个账户表并模拟一次转账,亲手感受事务的原子性和一致性。

4.1 准备账户表

sql 复制代码
CREATE TABLE accounts (
    id INT PRIMARY KEY,
    owner VARCHAR(50) NOT NULL,
    balance DECIMAL(10, 2) NOT NULL DEFAULT 0.00
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO accounts VALUES
('A', '张三', 500.00),
('B', '李四', 300.00);

-- 查看初始余额
SELECT * FROM accounts;

4.2 成功转账(提交事务)

sql 复制代码
START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';

-- 此时新开一个会话查询,看不到未提交的修改(隔离性)
-- 在当前会话内可以查看结果:
SELECT * FROM accounts;

COMMIT;  -- 提交后,其他会话才能看到修改

4.3 模拟失败(回滚事务)

假设 A 账户余额不足,我们检测到并决定回滚:

sql 复制代码
START TRANSACTION;

UPDATE accounts SET balance = balance - 600 WHERE id = 'A';  -- A 只有500
-- 在应用中检查 balance 是否为负数,如果是:
ROLLBACK;

-- 查看余额,应该和转账前一样
SELECT * FROM accounts;

这样,即使误操作导致负数,回滚也能保证数据恢复到初始状态。

4.4 清理环境

sql 复制代码
DROP TABLE IF EXISTS accounts;

5. 初识并发问题:脏读、不可重复读、幻读

当多个事务同时操作同一批数据时,如果不采取适当的隔离措施,就可能出现以下三种典型的并发问题。这里先用直觉理解,后续我们会详细讲解 InnoDB 的隔离级别和锁机制。

5.1 脏读 (Dirty Read)

一个事务读取到了另一个事务尚未提交的修改数据。如果那个事务随后回滚了,读到的数据就是"脏"的,不再有效。

举例

  • 事务 T1 将 A 的余额改为 400,但未提交。
  • 事务 T2 此时读取 A 的余额,得到 400。
  • T1 因某种原因回滚,余额恢复为 500。
  • T2 读到的 400 就是一个"脏"数据,基于它做出的业务决策可能出错。

5.2 不可重复读 (Non-Repeatable Read)

一个事务内,两次读取同一行数据,得到的值不同(因为被其他已提交事务修改了)。

举例

  • 事务 T1 读取 A 的余额,得到 500。
  • 事务 T2 将 A 的余额改为 400 并提交。
  • 事务 T1 再次读取 A 的余额,得到 400。
  • T1 在同一个事务中两次读到的值不一致。

不可重复读关注的是同一行数据的值被修改

5.3 幻读 (Phantom Read)

一个事务内,两次执行相同的范围查询,结果集的行数不同(因为其他事务插入了满足条件的新行)。

举例

  • 事务 T1 查询余额大于 300 的账户,返回 {张三, 李四}。
  • 事务 T2 插入了一条余额为 400 的新账户并提交。
  • 事务 T1 再次查询余额大于 300 的账户,返回 {张三, 李四, 王五}。
  • T1 发现结果集的行变多了,就像出现了"幻影"。

不可重复读针对的是行内容的修改 ,幻读针对的是行数量的变化(插入或删除)。


6. 事务隔离级别简介

为了解决上述并发问题,SQL 标准定义了四种事务隔离级别,由低到高依次为:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPEATABLE READ (InnoDB 默认) 不可能 不可能 可能(InnoDB 通过 Next-Key Lock 防止大部分幻读)
SERIALIZABLE 不可能 不可能 不可能

MySQL InnoDB 的默认隔离级别是 REPEATABLE READ ,它通过 MVCC(多版本并发控制) 解决了不可重复读,并通过 临键锁(Next-Key Lock) 在很大程度上抑制了幻读。我们将在第五阶段深入这些机制的实现原理,目前只需理解每种隔离级别能解决哪些问题即可。

查看和设置隔离级别

sql 复制代码
-- 查看当前会话的隔离级别
SELECT @@transaction_isolation;

-- 查看全局隔离级别
SELECT @@global.transaction_isolation;

-- 设置当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

7. 实战:观察脏读现象

我们通过两个并发的会话,体验 READ UNCOMMITTED 级别下的脏读。

准备工作:创建测试表并插入数据。

sql 复制代码
CREATE TABLE dirty_test (
    id INT PRIMARY KEY,
    value INT
) ENGINE=InnoDB;

INSERT INTO dirty_test VALUES (1, 100);

会话 A(操作方):

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE dirty_test SET value = 200 WHERE id = 1;
-- 注意:尚未提交!

会话 B(观察方):

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM dirty_test WHERE id = 1;  -- 结果:value = 200 (脏读!)

此时会话 B 读到了未提交的 200。如果会话 A 执行 ROLLBACK,数据恢复 100,但 B 基于 200 的后续操作就出错了。

会话 A 回滚

sql 复制代码
ROLLBACK;

会话 B 再次查询

sql 复制代码
SELECT * FROM dirty_test WHERE id = 1;  -- 结果:value = 100

清理

sql 复制代码
DROP TABLE dirty_test;

这个实验直观展示了脏读的危害。在实际业务中,我们绝不希望读到未提交的数据,因此生产环境至少使用 READ COMMITTED 或更高隔离级别。


8. 小结

事务是保证数据库操作正确性的基石:

  • 事务具有 ACID 特性:原子性、一致性、隔离性、持久性。
  • 我们可以用 START TRANSACTION / COMMIT / ROLLBACK 手动控制事务边界。
  • autocommit 控制每条 SQL 是否自动提交,建议保持默认 1,显式开启事务。
  • 通过银行转账案例,你亲手验证了原子性和一致性;通过脏读实验,你看到了隔离性缺失带来的风险。
  • 并发访问会引发脏读、不可重复读、幻读三类问题,隔离级别越高越安全,但并发性能越低。InnoDB 默认 REPEATABLE READ 在多数场景下是性能和安全的较好平衡。

事务的底层实现(Undo Log、Redo Log、MVCC、锁)会在第四和第五阶段深度剖析。下一篇我们将进入 JDBC 编程,学习如何用 Java 连接 MySQL 并在代码中控制事务,迈出数据库与应用程序集成的第一步。

思考题

  1. 在 autocommit=1 的情况下,执行一条 UPDATE 时,MySQL 内部发生了什么?(提示:隐式开启事务、执行、自动提交)
  2. 为什么"不可重复读"和"幻读"有区别?举一个幻读但非不可重复读的例子。
  3. 试着设置隔离级别为 SERIALIZABLE 并运行转账案例,观察并发时的行为差异(可以模拟锁等待)。

参考资料


相关推荐
我爱吃土豆11 小时前
Agent 的记忆机制
开发语言·数据库·人工智能
AOwhisky1 小时前
MySQL 学习笔记(第五期):用户管理与权限控制
linux·运维·数据库·笔记·学习·mysql
梦想的颜色1 小时前
Redis数据类型全解析:从底层原理到生产实战
运维·数据库·redis·缓存·高并发·分布式锁·数据类型
C137的本贾尼1 小时前
InnoDB 的物理世界:表空间、段、区与页
数据库
JdSnE27zv1 小时前
EF Code First学习笔记:数据库创建
数据库·笔记·学习
我是一颗柠檬2 小时前
【Redis】Redis性能优化Day14(2026年)
数据库·redis·性能优化
程序员老油条2 小时前
用 AI 生成复杂 SQL:LangChain4j + 本地模型实践
数据库·人工智能·sql
IT邦德2 小时前
Oracle 26ai RAC 通过gold image RU打补丁
数据库·oracle
smith成长之旅2 小时前
08 | Mem0 框架分析: BM25 的 Sigmoid 归一化
数据库·python·算法