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中,事务的控制语句主要包括:

sql 复制代码
-- 事务控制

START TRANSACTION;   -- 或 BEGIN;        -- 开始事务
COMMIT;                                   -- 提交事务
ROLLBACK;                                 -- 回滚事务

-- 保存点控制

SAVEPOINT sp_name;                         -- 设置保存点
ROLLBACK TO sp_name;                       -- 回滚到保存点
RELEASE SAVEPOINT sp_name;                 -- 释放保存点

二、MySQL事务使用实例

2.1 环境准备

以下示例基于MySQL 8.0 + InnoDB引擎。

首先创建测试数据库和账户表:

sql 复制代码
-- 创建数据库(若不存在)
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)
) ENGINE=InnoDB
  DEFAULT CHARSET = utf8mb4;

-- 初始化测试数据
INSERT INTO accounts (name, balance) VALUES
    ('Alice',   1000.00),
    ('Bob',      500.00),
    ('Charlie', 2000.00);

2.2 基本事务操作

以下示例展示最基本的事务提交与回滚:

ini 复制代码
-- ============================================
-- 会话 A:开启事务并修改数据
-- ============================================

START TRANSACTION;

UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice';
-- 当前会话 A 查询结果:Alice 的 balance = 800
-- 在其他未提交事务的会话中仍看到原值(1000)
-- 体现事务的隔离性(Isolation)

-- ============================================
-- 确认无误后提交事务
-- ============================================

COMMIT;

如果执行过程中发现问题,可以回滚:

ini 复制代码
-- ============================================
-- 会话 A:开启事务并修改数据
-- ============================================

START TRANSACTION;

UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice';

-- ============================================
-- 发现业务逻辑错误,决定撤销修改
-- ============================================

ROLLBACK;
-- Alice 的余额恢复为 1000.00
-- 本次事务中的所有更改均被取消

2.3 转账场景示例

这是最经典的银行转账场景------Alice向Bob转账200元。该操作必须保证原子性:扣款和入账要么同时成功,要么同时失败。

正确的事务写法:

sql 复制代码
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)实现更精细的回滚控制:

sql 复制代码
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引擎不支持事务。

在自动提交模式(autocommit=1,默认开启)下,每条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 Server
REPEATABLE READ 不可能 不可能 可能(InnoDB实际已解决) MySQL InnoDB
SERIALIZABLE 不可能 不可能 不可能 ---

MySQL中查看和设置隔离级别:

sql 复制代码
-- ============================================================
-- 查看事务隔离级别
-- ============================================================

-- 查看当前会话的事务隔离级别
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多版本并发控制

MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB实现事务隔离的核心机制。其基本思想是:不去阻塞写操作来等待读操作,也不阻塞读操作来等待写操作,而是为每个事务提供数据的一个"快照"版本。

InnoDB的MVCC实现依赖以下关键数据结构:

隐藏列: InnoDB为每行数据隐式添加三个列------

① DB_TRX_ID(6字节):最近一次修改该行的事务ID。

② DB_ROLL_PTR(7字节):回滚指针,指向undo log中该行的上一个版本。

③ DB_ROW_ID(6字节):单调递增的行ID(当表没有主键时作为聚簇索引的键)。

Undo Log(回滚日志): 记录了数据的历史版本。每次对数据进行修改时,InnoDB都会将旧版本的数据记录到undo log中,并通过DB_ROLL_PTR形成一条版本链。事务根据自身的隔离级别,沿着这条版本链找到应该看到的那个版本。

Read View(读视图): 事务在执行快照读时生成的"可见性快照",包含以下关键信息:

① m_ids:生成Read View时,系统中所有活跃(未提交)的事务ID列表。

② min_trx_id:m_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级别下的不可重复读):

ini 复制代码
-- ============================================================
-- 初始数据
-- ============================================================
-- 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 UPDATE),READ COMMITTED使用半一致性读(semi-consistent read):当遇到被锁定的行时,先读取其最新提交版本,如果该版本满足WHERE条件才等待锁释放。这样可以减少不必要的锁等待。

4.4 REPEATABLE READ的实现

REPEATABLE READ是InnoDB的默认隔离级别,也是其实现最为精妙的部分。核心特点:事务在执行第一次快照读时生成一个Read View,此后整个事务期间都使用同一个Read View。

这意味着:

• 同一事务内所有快照读都基于同一个"时间点"的数据快照,保证可重复读。

• 其他事务的提交不会影响当前事务已打开的Read View。

• 解决了不可重复读问题。

具体流程示例(RR级别的可重复读保证):

ini 复制代码
-- ============================================================
-- 初始数据
-- ============================================================
-- 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示例:

sql 复制代码
-- ============================================================
-- 假设 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级别下,如果autocommit=0(即使用了显式事务),InnoDB会禁用快照读,所有SELECT都变为锁定读。

• 写操作: 同样使用排他锁(X锁),与共享锁互斥。

• 效果: 通过将所有读操作升级为锁定读,SERIALIZABLE强制事务完全串行执行。一个事务读取某行后,其他事务无法修改该行,必须等待第一个事务提交或回滚。

4.6 锁机制补充

除了MVCC,InnoDB还使用多种锁机制来实现不同隔离级别的要求:

共享锁(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 Lock
READ UNCOMMITTED 读最新版本 不使用 正常加锁 不添加
READ COMMITTED 每次读取生成新Read View 每次快照读生成 仅记录锁 不添加
REPEATABLE READ 首次读取生成Read View,事务内复用 事务内唯一 记录锁+Gap Lock 添加
SERIALIZABLE 普通SELECT自动变为锁定读 不使用 所有SELECT加S锁 添加

总结

InnoDB通过MVCC提供高效的非阻塞读,通过多粒度锁(行锁、间隙锁、临键锁)保证写操作的并发安全,两者协同工作,在不同隔离级别下实现不同力度的并发控制。理解MVCC与锁机制的配合,是深入掌握数据库事务的核心。

相关推荐
叫我少年7 小时前
Quartz.NET 调度框架:从入门到封装实战
后端
砍材农夫7 小时前
物联网 基于netty构建mqtt服务udp支持
后端
JavaAgent架构师7 小时前
Spring AI接入OpenAI报错401/429?3种原因+完整解决代码
人工智能·后端
子兮曰7 小时前
AI Coding 为什么全选了 TUI?从 Claude Code 到 Codex CLI,终端架构的底层逻辑
前端·后端·ai编程
voyaqi7 小时前
从零设计企业级校验框架:Spring Boot + SPI 实战指南
spring boot·后端·log4j
XovH7 小时前
Django 从 0 到 1 打造完整电商平台:电商项目需求分析与数据库设计
后端
可乐泡枸杞7 小时前
《我用AI,一个月做出学了吗APP》
前端·人工智能·后端
huipeng9268 小时前
基于SpringCloud的博客系统
java·运维·后端·spring·spring cloud·微服务
神奇小汤圆8 小时前
一致性Hash算法:如何实现分布式系统中的高效数据分片?
后端