MySQL事务实战:MySQL实例 · 隔离级别 · InnoDB实现机制
一、数据库事务基本概念1.1 事务的定义事务Transaction是数据库管理系统执行过程中的一个逻辑单位由一个有限的数据库操作序列构成。事务是数据库并发控制和恢复的基本单位具有不可分割的特性——事务中的所有操作要么全部执行成功要么全部不执行。在关系型数据库中事务通常由一条或多条SQL语句组成。以银行转账为例从账户A扣款100元并向账户B增加100元这两个操作必须作为一个整体——要么都成功要么都失败回滚绝不能出现“钱扣了但没到账”的情况。1.2 ACID特性事务的可靠性由ACID四个核心特性来保证原子性Atomicity事务是一个不可分割的最小工作单元。事务中的所有操作要么全部完成要么全部不执行。如果在执行过程中发生错误系统会将已经执行的操作回滚到事务开始前的状态。一致性Consistency事务执行前后数据库必须从一个一致性状态转换到另一个一致性状态。一致性是由业务规则和数据库约束如主键、外键、唯一约束、CHECK约束等共同定义的。隔离性Isolation多个事务并发执行时每个事务都感觉不到其他事务的存在仿佛自己是数据库中唯一运行的事务。隔离性通过锁机制和多版本并发控制MVCC来实现。持久性Durability一旦事务被提交其结果就是永久性的即使系统发生崩溃也不会丢失。InnoDB通过Redo Log重做日志和Double Write Buffer双写缓冲区来保证持久性。1.3 事务的生命周期一个事务从开始到结束经历以下状态① 活动状态Active事务开始执行数据库操作正在进行中。② 部分提交状态Partially Committed最后一条语句执行完毕但尚未将结果持久化到磁盘。③ 失败状态Failed事务执行过程中发生错误无法继续正常执行。④ 中止状态Aborted事务回滚完成数据库恢复到事务开始前的状态。⑤ 提交状态Committed事务成功完成所有修改已持久化到磁盘。在MySQL中事务的控制语句主要包括-- 事务控制 START TRANSACTION; -- 或 BEGIN; -- 开始事务 COMMIT; -- 提交事务 ROLLBACK; -- 回滚事务 -- 保存点控制 SAVEPOINT sp_name; -- 设置保存点 ROLLBACK TO sp_name; -- 回滚到保存点 RELEASE SAVEPOINT sp_name; -- 释放保存点二、MySQL事务使用实例2.1 环境准备以下示例基于MySQL 8.0 InnoDB引擎。首先创建测试数据库和账户表-- 创建数据库若不存在 CREATE DATABASE IF NOT EXISTS transaction_demo; USE transaction_demo; -- 创建账户表 CREATE TABLE accounts ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, balance DECIMAL(12,2) NOT NULL DEFAULT 0.00, CHECK (balance 0) ) ENGINEInnoDB DEFAULT CHARSET utf8mb4; -- 初始化测试数据 INSERT INTO accounts (name, balance) VALUES (Alice, 1000.00), (Bob, 500.00), (Charlie, 2000.00);2.2 基本事务操作以下示例展示最基本的事务提交与回滚-- -- 会话 A开启事务并修改数据 -- START TRANSACTION; UPDATE accounts SET balance balance - 200 WHERE name Alice; -- 当前会话 A 查询结果Alice 的 balance 800 -- 在其他未提交事务的会话中仍看到原值1000 -- 体现事务的隔离性Isolation -- -- 确认无误后提交事务 -- COMMIT;如果执行过程中发现问题可以回滚-- -- 会话 A开启事务并修改数据 -- START TRANSACTION; UPDATE accounts SET balance balance - 200 WHERE name Alice; -- -- 发现业务逻辑错误决定撤销修改 -- ROLLBACK; -- Alice 的余额恢复为 1000.00 -- 本次事务中的所有更改均被取消2.3 转账场景示例这是最经典的银行转账场景——Alice向Bob转账200元。该操作必须保证原子性扣款和入账要么同时成功要么同时失败。正确的事务写法START TRANSACTION; -- 只有余额充足时才扣款 UPDATE accounts SET balance balance - 200 WHERE name Alice AND balance 200; -- 判断是否真的扣款成功 SELECT ROW_COUNT() INTO rows; IF rows 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 余额不足转账失败; ELSE UPDATE accounts SET balance balance 200使用保存点SAVEPOINT实现更精细的回滚控制DELIMITER $$ CREATE PROCEDURE safe_transfer() BEGIN DECLARE alice_balance DECIMAL(12,2); START TRANSACTION; -- 保存点扣款前 SAVEPOINT before_deduct; UPDATE accounts SET balance balance - 200 WHERE name Alice; SELECT balance INTO alice_balance FROM accounts WHERE name Alice; -- 余额不足仅回滚扣款操作 IF alice_balance 0 THEN ROLLBACK TO before_deduct; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 余额不足转账终止; ELSE -- 保存点入账前 SAVEPOINT before_add; UPDATE accounts SET balance balance 200 WHERE name Bob; INSERT INTO transfer_log (from_account, to_account, amount, created_at) VALUES (Alice, Bob, 200.00, NOW()); COMMIT; END IF; END $$ DELIMITER ;关键要点•InnoDB是MySQL默认存储引擎支持完整的事务ACID特性。MyISAM引擎不支持事务。•在自动提交模式autocommit1默认开启下每条SQL语句自动成为一个事务。需显式使用START TRANSACTION开启多语句事务。•DDL语句如CREATE TABLE、ALTER TABLE在MySQL中会隐式提交当前事务无法回滚。三、事务隔离级别3.1 并发事务引发的问题当多个事务同时操作同一数据时如果不加以控制会出现以下三类典型问题脏读Dirty Read一个事务读取到另一个未提交事务的修改数据。如果那个事务最终回滚读取到的就是“脏数据”——这些数据从未真正存在于数据库中。不可重复读Non-Repeatable Read一个事务内两次读取同一行数据但两次读取的结果不同。这是因为在两次读取之间另一个事务修改并提交了该行数据。幻读Phantom Read一个事务按照相同条件两次查询第二次查询结果的行数发生了变化增多或减少。这是因为另一个事务在此期间插入或删除了满足条件的行。幻读与不可重复读的核心区别幻读关注的是行数的变化INSERT/DELETE不可重复读关注的是同一行内容的变化UPDATE。3.2 四种隔离级别SQL标准定义了四种事务隔离级别从低到高依次为READ UNCOMMITTED读未提交最低级别。事务可以读取其他事务未提交的修改。存在脏读、不可重复读、幻读问题。几乎不用于生产环境仅在某些需要非阻塞读的日志/监控场景中有零星应用。READ COMMITTED读已提交事务只能读取其他事务已提交的数据解决了脏读问题。但不可重复读和幻读仍然存在。这是Oracle和PostgreSQL的默认隔离级别。REPEATABLE READ可重复读事务在执行期间看到的数据始终保持一致——同一事务内多次读取同一行数据结果相同。解决了脏读和不可重复读问题但理论上仍存在幻读。这是MySQL InnoDB的默认隔离级别。SERIALIZABLE可串行化最高级别。事务完全串行执行通过强制排序彻底杜绝所有并发问题。代价是并发性能极低通常只在金融等对数据一致性要求极高的场景中使用。3.3 隔离级别对比四种隔离级别与并发问题的关系总结如下隔离级别脏读不可重复读幻读默认数据库READ UNCOMMITTED可能可能可能—READ COMMITTED不可能可能可能Oracle、PostgreSQL、SQL ServerREPEATABLE READ不可能不可能可能InnoDB实际已解决MySQL InnoDBSERIALIZABLE不可能不可能不可能—MySQL中查看和设置隔离级别-- -- 查看事务隔离级别 -- -- 查看当前会话的事务隔离级别 SELECT transaction_isolation; -- 查看全局事务隔离级别 SELECT global.transaction_isolation; -- -- 设置事务隔离级别 -- -- 设置当前会话的隔离级别仅对当前连接生效 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置全局隔离级别对所有新建连接生效不影响已有连接 SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;四、InnoDB隔离级别实现机制4.1 MVCC多版本并发控制MVCCMulti-Version Concurrency Control多版本并发控制是InnoDB实现事务隔离的核心机制。其基本思想是不去阻塞写操作来等待读操作也不阻塞读操作来等待写操作而是为每个事务提供数据的一个“快照”版本。InnoDB的MVCC实现依赖以下关键数据结构隐藏列InnoDB为每行数据隐式添加三个列——① DB_TRX_ID6字节最近一次修改该行的事务ID。② DB_ROLL_PTR7字节回滚指针指向undo log中该行的上一个版本。③ DB_ROW_ID6字节单调递增的行ID当表没有主键时作为聚簇索引的键。Undo Log回滚日志记录了数据的历史版本。每次对数据进行修改时InnoDB都会将旧版本的数据记录到undo log中并通过DB_ROLL_PTR形成一条版本链。事务根据自身的隔离级别沿着这条版本链找到应该看到的那个版本。Read View读视图事务在执行快照读时生成的“可见性快照”包含以下关键信息① m_ids生成Read View时系统中所有活跃未提交的事务ID列表。② min_trx_idm_ids中的最小值。③ max_trx_id生成Read View时系统下一个将分配的事务ID。④ creator_trx_id创建该Read View的事务ID。可见性判断规则当要读取一行数据时InnoDB根据该行的DB_TRX_ID与Read View中的信息进行比较• 如果DB_TRX_ID min_trx_id说明修改该行的事务在Read View生成前已提交该版本可见。• 如果DB_TRX_ID max_trx_id说明修改该行的事务在Read View生成后才开始该版本不可见需沿undo log向前查找。• 如果min_trx_id DB_TRX_ID max_trx_id需判断DB_TRX_ID是否在m_ids中。如果在说明事务未提交不可见如果不在说明事务已提交可见。4.2 READ UNCOMMITTED的实现READ UNCOMMITTED是最简单的隔离级别——几乎不做任何并发控制• 读操作始终读取数据的最新版本不生成Read View不检查版本可见性。因此可以读到其他事务尚未提交的修改脏读。• 写操作仍然使用行锁写操作之间互斥保证不会出现两个事务同时修改同一行导致的数据损坏。• 锁定读SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE 仍然会加锁按正常锁机制处理。4.3 READ COMMITTED的实现READ COMMITTED级别下每次执行快照读普通SELECT时都会生成一个新的Read View。这意味着• 每次读取都能看到其他事务在当前时刻之前已提交的最新数据。• 解决了脏读问题——因为只读已提交版本永远不会看到未提交的数据。• 但同一事务内的两次读取可能看到不同的数据不可重复读因为第二次读取会生成全新的Read View。具体流程示例RC级别下的不可重复读-- -- 初始数据 -- -- Alice.balance 1000 -- -- 事务 A -- START TRANSACTION; -- 第一次读取生成 Read View 1 SELECT balance FROM accounts WHERE name Alice; -- 结果1000 -- -- 事务 B并发执行 -- START TRANSACTION; UPDATE accounts SET balance 800 WHERE name Alice; COMMIT; -- -- 事务 A 再次读取 -- -- 第二次读取生成新的 Read View 2 SELECT balance FROM accounts WHERE name Alice; -- 结果800不可重复读 COMMIT;对于锁定读SELECT ... FOR UPDATEREAD COMMITTED使用半一致性读semi-consistent read当遇到被锁定的行时先读取其最新提交版本如果该版本满足WHERE条件才等待锁释放。这样可以减少不必要的锁等待。4.4 REPEATABLE READ的实现REPEATABLE READ是InnoDB的默认隔离级别也是其实现最为精妙的部分。核心特点事务在执行第一次快照读时生成一个Read View此后整个事务期间都使用同一个Read View。这意味着• 同一事务内所有快照读都基于同一个“时间点”的数据快照保证可重复读。• 其他事务的提交不会影响当前事务已打开的Read View。• 解决了不可重复读问题。具体流程示例RR级别的可重复读保证-- -- 初始数据 -- -- Alice.balance 1000 -- -- 事务 A -- START TRANSACTION; -- 第一次读取生成 Read View整个事务唯一 SELECT balance FROM accounts WHERE name Alice; -- 结果1000 -- -- 事务 B并发执行 -- START TRANSACTION; UPDATE accounts SET balance 800 WHERE name Alice; COMMIT; -- -- 事务 A 再次读取 -- -- 复用同一个 Read View SELECT balance FROM accounts WHERE name Alice; -- 结果1000可重复读 COMMIT;幻读问题在InnoDB RR级别下的处理——Next-Key Lock临键锁SQL标准认为REPEATABLE READ无法解决幻读但InnoDB通过Next-Key Lock机制在很大程度上解决了幻读问题。Next-Key Lock 记录锁Record Lock 间隙锁Gap Lock是行锁与间隙锁的组合。• 记录锁Record Lock锁定索引中的具体记录防止其他事务对该行进行UPDATE或DELETE。• 间隙锁Gap Lock锁定索引记录之间的间隙防止其他事务在该间隙中INSERT新行。• 临键锁Next-Key Lock同时锁定记录和它之前的间隙形成左开右闭区间从根本上杜绝幻读。Next-Key Lock示例-- -- 假设 accounts 表 id 列已有记录1, 5, 10, 20 -- -- 事务 A START TRANSACTION; SELECT * FROM accounts WHERE id 10 FOR UPDATE; -- InnoDB 会添加以下锁 -- 记录锁Record Lock锁定 id 20 -- 间隙锁Gap Lock锁定 (10, 20) 区间 -- 临键锁Next-Key Lock(10, 20]左开右闭 -- 事务 B 在此期间 -- ❌ 无法在 (10, 20] 区间插入新行 -- ❌ 无法在 (20, ∞) 插入新行 -- ✅ 从而避免幻读Phantom Read -- ⚠️ 重要注意 -- 如果 WHERE 条件无法使用索引导致全表扫描 -- InnoDB 会锁定表中所有记录和所有间隙4.5 SERIALIZABLE的实现SERIALIZABLE是最高隔离级别实现方式最为严格• 所有普通SELECT语句自动转换为SELECT ... LOCK IN SHARE MODE共享锁即每读取一行都加共享锁。• 与MVCC的关系在SERIALIZABLE级别下如果autocommit0即使用了显式事务InnoDB会禁用快照读所有SELECT都变为锁定读。• 写操作同样使用排他锁X锁与共享锁互斥。• 效果通过将所有读操作升级为锁定读SERIALIZABLE强制事务完全串行执行。一个事务读取某行后其他事务无法修改该行必须等待第一个事务提交或回滚。4.6 锁机制补充除了MVCCInnoDB还使用多种锁机制来实现不同隔离级别的要求共享锁S锁 / Shared Lock允许多个事务同时读取同一行数据但不允许任何事务修改被共享锁锁定的行。通过SELECT ... LOCK IN SHARE MODE显式获取或在SERIALIZABLE级别下自动获取。排他锁X锁 / Exclusive Lock只允许持有锁的事务读取和修改数据其他事务既不能读也不能写该行。通过SELECT ... FOR UPDATE或UPDATE/DELETE/INSERT语句获取。意向锁Intention Lock表级锁用于协调行锁和表锁之间的关系。分为意向共享锁IS和意向排他锁IX。当事务要获取行级S锁时必须先获取表的IS锁要获取行级X锁时必须先获取表的IX锁。自增锁AUTO-INC Lock特殊的表级锁在INSERT到自增列AUTO_INCREMENT时使用确保自增值的唯一性和连续性。各隔离级别下的锁使用总结隔离级别快照读MVCC Read View锁定读行为Gap LockREAD UNCOMMITTED读最新版本不使用正常加锁不添加READ COMMITTED每次读取生成新Read View每次快照读生成仅记录锁不添加REPEATABLE READ首次读取生成Read View事务内复用事务内唯一记录锁Gap Lock添加SERIALIZABLE普通SELECT自动变为锁定读不使用所有SELECT加S锁添加总结InnoDB通过MVCC提供高效的非阻塞读通过多粒度锁行锁、间隙锁、临键锁保证写操作的并发安全两者协同工作在不同隔离级别下实现不同力度的并发控制。理解MVCC与锁机制的配合是深入掌握数据库事务的核心。