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

📅 2026/7/18 23:30:43
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修改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 时刻读到的age18是事务 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。这会导致数据的不确定性比如第一次读取 age16程序进行了一些计算第二次读取 age18之前的计算就变得毫无意义与脏读的区别脏读不可重复读读取的数据来源其他事务未提交的数据其他事务已提交的数据数据最终是否存在可能不存在被回滚已永久存在关键区别读取时数据尚未定型读取时数据已定型已提交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 标准定义了四种隔离级别MySQLInnoDB对其有不同程度的支持。隔离级别脏读不可重复读幻读并发性能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 READMySQL 默认级别InnoDB 额外解决了幻读适合对一致性要求较高的场景SERIALIZABLE极度保守几乎不用在线上高并发环境生产环境建议大多数互联网业务MySQL 默认的REPEATABLE READ就是安全且性能不错的选择无需刻意调整。如果业务对幻读不敏感且需要更高的并发吞吐可以考虑改为READ COMMITTED但要注意处理好不可重复读带来的业务影响。无论哪种级别都要在业务代码中合理设计锁和重试机制因为即使隔离级别再高也避免不了业务层面的并发冲突如超卖。