找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

311

积分

0

好友

37

主题
发表于 2025-12-26 09:12:23 | 查看: 29| 回复: 0

本文旨在系统性地阐述MySQL中的事务机制、并发控制原理及其核心实现。内容涵盖事务的自动提交设置、并发读写可能引发的数据一致性问题、四种标准隔离级别的原理与差异,以及支撑事务四大特性(ACID)的关键技术,如MVCC、Undo Log、Redo Log和两阶段提交协议。

事务基础:连接与自动提交

在MySQL中,事务的生命周期与数据库连接(Connection)紧密绑定。

  • autoCommit = true:在此模式下,每一条SQL语句都会被视为一个独立的事务,并在执行后自动提交。
  • autoCommit = false:在此模式下,需要手动调用commit()来提交事务。在该连接上、手动提交之前执行的所有数据库操作,都将属于同一个事务。

并发事务的挑战与隔离级别

在并发环境下,写-写操作通过行锁、间隙锁等机制可以避免,但读-写并发则会引发经典的数据一致性问题:脏读、不可重复读和幻读。为解决这些问题,SQL标准定义了四种事务隔离级别。

需要明确的是,MVCC(多版本并发控制)是所有隔离级别都具备的底层机制。它使得“读”操作(通常是快照读)无需加锁,而是通过读取数据行的历史版本来实现非阻塞读。

  1. 读未提交 (Read Uncommitted):事务可以读取到其他未提交事务修改的MVCC版本数据。存在脏读、不可重复读、幻读问题。
  2. 读已提交 (Read Committed):事务只能读取到其他已提交事务的MVCC版本数据。解决了脏读,但存在不可重复读和幻读问题。
  3. 可重复读 (Repeatable Read,MySQL默认级别):事务只能读取到其事务ID之前已提交的MVCC版本数据。通过MVCC解决了快照读下的幻读问题;通过间隙锁 (Gap Lock) 实现了当前读下的读写互斥,从而解决了当前读的幻读问题。
  4. 串行化 (Serializable):通过加表级锁来实现最严格的隔离,所有读写操作均串行化。能解决所有并发问题,但性能开销最大,通常不推荐使用。

当前读 vs. 快照读

  • 快照读:基于MVCC,读取符合事务ReadView规则的数据历史版本(通常是普通SELECT语句)。
  • 当前读:直接读取数据行的最新版本,需要加锁(如SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE, DELETE, INSERT语句),通过Next-Key Lock(临键锁)保证。

MVCC的核心:ReadView(读视图)

每个事务在启动时(具体时机因隔离级别而异)会生成一个自己的ReadView,用于判断数据版本的可见性。

ReadView的核心结构

  • m_ids:生成ReadView时,系统中所有活跃(未提交) 事务ID的列表。
  • min_trx_idm_ids中的最小值。
  • max_trx_id:生成ReadView时,系统应该分配给下一个事务的ID值。
  • creator_trx_id:生成该ReadView的事务自身的ID。

可见性判断规则

  1. 若数据版本的trx_id < min_trx_id,说明该版本在ReadView创建前就已提交,可见
  2. 若数据版本的trx_id >= max_trx_id,说明该版本是由在ReadView创建之后才启动的事务修改的,不可见
  3. min_trx_id <= trx_id < max_trx_id,则进一步判断:
    • trx_idm_ids列表中,说明修改该版本的事务在生成ReadView时仍活跃,不可见
    • trx_id不在m_ids列表中,说明该事务已提交,可见
  4. 若数据版本的trx_id等于creator_trx_id,说明是自己事务修改的,可见

事务的基石:ACID特性实现原理

  1. 原子性 (Atomicity):通过Undo Log(回滚日志) 实现。事务中的任何操作,在Undo Log中都有相反操作的记录。若事务失败回滚,InnoDB会根据Undo Log执行反向操作,恢复到事务开始前的状态。
  2. 一致性 (Consistency):由应用层、数据库约束和事务机制共同保证。在主从复制场景下,Binlog(二进制日志) 用于保证主从数据一致性。而Binlog与Redo Log之间的一致性,则通过下文介绍的两阶段提交来保障。
  3. 隔离性 (Isolation)
    • 快照读的隔离:由MVCCUndo Log版本链共同实现,允许多版本并发读写。
    • 当前读的隔离:由行锁间隙锁(合称Next-Key Lock) 实现,通过锁机制达到读写互斥,实现更严格的隔离。
  4. 持久性 (Durability):主要由Redo Log(重做日志) 和两阶段提交保证。InnoDB的缓冲池(Buffer Pool)机制将数据页(通常16KB)缓存在内存中,并非每次修改都直接写盘。Redo Log记录了数据页的物理变化,可以在数据库异常重启后用于重放操作,确保已提交事务的持久性。关于网络和系统的底层交互,可以参考网络/系统相关内容。

两阶段提交:Redo Log与Binlog的协作

一条SQL语句的执行,特别是涉及事务提交时,其日志落盘流程至关重要。下图展示了在典型“双1”配置下,Redo Log和Binlog的协作过程:

数据库事务流程图
(图示:MySQL事务提交过程中,Redo Log与Binlog的两阶段提交流程)

关键参数与落盘时机

  • Redo Log Buffer 落盘 (innodb_flush_log_at_trx_commit)

    • 0:每秒一次,将Buffer写入OS缓存并刷盘(flush)。
    • 1(推荐):每次事务提交,都执行Buffer写入并强制刷盘。
    • 2:每次事务提交,仅将Buffer写入OS缓存,依赖操作系统刷盘(断电可能丢失数据)。
  • Binlog Cache 落盘 (sync_binlog)

    • 0:依赖操作系统刷盘。
    • 1(推荐):每次事务提交,强制刷盘。
    • N:每N次事务提交后,强制刷盘一次。

两阶段状态

  1. Prepare阶段:事务提交时,先将Redo Log标记为PREPARE状态并持久化。
  2. Commit阶段:接着,将Binlog写入并刷盘。最后,将Redo Log标记为COMMIT状态。

崩溃恢复:基于日志的数据修复

数据库崩溃重启后,InnoDB会从Redo Log的最后一个检查点(Checkpoint)开始,恢复之后所有状态的记录(包括ACTIVE, PREPARE, COMMITTED)。

  1. ACTIVE状态:事务未提交,使用Undo Log进行回滚。
  2. COMMITTED状态:事务已提交,将对应的修改重放(前滚)到数据页。
  3. PREPARE状态(两阶段提交的第一阶段):需检查Binlog。
    • 若Binlog中存在该事务完整记录,则判定事务提交成功,进行重放。
    • 若Binlog中无该事务记录,则判定事务提交失败,使用Undo Log回滚。

说明:Redo Log是物理日志,记录“在某个数据页的某个偏移量处,将数据A改为数据B”。Binlog是逻辑日志,记录原始SQL语句或行变化逻辑。

应对页损坏:Double Write机制

InnoDB数据页大小为16KB,而操作系统(如Linux)的页(Page Cache)大小为4KB,磁盘I/O的最小单位是操作系统页。在写入16KB数据页时,可能发生部分写(Partial Page Write) 问题:即只成功写入了其中几个4KB的OS页,导致该16KB的数据库页数据损坏且不可恢复(Redo Log重放需要基于一个完整的正确旧页)。

Double Write(双写缓冲) 机制用于解决此问题:

  1. 在将脏数据页写入磁盘实际位置前,先将它们连续地写入磁盘上一个名为doublewrite buffer的公共区域。
  2. 然后再将数据页写入各自的表空间文件。
  3. 如果发生崩溃,恢复时可以先从doublewrite buffer中找到该页的完好副本,用于恢复损坏的页,然后再应用Redo Log。

Binlog的格式选择

  • STATEMENT:记录原始的SQL语句。可能导致主从不一致(如SQL中含RAND(), NOW()等非确定性函数)。
  • ROW(推荐):记录每一行数据的变化细节(修改前、修改后的值)。数据一致性最强,但日志量较大。
  • MIXED:混合模式。通常记录SQL语句,但当SQL可能引起主从不一致时(如使用了不确定函数),自动切换为ROW格式记录。

MVCC的物理存储:版本链

每行数据记录(Record)后面都隐藏了几个系统字段,其中包括:

  • DB_TRX_ID:最近一次修改该行的事务ID。
  • DB_ROLL_PTR:指向该行数据在Undo Log中上一个历史版本记录的指针。

Undo Log中存储了数据行的版本链。当某个事务需要读取数据时,会根据自身ReadView的规则,沿着这个版本链寻找对其可见的、正确版本的数据。更多关于数据库内部机制的深入讨论,可以访问数据库/中间件板块。

数据库日志记录
(图示:通过DB_TRX_ID和DB_ROLL_PTR形成的MVCC数据版本链)

例如,在REPEATABLE READ级别下,执行DELETE操作并不会立即物理删除数据行,而是将其标记为删除(更新DB_TRX_ID等)。这是因为其他事务可能还需要通过版本链访问该行的旧版本数据。真正的物理清理由后台的Purge线程在合适的时机进行。




上一篇:深入浅出JavaScript async/await:从原理到实践,告别回调地狱
下一篇:Java单例模式五种写法详解:从线程安全到枚举最佳实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 11:55 , Processed in 0.212127 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表