MySQL 事务

MySQL 事务

MySQL 事务

MySQL主从复制:https://blog.csdn.net/a18792721831/article/details/146117935

MySQL Binlog:https://blog.csdn.net/a18792721831/article/details/146606305

MySQL General Log:https://blog.csdn.net/a18792721831/article/details/146607343

MySQL Slow Log:https://blog.csdn.net/a18792721831/article/details/147166971

MySQL Error Log:https://blog.csdn.net/a18792721831/article/details/147167038

MySQL Redo Log: https://blog.csdn.net/a18792721831/article/details/149862528

MySQL Undo Log: https://blog.csdn.net/a18792721831/article/details/149880355

MySQL 索引算法: https://blog.csdn.net/a18792721831/article/details/149883014

MySQL 索引类型: https://blog.csdn.net/a18792721831/article/details/150275404

MySQL 索引优化: https://blog.csdn.net/a18792721831/article/details/150282163

MySQL 锁: https://blog.csdn.net/a18792721831/article/details/154197322

MySQL 事务

事务(Transaction) 是数据库操作的最小工作单元,也是一组操作的集合。事务是一个逻辑概念,事务中的操作要么全部成功,要么全部失败。事务即是区分文件系统和数据库的重要特征之一,也是一个关系型数据库的灵魂所在。

事务的特性

事务一般具有(ACID)特性:

  • A(Atomicity):原子性,一个事务要么全部成功,要么全部失败。
  • C(Consistency):一致性,事务的执行不能破坏数据库的完整性和一致性,事务执行前后的数据库必须处于一致的状态。
  • I(Isolation):隔离性,隔离性一般指的是在并发环境下,多个事务之间相互隔离,一个事务的执行不能被其他事务破坏或干扰。
  • D(Durability):持久性,事务一旦提交,那么对数据库中的数据的改变就是永久性的,即使发生断点等宕机事故也能恢复数据。

事务的实现

MySQL InnoDB中的事务完全符合 ACID 的特性。

InnoDB中的事务实现需要3个工具,日志文件、锁、MVCC。

  • 日志文件:包含 Redo Log 和 Undo Log,记录了数据修改前后的日志。
  • 锁:锁在事务中主要用来实现隔离和并发。
  • MVCC:中文全称为多版本并发控制,主要通过行数据中两个隐藏的字段(事务ID和回滚指针)来实现。

原子性的实现

原子性是指事务中的所有操作要么全部成功,要么全部失败回滚。

InnoDB通过 Undo Log 来实现原子性。当事务对数据进行修改时,InnoDB会生成对应的 Undo Log。如果事务执行失败或者调用了 ROLLBACK,就可以利用 Undo Log 中的信息将数据回滚到修改之前的状态。

Undo Log 记录了数据修改前的值,回滚时按照 Undo Log 的记录进行逆向操作:

  • 对于 INSERT 操作,Undo Log 记录主键信息,回滚时执行 DELETE
  • 对于 DELETE 操作,Undo Log 记录整行数据,回滚时执行 INSERT
  • 对于 UPDATE 操作,Undo Log 记录修改前的值,回滚时执行反向 UPDATE

举个例子,创建一个账户表:

sql 复制代码
CREATE DATABASE IF NOT EXISTS test_tx;
USE test_tx;
DROP TABLE IF EXISTS account;
CREATE TABLE account (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    balance INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO account(name, balance) VALUES('张三', 1000), ('李四', 1000);

执行一个转账事务,然后回滚:

sql 复制代码
USE test_tx;
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = '张三';
UPDATE account SET balance = balance + 500 WHERE name = '李四';
SELECT * FROM account;
-- 此时张三余额500,李四余额1500
ROLLBACK;
SELECT * FROM account;
-- 回滚后张三余额1000,李四余额1000

执行结果:

复制代码
-- 事务中查询
id	name	balance
1	张三	500
2	李四	1500

-- 回滚后查询
id	name	balance
1	张三	1000
2	李四	1000

可以看到,回滚后数据恢复到了事务开始前的状态。这就是 Undo Log 保证原子性的体现。

Undo Log 的回滚流程:

  1. 事务开始时,记录当前事务ID
  2. 每次修改数据前,先将原数据写入 Undo Log
  3. 如果事务失败或执行 ROLLBACK,根据 Undo Log 逆向恢复数据
  4. 如果事务提交,Undo Log 不会立即删除(因为可能被 MVCC 使用)

一致性的实现

一致性是事务的最终目标,是指事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。

说白了,一致性是一个综合性的保障,它依赖于原子性、隔离性和持久性的共同作用:

  • 原子性保证事务要么全部成功,要么全部失败,不会出现部分执行的情况
  • 隔离性保证并发事务之间互不干扰,避免数据异常
  • 持久性保证已提交的事务数据不会丢失

除了 AID 的保障,一致性还依赖于:

  1. 数据库约束:主键约束、外键约束、唯一约束、非空约束、CHECK约束等
  2. 触发器:在数据变更时自动执行业务规则检查
  3. 应用层逻辑:业务代码保证数据的业务一致性

举个例子,银行转账场景:

sql 复制代码
-- 张三转账500给李四
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = '张三';
UPDATE account SET balance = balance + 500 WHERE name = '李四';
COMMIT;

一致性要求:转账前后,张三和李四的余额总和不变。

  • 如果事务执行成功,两条 UPDATE 都执行,总和不变
  • 如果事务执行失败,两条 UPDATE 都回滚,总和不变
  • 不会出现只执行一条 UPDATE 的情况

隔离性的实现

隔离性是指在并发环境下,多个事务之间相互隔离,一个事务的执行不能被其他事务干扰。

InnoDB通过 MVCC 两种机制来实现隔离性:

  • 锁机制:通过共享锁(S锁)、排它锁(X锁)、意向锁、间隙锁、临键锁等实现写-写冲突的隔离
  • MVCC机制:通过多版本并发控制实现读-写不冲突,提高并发性能

锁机制在之前的文章中已经详细介绍过,这里重点讲 MVCC。

不同的隔离级别,隔离性的实现方式不同:

隔离级别 实现方式
读未提交(READ UNCOMMITTED) 不加锁,直接读取最新数据
读已提交(READ COMMITTED) MVCC,每次读取生成新的 ReadView
可重复读(REPEATABLE READ) MVCC,事务开始时生成 ReadView,整个事务复用
串行化(SERIALIZABLE) 加锁,读加共享锁,写加排它锁

持久性的实现

持久性是指事务一旦提交,对数据库的修改就是永久性的,即使发生宕机等故障也能恢复。

InnoDB通过 Redo Log 来实现持久性。

MySQL采用 WAL(Write-Ahead Logging) 技术,先写日志再写磁盘:

  1. 事务修改数据时,先将修改记录写入 Redo Log Buffer
  2. 事务提交时,将 Redo Log Buffer 刷新到磁盘的 Redo Log 文件
  3. 后台线程异步将脏页刷新到数据文件

这样即使在数据页刷盘之前发生宕机,也可以通过 Redo Log 恢复数据。

Redo Log 的落盘策略由 innodb_flush_log_at_trx_commit 参数控制:

  • 0:每秒刷新一次,可能丢失1秒数据
  • 1:每次事务提交都刷新,最安全但性能最低
  • 2:每次提交写入OS缓存,每秒刷新到磁盘
sql 复制代码
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

生产环境建议设置为1,保证数据不丢失。

MVCC实现

什么是MVCC

MVCC(Multi-Version Concurrency Control),多版本并发控制,是一种并发控制的方法。

MVCC的核心思想是:为每一行数据维护多个版本,读操作读取的是数据的快照版本,写操作创建新的版本。这样读写操作就不会相互阻塞,大大提高了并发性能。

MVCC 主要用于实现 读已提交(RC)可重复读(RR) 两种隔离级别。

隐藏字段

InnoDB 在每行数据后面添加了三个隐藏字段:

字段名 大小 说明
DB_TRX_ID 6字节 最近修改该行的事务ID
DB_ROLL_PTR 7字节 回滚指针,指向 Undo Log 中该行的上一个版本
DB_ROW_ID 6字节 隐藏主键,如果表没有定义主键,则使用该字段

其中 DB_TRX_ID 和 DB_ROLL_PTR 是 MVCC 实现的关键。

版本链

每次对数据进行修改时,InnoDB 会:

  1. 将修改前的数据写入 Undo Log
  2. 更新数据行的 DB_TRX_ID 为当前事务ID
  3. 更新数据行的 DB_ROLL_PTR 指向 Undo Log 中的旧版本

这样,通过 DB_ROLL_PTR 就可以找到该行数据的历史版本,形成一条版本链。

举个例子,假设有一行数据:

复制代码
id=1, name='张三', balance=1000
DB_TRX_ID=100, DB_ROLL_PTR=null

事务200修改 balance 为 800:

复制代码
当前行:id=1, name='张三', balance=800, DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log
Undo Log:id=1, name='张三', balance=1000, DB_TRX_ID=100, DB_ROLL_PTR=null

事务300修改 balance 为 500:

复制代码
当前行:id=1, name='张三', balance=500, DB_TRX_ID=300, DB_ROLL_PTR -> Undo Log(200)
Undo Log(200):id=1, name='张三', balance=800, DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log(100)
Undo Log(100):id=1, name='张三', balance=1000, DB_TRX_ID=100, DB_ROLL_PTR=null

这就形成了一条版本链:500(300) -> 800(200) -> 1000(100)

ReadView

ReadView 是 MVCC 实现的核心,用于判断版本链中哪个版本对当前事务可见。

ReadView 包含以下关键信息:

字段 说明
m_ids 生成 ReadView 时,当前系统中活跃的事务ID列表
min_trx_id m_ids 中的最小值
max_trx_id 生成 ReadView 时,系统应该分配给下一个事务的ID
creator_trx_id 创建该 ReadView 的事务ID

ReadView 的生成时机:

  • 读已提交(RC):每次 SELECT 都生成新的 ReadView
  • 可重复读(RR):事务第一次 SELECT 时生成 ReadView,后续复用

可见性判断

当事务读取某行数据时,需要判断版本链中哪个版本对当前事务可见。判断规则如下:

  1. 如果 DB_TRX_ID == creator_trx_id,说明是当前事务修改的,可见

  2. 如果 DB_TRX_ID < min_trx_id,说明该版本在 ReadView 生成前已提交,可见

  3. 如果 DB_TRX_ID >= max_trx_id,说明该版本在 ReadView 生成后才开始,不可见

  4. 如果 min_trx_id <= DB_TRX_ID < max_trx_id

    • 如果 DB_TRX_ID 在 m_ids 中,说明该事务还未提交,不可见
    • 如果 DB_TRX_ID 不在 m_ids 中,说明该事务已提交,可见

如果当前版本不可见,则通过 DB_ROLL_PTR 找到上一个版本,继续判断,直到找到可见的版本或者版本链结束。

举个例子:

假设当前有三个事务:

  • 事务100:已提交
  • 事务200:活跃中
  • 事务300:活跃中

事务300执行 SELECT,生成 ReadView:

  • m_ids = [200, 300]
  • min_trx_id = 200
  • max_trx_id = 301
  • creator_trx_id = 300

对于版本链 500(300) -> 800(200) -> 1000(100):

  1. 版本500,DB_TRX_ID=300,等于 creator_trx_id,可见(如果是事务300自己修改的)
  2. 如果是其他事务读取,DB_TRX_ID=300 在 m_ids 中,不可见,继续找下一个版本
  3. 版本800,DB_TRX_ID=200 在 m_ids 中,不可见,继续找下一个版本
  4. 版本1000,DB_TRX_ID=100 < min_trx_id,可见

所以其他事务读取到的是 balance=1000。

普通读和当前读

普通读

普通读也叫快照读一致性读,是指不加锁的 SELECT 语句。

sql 复制代码
SELECT * FROM account WHERE id = 1;

普通读通过 MVCC 机制读取数据的快照版本,不会加任何锁,读写不冲突。

在不同隔离级别下的表现:

  • 读已提交(RC):每次读取都生成新的 ReadView,可能读到其他事务已提交的修改
  • 可重复读(RR):事务内复用同一个 ReadView,保证多次读取结果一致

当前读

当前读是指读取数据的最新版本,并且会对读取的数据加锁。

以下语句都是当前读:

sql 复制代码
-- 共享锁
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
SELECT * FROM account WHERE id = 1 FOR SHARE;  -- MySQL 8.0

-- 排它锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;

-- DML语句(隐式当前读)
UPDATE account SET balance = 500 WHERE id = 1;
DELETE FROM account WHERE id = 1;
INSERT INTO account(name, balance) VALUES('王五', 1000);

当前读会读取最新的已提交数据,并根据语句类型加相应的锁:

  • LOCK IN SHARE MODE / FOR SHARE:加共享锁(S锁)
  • FOR UPDATE / DML语句:加排它锁(X锁)

当前读和普通读的区别:

特性 普通读 当前读
读取版本 快照版本 最新版本
是否加锁 不加锁 加锁
实现机制 MVCC 锁机制
并发性能 较低

举个例子验证:

sql 复制代码
-- Session 1
USE test_tx;
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 不提交

-- Session 2
USE test_tx;
BEGIN;
-- 普通读,读取快照版本
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(读取的是修改前的版本)

-- 当前读,会阻塞等待 Session 1 释放锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 阻塞中...

隔离级别

MySQL InnoDB 支持四种隔离级别,默认是可重复读(REPEATABLE READ)。

sql 复制代码
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 或者
SELECT @@transaction_isolation;

读未提交

读未提交(READ UNCOMMITTED) 是最低的隔离级别。

特点:

  • 事务可以读取其他事务未提交的数据(脏读)
  • 不使用 MVCC,直接读取最新数据
  • 并发性能最高,但数据一致性最差
sql 复制代码
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

脏读示例:

sql 复制代码
-- Session 1
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 不提交

-- Session 2 (READ UNCOMMITTED)
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 500(读到了未提交的数据,脏读)

-- Session 1
ROLLBACK;

-- Session 2
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(数据又变回去了)

读已提交

读已提交(READ COMMITTED) 是大多数数据库的默认隔离级别(但不是MySQL的默认)。

特点:

  • 只能读取其他事务已提交的数据,解决了脏读
  • 每次 SELECT 都生成新的 ReadView
  • 可能出现不可重复读(同一事务内多次读取结果不同)
sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

不可重复读示例:

sql 复制代码
-- Session 1
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000

-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;

-- Session 1
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 500(同一事务内,两次读取结果不同)
COMMIT;

可重复读

可重复读(REPEATABLE READ) 是 MySQL InnoDB 的默认隔离级别。

特点:

  • 事务内多次读取同一数据结果一致,解决了不可重复读
  • 事务开始时生成 ReadView,整个事务复用
  • InnoDB 通过间隙锁解决了幻读问题
sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

可重复读示例:

sql 复制代码
-- Session 1
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000

-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;

-- Session 1
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(仍然是1000,可重复读)
COMMIT;

串行化

串行化(SERIALIZABLE) 是最高的隔离级别。

特点:

  • 所有事务串行执行,完全隔离
  • 读加共享锁,写加排它锁
  • 并发性能最低,但数据一致性最高
sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

在串行化级别下,普通的 SELECT 也会加共享锁:

sql 复制代码
-- Session 1 (SERIALIZABLE)
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 自动加共享锁

-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 阻塞,等待 Session 1 释放锁

四种隔离级别对比:

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 InnoDB不可能
串行化 不可能 不可能 不可能

总结

MySQL InnoDB 的事务实现是一个非常精妙的设计,通过 Redo Log、Undo Log、锁和 MVCC 四大机制协同工作,实现了完整的 ACID 特性。

  • 原子性:通过 Undo Log 实现,事务失败时可以回滚到修改前的状态
  • 一致性:是事务的最终目标,由 AID 共同保障,加上数据库约束和业务逻辑
  • 隔离性:通过锁和 MVCC 实现,不同隔离级别有不同的实现策略
  • 持久性:通过 Redo Log 实现,采用 WAL 技术保证数据不丢失

MVCC 是 InnoDB 实现高并发的关键技术,通过版本链和 ReadView 实现了读写不冲突。理解 MVCC 的工作原理,对于理解 MySQL 的并发控制和排查并发问题非常重要。

在实际开发中,需要根据业务场景选择合适的隔离级别:

  • 对数据一致性要求不高,追求性能:读已提交
  • 大多数场景:可重复读(MySQL默认)
  • 对数据一致性要求极高:串行化

同时也要注意普通读和当前读的区别,在需要读取最新数据或者需要加锁保护的场景,使用当前读(FOR UPDATE / LOCK IN SHARE MODE)。

以后还需要继续努力。加油!

相关推荐
梨落秋霜2 小时前
Python入门篇【元组】
android·数据库·python
Caarlossss2 小时前
mybatis
java·数据库·tomcat·maven·mybatis·mybatis-spring
AI Echoes2 小时前
自定义 LangChain 文档加载器使用技巧
数据库·人工智能·python·langchain·prompt·agent
在风中的意志2 小时前
[数据库SQL] [leetcode] 578. 查询回答率最高的问题
数据库·sql
liuc03173 小时前
AI下调用redis并调用deepseek
数据库·redis·mybatis
遇见火星3 小时前
Redis主从复制深度解析:数据高可用与负载均衡的核心方案
数据库·redis·缓存·负载均衡
酸菜牛肉汤面3 小时前
22、数据库的乐观锁和悲观锁是什么?怎么实现的?
数据库
陌路203 小时前
MYSQL事务篇--事务隔离机制
数据库·mysql
清风6666664 小时前
基于单片机的PID调节脉动真空灭菌器上位机远程监控设计
数据库·单片机·毕业设计·nosql·课程设计·期末大作业