MySQL架构揭秘:事务和读一致性问题
前言:为什么需要事务?
想象一个转账场景:张三给李四转 1000 元。这个操作在数据库中至少包含两步:
张三的账户余额减少 1000
李四的账户余额增加 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;事务结束的三种情况
执行
COMMIT—— 提交事务,修改永久生效执行
ROLLBACK—— 回滚事务,撤销所有修改客户端断开连接 —— 未提交的事务会自动回滚
三、核心问题:事务并发带来的读一致性问题
当多个事务同时操作同一份数据时,如果没有隔离机制,就会出现以下三大问题。我们通过具体例子来逐一拆解。
1. 脏读(Dirty Read)
定义:一个事务读取到了另一个事务尚未提交的数据。
场景演示
| 时间 | 事务 A(查询) | 事务 B(修改) |
|---|---|---|
| T1 | SELECT * FROM user WHERE id = 1;结果: (1, Ada, 16) | |
| T2 | UPDATE user SET age = 18 WHERE id = 1;(尚未提交) | |
| T3 | SELECT * FROM user WHERE id = 1;结果: (1, Ada, 18)←读到了未提交的数据! | |
| T4 | ROLLBACK;—— 修改被撤销 |
问题分析
事务 A 在 T3 时刻读到的age=18,是事务 B 尚未提交的临时修改。如果事务 B 最终回滚,那么这个 18 就成了“幽灵数据”,从未真正存在过。脏读的本质是读取了最终可能不存在的数据。
危害
如果事务 A 基于这个脏数据做业务决策(比如发优惠券、统计报表),就会产生严重的数据错误。
2. 不可重复读(Non-Repeatable Read)
定义:在同一个事务内,多次查询同一条记录,得到的结果不一致,因为期间被其他已提交的事务修改了。
场景演示
| 时间 | 事务 A(查询) | 事务 B(修改并提交) |
|---|---|---|
| T1 | SELECT * FROM user WHERE id = 1;结果: (1, Ada, 16) | |
| T2 | UPDATE user SET age = 18 WHERE id = 1;COMMIT;—— 提交成功 | |
| T3 | SELECT * FROM user WHERE id = 1;结果: (1, Ada, 18)← 前后不一致! |
问题分析
在同一个事务 A 内部,两次读取同一条记录,却得到了不同的年龄(16 → 18)。这会导致数据的不确定性,比如:
第一次读取 age=16,程序进行了一些计算
第二次读取 age=18,之前的计算就变得毫无意义
与脏读的区别
| 脏读 | 不可重复读 | |
|---|---|---|
| 读取的数据来源 | 其他事务未提交的数据 | 其他事务已提交的数据 |
| 数据最终是否存在 | 可能不存在(被回滚) | 已永久存在 |
| 关键区别 | 读取时数据尚未定型 | 读取时数据已定型(已提交) |
3. 幻读(Phantom Read)
定义:在同一个事务内,两次执行相同的条件查询,结果集的行数不一致,因为期间被其他已提交的事务插入或删除了新行。
场景演示
| 时间 | 事务 A(范围查询) | 事务 B(插入并提交) |
|---|---|---|
| T1 | SELECT * FROM user WHERE age > 10;结果: 1条记录 (Ada, 16) | |
| T2 | INSERT INTO user VALUES (2, 'Bob', 22);COMMIT; | |
| T3 | SELECT * 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 的一个增强特性。
五、总结与最佳实践
核心要点回顾
事务 ACID是保证数据一致性的四大支柱,其中隔离性由隔离级别来具体实现。
并发三大问题:
脏读 → 读未提交的临时数据
不可重复读 → 同一条记录多次读取值不同
幻读 → 同一个范围查询多次结果行数不同
隔离级别是权衡数据一致性和并发性能的杠杆:
READ UNCOMMITTED:基本不用,风险太大READ COMMITTED:大多数业务系统的默认选择(如 Oracle、SQL Server),接受不可重复读换取性能REPEATABLE READ:MySQL 默认级别,InnoDB 额外解决了幻读,适合对一致性要求较高的场景SERIALIZABLE:极度保守,几乎不用在线上高并发环境
生产环境建议
大多数互联网业务,MySQL 默认的
REPEATABLE READ就是安全且性能不错的选择,无需刻意调整。如果业务对幻读不敏感,且需要更高的并发吞吐,可以考虑改为
READ COMMITTED(但要注意处理好不可重复读带来的业务影响)。无论哪种级别,都要在业务代码中合理设计锁和重试机制,因为即使隔离级别再高,也避免不了业务层面的并发冲突(如超卖)。