MySQL主要有三种日志:undo log、redo log、binlog。前两种是InnoDB特有的,binlog是MySQL的Server层中的。
Buffer Pool
buffer pool是MySQL的缓冲池,里面存储了数据页、索引页、undo页等(与数据库不一致的即为脏页)。对数据库做的操作都会先在buffer pool中执行,后配合redo log持久化到磁盘。
undo log
事务回滚
undo log是回滚日志 ,用于保证事务原子性。在事务执行过程中会记录undo log日志(这里是先记录在buffer pool的undo页中,生成undo log,需要记录对应的redo log),若事务需要回滚,则根据undo log执行与原来相反的操作。
MVCC
我们知道数据库中的一条记录中包含了两个隐藏列:trx_id(事务ID)和roll_pointer(版本指针),undo log记录中也是如此,通过版本指针把记录的旧值连成链表(数据页中的数据就是最新版本),通过版本指针查找记录的旧版本,使用trx_id和Read View判断事务可见性。Read View是什么?
Read View : 快照,在可重复读级别下,每一个事务开始都会生成一个Read View,保证查看的数据一致。
重要字段:
- creator_trx_id:创建这个快照的事务ID
- m_ids:存储的是当前时刻活跃的事务ID(活跃事务指的是已已启动但未提交的事务)
- min_trx_id:活跃事务ID中的最小值
- max_trx_id:下一个事务分配的trx_id
如何判断事务可见性?
通过比较记录中的trx_id:
- trx_id < min_trx_id:代表修改记录的事务早就提交,可见。
- trx_id >= max_trx_id:代表修改记录的事务在当前时刻还没启动,不可见。
- 分两种情况:
- trx_id在 m_ids中:事务未提交,不可见。
- trx_id不在 m_ids中:事务已提交,不可见。
通过比较trx_id来控制并发事务,这就是MVCC(多版本并发控制)。
redo log
redo log是重做日志 ,使得MySQL具有crash-safe (崩溃恢复)能力,保证了事务的持久性 。
首先,redo log是一个循环文件组,写入为循环写。通过以下两个参数设置文件数量和文件大小:
innodb_log_files_in_group
innodb_log_file_size
事务执行时的操作会被写入redo log buffer中,记录的字段主要有:操作类型、修改的数据页号、修改数据的页内偏移量、修改的字段值。
write pos指向当前记录的位置,checkpoint指向当前要擦除的位置。
当write pos追上checkpoint时,说明redo log文件满了,这时不能再进行新的更新操作了,更新操作的SQL将被堵塞。此时会停下来把buffer pool中的脏页刷盘,让checkpoint后推,才可以进行操作。
刷盘
- 写入方式:redo log文件是顺序写(性能好),而数据页是随机写。
- 刷盘时机:
- 数据库正常关闭
- InnoDB会有一个后台线程每隔一秒将redo log buffer持久化到磁盘
- 事务提交时(控制参数:
innodb_flush_log_at_trx_commit
)
innodb_flush_log_at_trx_commit参数
- 参数为0:留在redo log buffer中,不刷新到磁盘(可能会丢失上1秒的所有数据)。
- 参数为1:直接刷新到磁盘(fsync)。
- 参数为2:写入redo log文件(操作系统中的Page Cache)(操作系统不挂就不会丢失)。
参数0和2是通过上文说到的后台线程来刷盘的。
- 0:redo log buffer (write) -> Page Cache (fsync) -> 磁盘
- 2:Page Cache (fsync) -> 磁盘
binlog
binlog是归档日志。主要用于数据备份、主从复制。
与redo log的区别
- 适用范围不同:开篇就说了,redo log是InnoDB存储引擎特有的,binlog是都能使用的
- 日志格式:binlog有三种格式:ROW、STATEMENT(默认)、MIXED。
STATEMENT
:记录操作的SQL
(相当于记录了逻辑操作,所以针对这种格式binlog
可以被称为逻辑日志 ),主从复制中根据SQL语句
重现,但是当记录的SQL
中使用了动态函数(uuid, now...)
,这样就会导致主从数据不一致。ROW
:记录行数据被修改成什么样了 (这时就不能称为是逻辑日志 了),产生记录多,导致binlog文件
过大。MIXED
:根据情况自动选择日志格式(STATEMENT, ROW)
。redo log
是物理日志,记录的是在某个数据页做了什么修改。比如对 XXX 表空间中的 XXX数据页 ZZZ 偏移量的地方做了 AAA 修改。
- 写入方式不同:binlog是追加写,写满一个文件就创建一个新文件接着写。
- 用途不同。
主从复制
- 主库写入
binlog
,提交事务,更新数据。 - 从库会创建一个专门的
I/O
线程连接主库的log dump线程
,来接收主库的binlog
日志,然后再把binlog
信息写入一个relay log(中继日志)
,返回给主库"复制成功"的响应。 - 从库会创建一个用于回放
binlog
的线程,去读relay log
,然后回放binlog
更新存储引擎中的数据,最终实现主从的数据一致性。
什么时候刷盘
首先要知道的是一个事务的binlog
不能被拆开(因为要保证原子性)。
在事务执行过程中,会先把binlog
存储到binlog cache
,binlog cache(通过binlog_cache_size控制单个线程中binlog cache的大小),满了需要暂存到磁盘。
在事务提交 时,会把 binlog cache
的日志 write
到 Page Cache(binglog文件)
,并清空 binlog cache
。通过sync_binlog
参数控制fsync
到磁盘的时机。
sync_binlog
参数:
- 0:不会
fsync
,后续由操作系统决定何时存储到磁盘 - 1:立刻
fsync
- N:等存储了 N 个事务的
binlog
再调用fsync
两阶段提交
目的:防止在提交事务时, redo log和binlog
有一个写入失败( 数据库宕机等原因 ),导致主从数据不一致。
主要方式就是先写redo log,后写binlog。
阶段1:准备阶段(Prepare ):写入redo log
,将事务状态设为prepare
。
阶段2:提交阶段(Commit ):写入binlog
并持久化到磁盘,成功后,将redo log
状态改为 commit
。
造成问题:
- 磁盘 I/O 次数多:一次事务提交至少需要两次刷盘
- 锁竞争激烈:为了保证多事务不发生顺序上的混乱
MySQL使用组提交的方式进行了优化,但是我在这里就不多说了。