mysql - 事务

1 事务介绍

事务是由 MySQL 的引擎来实现的,InnoDB 引擎它是支持事务的。

MySQL 原生的 MyISAM 引擎就不支持事务

1.1 4个特性

1.1.1 原子性(Atomicity)

一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态。

1.1.2 一致性(Consistency)

事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。

1.1.3 隔离性(Isolation)

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。

1.1.4持久性(Durability)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

1.2 InnoDB 引擎的事务

  1. 持久性是通过 redo log (重做日志)来保证的;
  2. 原子性是通过 undo log(回滚日志) 来保证的;
  3. 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
  4. 一致性则是通过持久性+原子性+隔离性来保证;

2 事务的各种读

2.1 脏读

一个事务读取到了另一个事务未提交的数据。

2.1.1 示例

  • 事务A修改某条记录,但是未提交,事务B读取到了
    image

2.2 不可重读读

在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况。重点是读已提交的数据。

2.2.1 示例

image

2.3 幻读

在一个事务内多次查询某个符合查询条件的「记录数量」,出现前后两次查询到的记录数量不一样的情况。

2.3.1 举例

【事务 A】(正在进行的事务)              【事务 B】(并发的事务)
--------------------------------      --------------------------------
  |
  |--> 开启事务 
  |
  |--> 第一次查询:select name  
  |    from employee;
  |
  |    此时读到结果:2行数据
  |
  |                                     |--> 开启事务
  |                                     |
  |                                     |--> 执行插入一条:INSERT INTO employee
  |                                     |    
  |                                     |
  |                                     |--> 提交事务 (COMMIT) 
  |                                     |    
  |
  |--> 第二次查询:select name  
  |    from employee;
  |
  |    此时读到结果:2行数据
  |
  |--> 提交事务
  • 必须是 INSERT(或 DELETE)操作导致的。事务 A 的查询条件没变,但是满足条件的总行数变了。

3 隔离级别

  1. 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;

  2. 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;

  3. 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;

  4. 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

3.1 图示:

image

3.2 隔离级别的演示

场景:

account (账户表),里面有一行数据:id=1, balance=1000。

3.2.1 读已提交

【事务 A】                                     【事务 B】
  |
  |--> 开启事务
  |
  |--> 第一次查询:SELECT balance 
  |    结果:1000 
  |
  |                                            |--> 开启事务
  |                                            |--> 修改:UPDATE balance = 500 
  |
  |--> 第二次查询:SELECT balance
  |    结果:1000  【没有脏读,因为B还没提交】
  |
  |                                            |--> 提交事务 (COMMIT )
  |                                            |    (500现在变成了真实有效数据)
  |
  |--> 第三次查询:SELECT balance 
  |    结果:500   【不可重复读发生!】
  |    (同一个事务A里,同样的查询,结果从1000变成了500)
  |
  |--> 提交事务

3.2.2 可重复读 —— MySQL 默认级别

【事务 A】                                     【事务 B】
  |
  |--> 开启事务
  |
  |--> 第一次查询:SELECT balance 
  |    结果:1000 
  |    【底层动作:生成快照,把1000拍下来】
  |
  |                                            |--> 开启事务
  |                                            |--> 修改:UPDATE balance = 500 
  |                                            |--> 提交事务 (COMMIT)
  |                                            |    (真实数据已经是500,但事务A看不到)
  |
  |--> 第二次查询:SELECT balance
  |    结果:1000   【不可重复读被解决!】
  |    (事务A只能看自己当初的快照)
  |
  |--> 提交事务
  |    

3.2.3 串行化

【事务 A】                                     【事务 B】
  |
  |--> 开启事务
  |
  |--> 第一次查询:SELECT balance 
  |    结果:1000 
  |    【底层动作:给整张表加上读锁】
  |
  |                                            |--> 开启事务
  |                                            |--> 修改:UPDATE balance = 500 
  |                                            |--> 阻塞中...等待锁释放...
  |                                            |    (因为事务A在查,所以B根本改不了!)
  |
  |--> 第二次查询:SELECT balance
  |    结果:1000  【绝对安全】
  |
  |--> 提交事务
  |   【底层动作:释放表级读锁】
  |                                            |--> 不阻塞了,修改成功
  |                                            |--> 提交事务 (COMMIT )

4 事务日志

4.1 redo日志

事务提交时,可能一次性要修改多条记录,那么会修改磁盘上的多条数据。但是磁盘上的数据页是随机分布的。如果每次修改都直接把数据页写回磁盘,就会产生大量随机写,随机写很慢。于是MySQL的做法是 WAL(Write-Ahead Logging),先写日志,再写数据页。不要每次都立刻把数据页刷到磁盘,而是先把“修改记录”顺序写到 redo log 里,然后数据页可以稍后由后台线程慢慢刷盘。

redo log 日志是在磁盘上的一组日志文件,可以把这一组文件看作是一个大的日志文件,记录日志用的,比如某个表空间的某个数据页,在某个偏移量,做了什么物理修改。

假设事务已经提交了,redo log 也刷到磁盘了,但是数据页还没来得及刷盘,MySQL 突然宕机。重启后,InnoDB 可以根据 redo log 把那次修改重新做一遍。

redo log是追加写、连续写、顺序写的。

4.1.1 大致流程

InnoDB有一个重要的内存区域Buffer Pool,里面缓存的是数据页。
执行以下语句时:

UPDATE t SET c = c + 1 WHERE id = 10;

InnoDB会检查对应的数据页有没有加载到 Buffer Pool。有就不去磁盘加载了,没有就加载到Buffer Pool

然后在内存中修改这个页,这个被修改的页成了脏页。被修改过、但还没有刷回磁盘的数据页是脏页(dirty page)。

当数据页在内存中被修改时,InnoDB 会生成 redo log 记录。redo 先写入 Log Buffer,生成 redo 记录以后,不是马上写磁盘。

Log Buffer 就是 redo log 的内存缓冲区。作用是,把很多小的 redo 记录先攒起来,减少磁盘 I/O。

事务提交时,redo log 要持久化

Log Buffer
        ↓
Redo Log File
        ↓
fsync 刷盘

后台线程慢慢刷脏页。InnoDB 后台有 page cleaner / flush 线程,会把 Buffer Pool 中的脏页慢慢刷回磁盘数据文件。

Buffer Pool 中的脏页
        ↓
Data File / 表空间文件
  • 这一过程是异步的。

4.1.2 流程示意图

SQL 执行
  ↓
修改 Buffer Pool 中的数据页
  ↓
数据页变成脏页
  ↓
redo 写入 Log Buffer
  ↓
事务提交时,根据刷盘策略把 redo 写到 Redo Log File / fsync
  ↓
事务提交成功
  ↓
后台线程异步刷脏页到 Data File

image

4.1.3 在磁盘上的redo log的文件示意图

image

4.1.4 刷盘策略

上述流程中的,把 Log Buffer 中的 redo 写到 redo log file,不一定真的落到物理磁盘。可能只是到了操作系统的 page cache。这是操作系统的一种机制。为此需要fsync,强制把文件内容真正刷到磁盘。所以事务安全性的关键是,提交时有没有 fsync。

这就是涉及到了,刷盘策略,即,什么时候强制把文件内容真正刷到磁盘。

设置刷盘策略,innodb_flush_log_at_trx_commit,有3个参数,0、1、2

参数值=1,默认

流程:

每次事务提交
  ↓
redo 到 Log Buffer write
  ↓
Log Buffer write到page cache,立即 fsync
  ↓
page cache到 Redo Log File
参数值=2

流程:

每次事务提交
  ↓
redo 到 Log Buffer write
  ↓
Log Buffer write 到 page cache
  ↓
后台大约每秒 fsync 一次
  • 如果MySQL 进程崩了,操作系统还活着,可能问题不大。

  • 如果机器断电或操作系统崩溃,可能最近约 1 秒的事务的数据可能丢失。

参数值 = 0

流程:

事务提交
  ↓
redo 留在 redo Log Buffer
  ↓
redo Log Buffer写入 page cache
  • 每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行fsync。由os自己决定什么时候同步到磁盘文件。

4.1.5 Checkpoint

redo log是固定大小的一组日志文件,是循环写的,循环使用。
redo log有两个关键位置:

write position : 当前 redo log 写到哪里
checkpoint position : 脏页刷盘点

如下图所示:

  1. 一开始,checkpoint position、write position在同一个位置。绿色表示空闲区域
  2. 有事务提交,write position前进,黄色表示写了这么多的redo log
  3. checkpoint position和write position同时前进,checkpoint position表示那些redo log对应的脏页已写入磁盘。
  4. 写的速度比较快时,快接近write position时,mysql会强制推动checkpoint前进。
    image

4.1.6 写入redo log buffer 过程

  1. Mini-Transaction:
    一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条
    redo日志,画个图表示它们的关系就是这样:
    image

  2. redo 日志写入log buffer
    image

  3. 每个mtr都会产生一组redo日志,用示意图来描述一下这些mtr产生的日志情况:
    image

  4. 不同的事务可能是 并发 执行的,所以 T1 、 T2 之间的 mtr 可能是 交替执行 的。
    image

4.1.7 redo log file 的参数

  • innodb_log_files_in_group:指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1...
    iblogfilen。默认2个,最大100个。

  • innodb_log_file_size:单个 redo log 文件设置大小,默认值为 48M 。最大值为512G,注意最大值
    指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大
    于最大值512G。

4.2 undo log

posted @ 2026-04-26 15:50  dvdhellohaha  阅读(17)  评论(0)    收藏  举报