MySQL Binlog/Redolog和CrashSafe机制

kyaa111 1月前 ⋅ 45 阅读

redo log

redo log是MySQL InnoDB的日志, 是物理日志, 记录的是"在某个数据页上做了什么修改"

提一下MySQL里经常说到的WAL技术, WAL的全称是Write Ahead Logging, 它的关键点就是先写日志, 再写磁盘. 日志是顺序写的, 磁盘是随机写. 顺序写速度是比随机写快的.

当有一条记录需要更新的时候, InnoDB引擎就会先把记录写到redo log 里面, 并更新内存, 这个时候更新就算完成了. 同时, InnoDB引擎会在适当的时候, 将这个操作记录更新到磁盘里面, 而这个更新往往是在系统比较空闲的时候做

redo log的大小

InnoDB的redo log是固定大小的, 比如可以配置为一组4个文件, 每个文件的大小是1GB, 那么总共就可以记录4GB的操作. 从头开始写, 写到末尾就又回到开头循环写

redo log的两个指针

  1. write pos是当前记录的位置, 一边写一边后移, 写到第3号文件末尾后就回到0号文件开头.
  2. checkpoint是当前要擦除的位置, 也是往后推移并且循环的, 擦除记录前要把记录更新到数据文件.
    write pos和checkpoint之间的是文件上还空着的部分, 可以用来记录新的操作. 如果write pos
    追上checkpoint, 表示文件满了, 这时候不能再执行新的更新, 需要把redo log对应的所有脏页都flush到磁盘上, 把checkpoint推进一下, 让redo log留出空间继续写

binlog

binlog是MySQL Server的日志, binlog是逻辑日志, 记录的是这个语句的原始逻辑, 比如"给 ID = 2这一行的 c 字段加 1 "

binlog的大小

binlog是追加写入的. “追加写”是指binlog文件写到一定大小后会切换到下一个, 并不会覆盖以前的日志


一个更新操作的流程

  1. 执行器(Server)先找引擎取ID=2这一行. ID是主键, 引擎直接用树搜索找到这一行. 如果ID=2这一行所在的数据页本来就在内存中, 就直接返回给执行器; 否则, 需要先从磁盘读入内存, 然后再返回.
  2. 执行器拿到引擎给的行数据, 把这个值加上1, 比如原来是N, 现在就是N+1, 得到新的一行数据, 再调用引擎接口写入这行新数据
  3. 如果数据页在内存中就直接更新, 而如果这个数据页还没有在内存中的话, 在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer(用的是buffer pool里的内存)中
  4. 引擎将这个更新操作记录到redo log里面 (如果把innodb_flush_log_at_trx_commit设置成1, 那么redo log在prepare阶段就要刷盘一次), 此时redo log处于prepare状态. 然后告知执行器执行完成了, 随时可以提交事务.
  5. 执行器生成这个操作的binlog, 并把binlog写入磁盘 (sync_binlog设置为1).
  6. 执行器调用引擎的提交事务接口, 引擎把刚刚写入的redo log改成提交(commit)状态(如果innodb_flush_log_at_trx_commit设置成1, 则redo log更改成commit状态时, 无需再次刷盘, 只写到文件系统中, redo log的prepare阶段就已经刷盘了), 更新完成

crash后mysql如何使用redo log和binlog进行校验

如果binlog写入完成, redo log未改成提交状态时, mysql crash了, 则mysql重启后, 会检查redo log为prepare的记录, 通过xid这个共同的字段与binlog的记录进行对比.

  1. 若redo log中存在的记录, 但binlog中没有, 则mysql认为此事务需要回滚
  2. 若redo log中存在的记录, binlog中也存在且是完整的, 则认为此次事务已完成, 将redo log改成提交状态

为什么日志要用二阶段提交

假设当前ID=2的行, 字段c的值是0, 再假设执行update语句 + 1 过程中在写完第一个日志后, 第二个日志还没有写完期间发生了crash

  1. 如果先写redo log再写binlog: 假如redo log写完后mysql崩溃, 由于redo log写完后, mysql即使崩溃, 仍然可以把数据恢复回来. 通过redo log恢复后, 数据库值为1, 如果后续通过binlog恢复备库数据时, 就会少了这一次更新, 备库数据为0, 数据不一致
  2. 如果先写binlog再写redo log: 假如binlog写完后mysql崩溃, 数据库就无法把数据恢复回来, 数据仍然为0, 但使用binlog恢复备库数据时会多了这一次更新, 备库数据为1, 数据不一致

只使用redolog或binlog中的其一能否实现crash-safe

  1. 只用redolog: 仍然可以实现crash-safe, 但是binlog在mysql中可以用来归档和主从复制, 有非常多基于binlog的中间件
  2. 只用binlog: 不能实现crash-safe, 因为binlog是逻辑日志, 不能实现数据页级别的恢复, 数据落盘是以页为单位,而一个sql可能涉及多个页(如abc三页),一旦crash的时候,只有a没有落盘,bc落盘了,那么根据sql进行重放就会出错了

相关参数的设置

  1. redo log用于保证crash-safe能力. innodb_flush_log_at_trx_commit这个参数设置成1的时候,
    表示每次事务的redo log都直接持久化(fsync)到磁盘. 设置成1, 可以保证MySQL异常重启之后数据不丢失. 如果设置0, 每隔1s将数据持久化到磁盘. 设置为2, 事务提交后, 将数据提交到文件系统缓存内, 由文件系统控制何时持久化到磁盘
  2. sync_binlog这个参数设置成1的时候, 表示每提交1次事务, binlog都持久化到磁盘. 这个参数建议设置成1, 这样可以保证MySQL异常重启之后binlog不丢失. 若设置成0, 表示事务提交之后, 将数据提交到文件系统, 不立马持久化到磁盘, 而让文件系统自行决定什么时候来做同步, 或者cache满了之后才同步到磁盘

change buffer

当需要更新一个数据页时, 如果数据页在内存中就直接更新, 而如果这个数据页还没有在内存中的话, 在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中, 这样就不需要从磁盘中读入这个数据页了. 在下次查询需要访问这个数据页的时候, 将数据页读入内存, 然后执行change buffer中与这个页有关的操作. 通过这种方式就能保证这个数据逻辑的正确性.

需要说明的是, 虽然名字叫作change buffer, 实际上它是可以持久化的数据. 也就是说, change buffer在内存中有拷贝, 也会被写入到磁盘上. 将change buffer中的操作应用到原数据页, 得到最新结果的过程称为merge. 除了访问这个数据页会触发merge外, 系统有后台线程会定期merge. 在数据库正常关闭(shutdown) 的过程中, 也会执行merge操作.

显然, 如果能够将更新操作先记录在change buffer, 减少读磁盘, 语句的执行速度会得到明显的提升. 而且, 数据读入内存是需要占用buffer pool的, 所以这种方式还能够避免占用内存, 提高内存利用率

但是假设一个业务的更新模式是写入之后马上会做查询, mysql将更新先记录在change buffer, 但之后由于马上要访问这个数据页, 会立即触发merge过程. 这样随机访问IO的次数不会减少, 反而增加了change buffer的维护代价. 所以, 对于这种业务模式来说, change buffer反而起到了副作用