【Mysql深入】二、事务

1、什么是事务

  • Atomicity原子性,就是操作要么都成功,要么都失败,事务中的所有操作被视为一个不可分割的单元------要么全部完成,要么全部不发生。
  • Consistency 一致性,不是数据库自动保证你的业务对,而是保证"你定的规矩不被破坏"。规矩要你自己写(约束、应用逻辑)。
  • Isolation 隔离性,是一个"权衡"------隔离越强,并发越差。MySQL 默认 REPEATABLE READ,在大多数场景下平衡了安全与性能
  • Durability 持久性,一旦事务提交成功,其对数据的修改就是永久性的,即使系统崩溃也不会丢失。
  • 不管是begin开启事务,还是直接执行sql,默认就有一个自动提交的事务。
隔离级别 能看到未提交数据? 同一事务内重复读会变吗? 会出现幻行吗? 实现机制
READ UNCOMMITTED ✅ 能(脏读) ✅ 会变 ✅ 会 几乎无锁
READ COMMITTED ❌ 不能 ✅ 会变(不可重复读) ✅ 会 每次读取新快照(MVCC)
REPEATABLE READ(MySQL默认) ❌ 不能 ❌ 不会变 ⚠️ 基本不会(InnoDB 用间隙锁防幻读) MVCC + 临键锁(Next-Key Lock)
SERIALIZABLE ❌ 不能 ❌ 不会变 ❌ 不会 强制串行化(加读锁)

2、为什么能做到

  • WAL,Write-Ahead Logging,先写日志后写数据
  • 通过undo log记录原始数据值,通过redo log记录变更值,事务id

2.1 Redo Log

sql 复制代码
BEGIN;
UPDATE t SET x = 10 WHERE id = 1;
  → 1. 分配 trx_id = 1001
  → 2. 生成 Redo Log 记录:"page 123, offset 456, set to 10"
  → 3. 将 Redo Log 写入内存 log buffer
  → 4. (根据 innodb_flush_log_at_trx_commit)刷 Redo Log 到磁盘
  → 5. 修改 Buffer Pool 中的数据页(此时数据页是"脏页")
COMMIT;
  → 6. 标记事务提交,Redo Log 已持久化 → 数据安全!

2.2 trx_id和Read View

  • 每一个事务在创建时,就会生成一个trx_id,和一个Read View
  • Read View包含还未提交的事务ids, 最小的事务id,下一个最大的事务id,还有当前事务的id
  • 好处就是,查看某个数据时,可以获取这行数据的最后一个事务修改id(DB_TRX_ID),
    • 如果DB_TRX_ID=当前事务id,表明是自己修改的,就可以看得到;
    • 如果DB_TRX_ID<最小事务id,表明在创建事务时,修改已提交,也可以看得到;
    • 如果DB_TRX_ID>=最大事务id,表明是在创建之后修改的,无法看到;
    • 如果DB_TRX_ID在未提交事务id列表中,表明其他事务修改后还未提交,无法看到;
    • 如果DB_TRX_ID不在未提交事务id列表中,且大于最小事务id,小于最大事务id,说明该事务已被提交,未提交事务id列表不是连续的,可以看到

2.3 Undo log

  • 每一个数据行,包含三个隐藏字段,DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID(如果不存在)
  • 如果同时有两个事务T1,T2开启,事务id分别是t1001和t1002,此时T1修改数据,但不提交事务,就会使得该数据的DB_TRX_ID=t1001,DB_ROLL_PTR=变更前的undo log指针位置,且会产生Read View,记录了当前事务id,最小的未提交事务id,以及最大的将要出现的事务id t1002
  • T2也会同样产生一个Read View,记录最小的未提交事务id t1001,此时它查询数据,发现最后一次修改该数据的事务id是 t1001,它就没法获取该数据,只能从DB_ROLL_PTR找到undo log,undo log里面记录了对应数据的DB_TRX_ID,DB_ROLL_PTR,再进行对比,如果满足,就可以看到该数据的历史版本,这就是MVCC,Multi-Version Concurrency Control
时间 事务 T1 (trx_id=1001) 事务 T2 (trx_id=1002)
t1 BEGIN;
t2 UPDATE users SET name='Alice' WHERE id=1; (未提交)
t3 BEGIN;
t4 SELECT name FROM users WHERE id=1;
sql 复制代码
内存/磁盘中的行(最新):
┌───────────────┐
│ name = 'Alice'│ ← T2 读到这个,但发现 DB_TRX_ID=1001(不可见)
│ DB_TRX_ID=1001│
│ ROLL_PTR ─────┼──→ 指向 Undo Log
└───────────────┘
        ↓
Undo Log(版本1):
┌───────────────┐
│ name = 'Bob'  │ ← T2 检查这个:DB_TRX_ID=999(已提交,可见!)
│ DB_TRX_ID=999 │
│ ROLL_PTR ─────┼──→ 更早版本(可能不需要了)
└───────────────┘

2.4 Undo log 对比 Redo log

特性 Redo Log Undo Log
用途 崩溃恢复(持久性) 回滚 + MVCC(原子性 + 隔离性)
日志类型 物理日志(记录页修改) 逻辑日志(记录反向操作)
存储位置 独立日志文件(ib_logfile* 系统/undo 表空间(ibdata1undo_*.ibd
写入方式 顺序写 + 循环覆盖 随机写 + 动态分配
是否循环 ✅ 是 ❌ 否
何时释放 对应脏页刷盘后即可覆盖 所有事务都不再需要该版本时(由 Purge 线程清理)
空间风险 固定大小,无膨胀风险 可能因长事务导致空间暴涨

2.5 幻读

  • 幻读就是在同一个事务中,两次查询结果不一致,比如SELECT ... FOR UPDATE就是当前读,如果没有间隙锁,很可能由于第二个事务的新增操作哪怕未提交,导致SELECT ... FOR UPDATE查到第二个事务的新增数据
  • 普通 SELECT:活在过去(MVCC 快照)
    UPDATE/DELETE/FOR UPDATE:活在当下(当前读 + 加锁)
  • DELETE操作实际上只是给数据行打上了删除标记,后面由线程清理,所以MVCC也可以看到该数据
  • SELECT ... FOR UPDATE 加的排他锁(X Lock),对其他事务的 UPDATE、DELETE、SELECT ... FOR UPDATE 都有阻塞作用
  • 唯一索引 + 等值 + 命中 → 只加记录锁
    其他所有情况(范围、非唯一索引、未命中) → 加 Next-Key Lock(含间隙锁)

2.6 排他锁

  • 就如上所述,UPDATE、DELETE、SELECT ... FOR UPDATE都会有排他锁X,
  • 一个事务持有某行的 X 锁 → 其他事务无法再获取该行的任何锁(包括 S 锁和 X 锁)
  • 所以一旦一个事务执行update,另一个事务必须等待执行完毕才能继续执行

2.7 行已不存在 和 表级操作 vs 行级锁

  • TRUNCATE TABLE 和 DROP TABLE 属于 DDL(Data Definition Language)
    InnoDB 要求:执行 DDL 前,必须获取表的元数据锁(Metadata Lock, MDL)
    而任何未提交的 UPDATE/INSERT/DELETE 都会持有 MDL 读锁
    DDL 需要 MDL 写锁 → 与 DML 的 MDL 读锁冲突 → DDL 阻塞等待!
问题 答案
T1 删除,T2 更新同一行 T2 不会报错,但更新 0 行(需应用层检查)
T1 更新时执行 TRUNCATE TRUNCATE 会阻塞,直到 T1 提交/回滚
TRUNCATE 能绕过行锁吗? ❌ 不能!它要等表级元数据锁(MDL)
如何避免 DDL 阻塞? 在低峰期执行,或确保无长事务

2.8 完整的事务流程

  1. 使用 InnoDB 引擎时,只有当 binlog 开启且 binlog_format 不为 STATEMENT(或即使为 STATEMENT 但涉及事务)时,才会启用两阶段提交(2PC)。
  2. 事务开始时分配 trx_id;当执行第一个快照读(snapshot read) 时,创建 Read View,用于 MVCC 可见性判断。
  3. 执行更新时,将修改前的整行数据(包括其 DB_TRX_ID 和 DB_ROLL_PTR)写入当前事务的 undo log segment,形成版本链。Undo log 内容属于历史版本,其 DB_TRX_ID 是上一次修改该行的事务 ID。归属通过 undo segment header 管理
  4. 更新时:写 redo log(物理日志,无 trx_id,无状态)→ 存入 redo log buffer
    COMMIT 时:
    Prepare 阶段:将 redo log buffer 刷盘,并在 redo log 中写入 TRX_PREPARE 标记
    Commit 阶段:写 binlog → 再在 redo log 中写入 TRX_COMMIT 标记
  5. 提交时(2PC):
    InnoDB 将 redo log 刷盘并标记为 PREPARE;
    MySQL Server 将 binlog 刷盘;
    InnoDB 将 redo log 标记为 COMMIT;
    数据页仍留在内存中,后续由 checkpoint 异步刷盘。
  6. 回滚时,根据本事务的 undo log 逆向修改内存中的数据页,不写 redo log。这些修改仅在内存中生效,最终随脏页清理或 purge 完成。
  7. 从 checkpoint LSN 开始,重做(redo)所有 redo log 到 buffer pool
    扫描 redo log,找出所有处于 PREPARE 状态 的事务
    对每个 PREPARE 事务:
    若开启 binlog:检查 binlog 是否包含该事务的完整记录(XID 匹配)
    有 → 补 COMMIT(提交)
    无 → 回滚(rollback via undo)
    若未开启 binlog:一律回滚(因无法保证主从一致,保守策略)

2.9 Undo log 和 Redo log

  • Undo log 建立在 redo log 之上;
  • 写 undo 需要 redo,用 undo 回滚不需要 redo。
sql 复制代码
        ┌──────────────────────┐
        │   事务回滚 (ROLLBACK) │ ← 仅依赖 undo log(逻辑操作)
        └──────────┬───────────┘
                   ↓
        ┌──────────────────────┐
        │     Undo Log         │ ← 存储旧版本数据,用于回滚 & MVCC
        └──────────┬───────────┘
                   ↓ (写入时需要持久化保护)
        ┌──────────────────────┐
        │     Redo Log         │ ← 保护所有物理页修改(包括 undo 页!)
        └──────────┬───────────┘
                   ↓
        ┌──────────────────────┐
        │   物理存储(磁盘页)  │ ← 数据页、undo 页、系统页等
        └──────────────────────┘
相关推荐
Languorous.4 小时前
Linux 登录用户、主机名、提示符详解(新手不迷路)
linux·数据库·postgresql
ChoSeitaku4 小时前
10.枚举_Record_密封类_debug_API文档_Object类_lombok_Junit
java·数据库·junit
Cloud_Shy6184 小时前
Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(第十一章 Python 包跟踪器 中篇)
数据库·python·sql·数据分析·excel·web
Elnaij5 小时前
MySQL数据库入门到进阶!(3)——MySQL数据类型和MySQL表的约束
数据库·mysql
青柠代码录5 小时前
【Redis】数据类型:String
数据库·redis·缓存
TDengine (老段)5 小时前
TDengine 超级表/子表/普通表 — 设计理念与内部表示
android·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
老纪5 小时前
c++怎么利用std--variant处理多种二进制子协议包的自动分支解析【进阶】
jvm·数据库·python
pigs20186 小时前
Docker容器中Kingbase数据库授权到期更换解决方案
数据库·docker·容器
guygg886 小时前
C# 监听数据库数据变化(SqlDependency 实现)
数据库·oracle·c#