MySQL架构揭秘:事务和读一致性问题

📅 2026/7/18 23:49:20 👁️ 阅读次数 📝 编程学习
MySQL架构揭秘:事务和读一致性问题

前言:为什么需要事务?

想象一个转账场景:张三给李四转 1000 元。这个操作在数据库中至少包含两步:

  1. 张三的账户余额减少 1000

  2. 李四的账户余额增加 1000

如果第一步执行成功,第二步执行失败(比如系统崩溃),那就会导致张三的钱扣了,李四却没收到——这显然是灾难性的。

事务(Transaction)就是为了解决这类问题而生的。它将一组操作捆绑为一个不可分割的工作单元:要么全部执行成功,要么全部不执行,绝不会出现“半成品”状态。

在银行、电商、证券、航空订票等对数据一致性要求极高的系统中,事务是基石般的存在。


一、事务的四大特性(ACID)

特性含义通俗解释
原子性(Atomicity)事务中的所有操作是一个整体,不可分割要么全做,要么全不做,没有中间状态
一致性(Consistency)事务执行前后,数据保持合法、一致的状态转账前后,双方总金额不变
隔离性(Isolation)并发执行的事务互不干扰你转你的账,我查我的余额,彼此不影响
持久性(Durability)事务一旦提交,修改就永久保存哪怕数据库宕机重启,数据也不会丢

底层支撑

  • 原子性:依靠undo log(回滚日志),记录修改前的状态,一旦需要回滚,就根据 undo log 恢复到之前的样子。

  • 持久性:依靠redo log(重做日志),在事务提交时,将修改先写入 redo log(顺序写),即使系统崩溃,重启后也能根据 redo log 恢复已提交的数据。

隔离性一致性,则与今天我们要重点讨论的“读一致性问题”密切相关。


二、事务的开启与结束

查看当前自动提交状态

SHOW VARIABLES LIKE 'autocommit';

MySQL 默认开启自动提交(autocommit = ON),意味着每条 SQL 语句都会被视为一个独立的事务自动提交。

手动控制事务的三种方式

方式一:关闭自动提交
SET SESSION autocommit = OFF; -- 此时所有操作需要手动提交或回滚 UPDATE t_person SET username = '张三666' WHERE id = 1; COMMIT; -- 提交 -- 或者 ROLLBACK; -- 回滚
方式二:显式开启事务
BEGIN; -- 或 START TRANSACTION; UPDATE t_person SET username = '张三666666' WHERE id = 1; COMMIT;
事务结束的三种情况
  1. 执行COMMIT—— 提交事务,修改永久生效

  2. 执行ROLLBACK—— 回滚事务,撤销所有修改

  3. 客户端断开连接 —— 未提交的事务会自动回滚


三、核心问题:事务并发带来的读一致性问题

当多个事务同时操作同一份数据时,如果没有隔离机制,就会出现以下三大问题。我们通过具体例子来逐一拆解。


1. 脏读(Dirty Read)

定义:一个事务读取到了另一个事务尚未提交的数据。

场景演示
时间事务 A(查询)事务 B(修改)
T1SELECT * FROM user WHERE id = 1;
结果:(1, Ada, 16)
T2UPDATE user SET age = 18 WHERE id = 1;
尚未提交
T3SELECT * FROM user WHERE id = 1;
结果:(1, Ada, 18)读到了未提交的数据!
T4ROLLBACK;—— 修改被撤销
问题分析

事务 A 在 T3 时刻读到的age=18,是事务 B 尚未提交的临时修改。如果事务 B 最终回滚,那么这个 18 就成了“幽灵数据”,从未真正存在过。脏读的本质是读取了最终可能不存在的数据。

危害

如果事务 A 基于这个脏数据做业务决策(比如发优惠券、统计报表),就会产生严重的数据错误。


2. 不可重复读(Non-Repeatable Read)

定义:在同一个事务内,多次查询同一条记录,得到的结果不一致,因为期间被其他已提交的事务修改了。

场景演示
时间事务 A(查询)事务 B(修改并提交)
T1SELECT * FROM user WHERE id = 1;
结果:(1, Ada, 16)
T2UPDATE user SET age = 18 WHERE id = 1;
COMMIT;—— 提交成功
T3SELECT * FROM user WHERE id = 1;
结果:(1, Ada, 18)← 前后不一致!
问题分析

在同一个事务 A 内部,两次读取同一条记录,却得到了不同的年龄(16 → 18)。这会导致数据的不确定性,比如:

  • 第一次读取 age=16,程序进行了一些计算

  • 第二次读取 age=18,之前的计算就变得毫无意义

与脏读的区别
脏读不可重复读
读取的数据来源其他事务未提交的数据其他事务已提交的数据
数据最终是否存在可能不存在(被回滚)已永久存在
关键区别读取时数据尚未定型读取时数据已定型(已提交)

3. 幻读(Phantom Read)

定义:在同一个事务内,两次执行相同的条件查询,结果集的行数不一致,因为期间被其他已提交的事务插入或删除了新行。

场景演示
时间事务 A(范围查询)事务 B(插入并提交)
T1SELECT * FROM user WHERE age > 10;
结果:1条记录 (Ada, 16)
T2INSERT INTO user VALUES (2, 'Bob', 22);
COMMIT;
T3SELECT * FROM user WHERE age > 10;
结果:2条记录 (Ada,16), (Bob,22)← 多出了一行!
问题分析

同一个查询条件,第一次返回 1 行,第二次返回 2 行。多出来的那行就像“幻影”一样凭空出现,因此得名幻读

注意:不可重复读侧重于同一条记录字段值变化,而幻读侧重于结果集的行数变化(新增或删除)。两者关注点不同。


四、解决方案:事务隔离级别

为了应对上述问题,SQL 标准定义了四种隔离级别,MySQL(InnoDB)对其有不同程度的支持。

隔离级别脏读不可重复读幻读并发性能
READ UNCOMMITTED(读未提交)✅ 可能✅ 可能✅ 可能最高
READ COMMITTED(读已提交)❌ 不可能✅ 可能✅ 可能较高
REPEATABLE READ(可重复读)❌ 不可能❌ 不可能InnoDB 下不可能中等
SERIALIZABLE(串行化)❌ 不可能❌ 不可能❌ 不可能最低

特别说明:InnoDB 在REPEATABLE READ级别下,通过Next-Key Lock(间隙锁 + 行锁)机制,实际上已经解决了幻读问题。这与标准 SQL 的定义有所不同,是 InnoDB 的一个增强特性。


五、总结与最佳实践

核心要点回顾

  1. 事务 ACID是保证数据一致性的四大支柱,其中隔离性由隔离级别来具体实现。

  2. 并发三大问题

    • 脏读 → 读未提交的临时数据

    • 不可重复读 → 同一条记录多次读取值不同

    • 幻读 → 同一个范围查询多次结果行数不同

  3. 隔离级别是权衡数据一致性和并发性能的杠杆:

    • READ UNCOMMITTED:基本不用,风险太大

    • READ COMMITTED:大多数业务系统的默认选择(如 Oracle、SQL Server),接受不可重复读换取性能

    • REPEATABLE READ:MySQL 默认级别,InnoDB 额外解决了幻读,适合对一致性要求较高的场景

    • SERIALIZABLE:极度保守,几乎不用在线上高并发环境

生产环境建议

  • 大多数互联网业务,MySQL 默认的REPEATABLE READ就是安全且性能不错的选择,无需刻意调整。

  • 如果业务对幻读不敏感,且需要更高的并发吞吐,可以考虑改为READ COMMITTED(但要注意处理好不可重复读带来的业务影响)。

  • 无论哪种级别,都要在业务代码中合理设计锁和重试机制,因为即使隔离级别再高,也避免不了业务层面的并发冲突(如超卖)。