【MySQL深入详解】第03篇:事务的本质——ACID与隔离级别深度解读

2 阅读10分钟

写在前面:事务是数据库最核心的特性之一。没有事务,我们的银行转账可能钱打出去但账户没扣款;没有事务,订单创建可能商品库存减了但订单没生成。这些数据不一致的问题,在事务面前都将不复存在。

开篇引入:转账失败的惊魂一刻

想象这个场景:你在线转账200元给朋友。系统显示"转账成功",你很开心。但第二天朋友说没收到。一查流水,钱确实扣了,但朋友账户没增加。

这在有事务保护的数据库中是不可能发生的。转账操作会被包裹在一个事务中:扣款和存款,要么同时成功,要么同时失败。

今天,让我们深入理解事务——这个数据库世界最重要的概念之一。

什么是事务?

事务(Transaction)是一组SQL语句,作为一个工作单元以原子方式进行处理。原子意味着不可分割——要么全部执行成功,要么全部执行失败。

START TRANSACTION;

-- 步骤1:检查余额
SELECT balance FROM accounts WHERE user_id = 1;
-- 如果余额 >= 200,继续

-- 步骤2:扣款
UPDATE accounts SET balance = balance - 200 WHERE user_id = 1;

-- 步骤3:存款
UPDATE accounts SET balance = balance + 200 WHERE user_id = 2;

-- 如果以上都成功
COMMIT;
-- 如果任何一步失败
ROLLBACK;  -- 全部回滚到事务开始前的状态

事务的四个阶段

START TRANSACTION
    ↓
执行SQL(修改数据)
    ↓
┌─────────────────┐
│ 一切顺利?      │
└────────┬────────┘
         │
    ┌────┴────┐
    ↓         ↓
  COMMIT    ROLLBACK
    ↓         ↓
  提交     回滚
  生效     撤销

ACID:事务的四条铁律

ACID是数据库事务处理遵循的四个原则,代表了事务应有的特性。

A - Atomicity(原子性)

原子性意味着事务是不可分割的工作单元。事务中的所有操作要么全部提交,要么全部回滚。

实际案例

START TRANSACTION;
    UPDATE accounts SET balance = balance - 200 WHERE user_id = 1;  -- 成功
    UPDATE accounts SET balance = balance + 200 WHERE user_id = 2;  -- 成功
COMMIT;  -- 提交,整个转账完成

-- 或者中途失败
START TRANSACTION;
    UPDATE accounts SET balance = balance - 200 WHERE user_id = 1;  -- 成功
    UPDATE accounts SET balance = balance + 200 WHERE user_id = 2;  -- 网络断开!
COMMIT;  -- 失败
ROLLBACK;  -- 自动回滚,扣款也撤销

如果原子性不保证,钱可能扣了但没到账——这是灾难性的。

C - Consistency(一致性)

一致性确保数据库从一个一致状态转换到另一个一致状态。事务执行前后,数据库的完整性约束必须保持。

什么是完整性约束?

-- 主键约束:id 必须唯一
-- 外键约束:orders.user_id 必须引用 users.id
-- 唯一约束:email 不能重复
-- 非空约束:name 不能为空
-- 检查约束:age > 0

一致性示例

-- 假设有外键约束:orders.user_id 引用 users.id
START TRANSACTION;
    INSERT INTO users (id, name) VALUES (1, 'Alice');
    INSERT INTO orders (user_id, amount) VALUES (1, 100);
COMMIT;  -- 成功,两个表都有一致的数据

START TRANSACTION;
    DELETE FROM users WHERE id = 1;  -- 失败!有订单引用这个用户
COMMIT;
-- 触发外键约束,事务回滚

I - Isolation(隔离性)

隔离性意味着一个事务的修改在提交前对其他事务不可见。这防止了并发导致的数据混乱。

并发问题的例子

-- 问题场景:脏读
Session A                    Session B
────────                    ────────
SELECT balance              UPDATE balance = 0
  FROM accounts              WHERE user_id = 1;
  WHERE user_id = 1;        
                           COMMIT;

-- A 读取到了 B 未提交的修改
-- 如果 B 回滚了,A 读取的数据就是"脏"的

隔离性通过隔离级别来控制,后面会详细讲解。

D - Durability(持久性)

持久性意味着一旦事务提交,其修改将永久保存,即使系统崩溃也不会丢失。

START TRANSACTION;
    INSERT INTO orders (id, amount) VALUES (1, 100);
COMMIT;  -- 返回"提交成功"
    
-- 即使数据库服务器立即断电重启
-- 这条订单记录依然存在

ACID vs CAP:不同的哲学

分布式系统中有CAP定理:一个系统不能同时满足一致性(Consistency)、可用性(Availability)和分区容错(Partition tolerance)。

MySQL作为单节点数据库,可以通过ACID保证强一致性。分布式数据库可能为了可用性而牺牲强一致性。

MySQL的事务控制

基本语法

-- 开始事务
START TRANSACTION;
-- 或者
BEGIN;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

-- 设置保存点
SAVEPOINT point1;
-- 回滚到保存点
ROLLBACK TO SAVEPOINT point1;

AUTOCOMMIT模式

MySQL默认启用自动提交模式——每条SQL语句立即生效。

-- 查看当前 AUTOCOMMIT 设置
SHOW VARIABLES LIKE 'autocommit';

-- 禁用自动提交
SET autocommit = 0;

-- 执行多条SQL(需要手动 COMMIT)
INSERT INTO users VALUES (1, 'Alice');
INSERT INTO orders VALUES (1, 1, 100);
COMMIT;

-- 重新启用
SET autocommit = 1;

建议:除非是只读查询,否则显式使用事务。

多语句事务的陷阱

-- 第一个 INSERT 立即生效(AUTOCOMMIT=1)
INSERT INTO users VALUES (1, 'Alice');
-- 第二个 INSERT 在事务中
START TRANSACTION;
INSERT INTO orders VALUES (1, 1, 100);
ROLLBACK;  -- 只回滚第二个 INSERT

事务隔离级别

这是事务最复杂的部分。SQL标准定义了4种隔离级别,从低到高依次为:

READ UNCOMMITTED(未提交读)

最低的隔离级别。事务可以读取其他事务未提交的修改。

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- Session A
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
-- 未提交

-- Session B(同一时间)
SELECT balance FROM accounts WHERE user_id = 1;
-- 读取到 100(脏数据)!即使 A 还没提交

问题:脏读、不可重复读、幻读都可能发生

场景:几乎不用,除非你能接受脏数据

READ COMMITTED(提交读)

只能读取已提交事务的修改。大多数数据库的默认级别。

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Session A
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
COMMIT;  -- 提交后

-- Session B
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1;
-- 只能读到提交后的数据

问题:不可重复读、幻读可能发生

场景:适合对数据一致性要求不高的系统

REPEATABLE READ(可重复读)

MySQL InnoDB的默认隔离级别。同一个事务中多次读取相同数据,结果一致。

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Session A
START TRANSACTION;
SELECT SUM(amount) FROM orders;  -- 第一次读取:1000

-- Session B(同一时间)
INSERT INTO orders VALUES (2, 1, 500);
COMMIT;

-- Session A 再次读取
SELECT SUM(amount) FROM orders;  -- 结果还是 1000!
-- B 的插入对 A 不可见

问题:理论上幻读可能发生,但InnoDB通过MVCC解决

场景:MySQL默认,适合大多数应用

SERIALIZABLE(可串行化)

最高的隔离级别。强制事务串行执行,完全消除并发问题。

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Session A
START TRANSACTION;
SELECT * FROM orders;
-- 实际上给所有读取的行加了锁

-- Session B
START TRANSACTION;
INSERT INTO orders VALUES (3, 1, 200);
-- 等待 A 提交/回滚后才执行

问题:高并发下性能很差

场景:对数据一致性要求极高、并发要求极低的系统

隔离级别对比

隔离级别脏读不可重复读幻读并发性能
READ UNCOMMITTED可能可能可能最高
READ COMMITTED不可能可能可能
REPEATABLE READ不可能不可能可能*
SERIALIZABLE不可能不可能不可能

*InnoDB通过MVCC解决了幻读

事务日志:持久性的秘密

事务日志(Transaction Log)是实现持久性的关键。

Write-Ahead Logging(WAL)

普通写入:
内存 → 磁盘(随机I/O) → 完成

WAL写入:
内存 → 事务日志(顺序I/O) → 后台线程 → 磁盘
          ↓ 快速完成           ↓ 异步写入
      日志已持久化

Redo Log vs Undo Log

Redo Log(重做日志):

  • 记录"将要执行"的操作
  • 用于崩溃恢复,重做未完全持久化的修改
  • InnoDB特有

Undo Log(回滚日志):

  • 记录"反向操作"
  • 用于回滚未提交的事务
  • 支持MVCC
-- 查看 Redo Log 配置
SHOW VARIABLES LIKE 'innodb_log%';

-- innodb_log_file_size:单个日志文件大小
-- innodb_log_files_in_group:日志文件数量
-- 总大小 = innodb_log_file_size × innodb_log_files_in_group

崩溃恢复流程

数据库崩溃
    ↓
重启后检查 Redo Log
    ↓
重做已提交但未持久化的修改
    ↓
回滚未提交事务的修改(使用 Undo Log)
    ↓
恢复完成

实战:事务使用最佳实践

1. 短事务原则

-- ❌ 反面:长事务
BEGIN;
-- 用户登录检查
SELECT * FROM users WHERE email = 'xxx';
-- 用户下单
INSERT INTO orders VALUES (...);
-- 发送通知邮件(可能很慢)
-- ...
COMMIT;  -- 整个过程都持有锁

-- ✅ 正面:短事务
BEGIN;
INSERT INTO orders VALUES (...);
COMMIT;

-- 发送通知邮件(异步)

2. 批量操作的事务控制

-- 插入10000条记录
-- ❌ 方式1:一个大事务
BEGIN;
INSERT INTO orders VALUES (...);  -- 重复10000次
COMMIT;  -- 风险:长时间锁表

-- ✅ 方式2:分批小事务
DELIMITER //
CREATE PROCEDURE batch_insert()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i <= 10000 DO
        INSERT INTO orders VALUES (i, 1, 100);
        IF i % 1000 = 0 THEN
            COMMIT;
        END IF;
        SET i = i + 1;
    END WHILE;
    COMMIT;
END //
DELIMITER ;

3. 异常处理与事务

DELIMITER //
CREATE PROCEDURE transfer_money(
    IN from_user INT,
    IN to_user INT,
    IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT 'Transfer failed' AS message;
    END;
    
    START TRANSACTION;
    
    UPDATE accounts SET balance = balance - amount 
    WHERE user_id = from_user;
    
    IF (SELECT balance FROM accounts WHERE user_id = from_user) < 0 THEN
        ROLLBACK;
        SELECT 'Insufficient balance' AS message;
    ELSE
        UPDATE accounts SET balance = balance + amount 
        WHERE user_id = to_user;
        COMMIT;
        SELECT 'Transfer successful' AS message;
    END IF;
END //
DELIMITER ;

4. 避免事务中的远程调用

-- ❌ 反面:事务中调用外部API
BEGIN;
UPDATE orders SET status = 'paid' WHERE id = 1;
-- 远程调用支付接口(网络延迟大)
COMMIT;  -- 锁等待时间过长

-- ✅ 正面:先更新状态,异步调用
BEGIN;
UPDATE orders SET status = 'paying' WHERE id = 1;
COMMIT;

-- 异步调用支付API,成功后
UPDATE orders SET status = 'paid' WHERE id = 1;

事务的陷阱与注意事项

1. 混合使用存储引擎

-- MyISAM 表不遵守事务语义
START TRANSACTION;
    UPDATE accounts SET balance = balance - 100;  -- InnoDB,事务性
    INSERT INTO logs VALUES (NOW(), 'withdraw');   -- MyISAM,非事务性
COMMIT;
-- 第一个成功,第二个失败,无法一起回滚!

建议:所有表使用相同的存储引擎(InnoDB)

2. DDL语句的隐式提交

某些语句会自动提交当前事务:

START TRANSACTION;
INSERT INTO users VALUES (1, 'Alice');
ALTER TABLE users ADD COLUMN phone VARCHAR(20);  -- 自动提交!
-- Alice 的插入已经提交,ALTER TABLE 前的事务已结束

3. 锁等待与死锁

长事务会长时间持有锁,增加死锁风险。

-- Session A
BEGIN;
UPDATE users SET name = 'A' WHERE id = 1;
-- 长时间持有锁

-- Session B
BEGIN;
UPDATE orders SET status = 'new' WHERE user_id = 1;
UPDATE users SET name = 'B' WHERE id = 1;  -- 等待 A 释放锁
-- 潜在死锁风险

隔离级别配置

配置服务器默认级别

[mysqld]
transaction-isolation = REPEATABLE-READ

配置会话级别

-- 仅当前会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 全局(对新连接生效)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';

小结

事务是数据库一致性的守护者。

核心要点

  1. ACID四原则:原子性、一致性、隔离性、持久性
  2. 四种隔离级别:从 READ UNCOMMITTED 到 SERIALIZABLE
  3. MySQL默认是REPEATABLE READ:InnoDB通过MVCC解决幻读
  4. 事务日志是持久性的保证:WAL机制
  5. 短事务原则:减少锁持有时间,降低死锁风险
  6. 不要混合存储引擎:保持事务语义一致

实践建议

  • 显式使用事务(START TRANSACTION)
  • 保持事务简短
  • 避免在事务中执行外部调用
  • 合理选择隔离级别
  • 监控长事务和死锁

上一篇【第02篇】并发控制的艺术——读写锁与锁粒度

下一篇【第04篇】MVCC机制揭秘——MySQL如何实现高性能并发


延伸阅读

  • 《高性能MySQL(第4版)》第1章 - 事务部分
  • MySQL 8.0 Reference Manual - Transaction Management
  • InnoDB Transaction Model