浅谈MySQL数据库持久化过程

数据持久化的方式有很多种,下面结合各个持久化阶段,来看看MySQL数据库是如何实现数据持久化的。

| 第一阶段 |

数据直接写入到磁盘。

浅谈MySQL数据库持久化过程

问题:

速度慢,通常情况下,磁盘写入速度比内存写入速度慢很多,如果在短时间内积压大量I/O请求,很难保证数据库性能。

| 第二阶段 |

解决方案:

数据先写入内存,后批量异步刷新到磁盘。

浅谈MySQL数据库持久化过程

问题:

内存数据异步刷到磁盘过程中,数据还没完全写入磁盘,此时内存或系统崩溃,数据丢失了怎么办?

浅谈MySQL数据库持久化过程

| 第三阶段 |

解决方案:redo

写入内存后,为了提高速度,并不马上写磁盘,后面会延时批量写入磁盘,同时为了数据安全,引入redo log,在内存写入数据时,会同时生成redo log数据,记录数据修改操作,用于崩溃恢复。

浅谈MySQL数据库持久化过程

什么是redo呢?

Redo log,又叫重做日志,主要记录数据的改变,用于数据库异常情况下的实例恢复。

Redo记录示例:

update t1 set id=2 were n=10;

将第5号表空间中第100号页面中偏移量为150处的值更新为2。

崩溃恢复:

当系统崩溃,内存的数据全部丢失,重启后,只需要按照redo记录重新更新一遍数据页,就可以恢复丢失的数据。

为什么redo log buffer写入到redo log file速度比脏块写入到datafile快?

1.redo日志占用空间非常小。

2.redo日志是顺序写入磁盘的,速度比随机性快。

|redo log buffer写入到redo log file触发条件:|

1.log buffer空间不足时

通过系统变量innodb_log_buffer_size指定log_buffer大小,如果log buffer的redo日志量已经占满log buffer总空间50%左右时,会将日志刷新到磁盘。

2.事务提交

事务提交时,可以不把修改过的buffer pool页面立即刷新到datafile里,但是为了保证持久性,必须要把数据修改时所对应的redo日志刷新到磁盘redo log file,用于崩溃恢复。

这个过程和innodb_flush_log_at_trx_commit参数有关,该参数有3个可选值,0、1、2。

参数值为0时:

表示事物提交时,不会立即向磁盘同步redo日志,这个任务交给后台线程来处理。

参数值为1时:

表示在事物提交时需要将redo日志同步到磁盘,可以保证事物的持久性,这也是默认值。

参数值为2时:

表示事务提交时需要将redo日志写入到操作系统缓冲区中,但并不需要保证将日志真正的落盘。

3.buffer pool中脏页刷新到磁盘datafile

buffer pool中脏页刷新到磁盘datafile前,业务先执行对应redo日志的刷盘。

4.每1秒

后台线程会以每1秒一次的频率将log buffer中redo日志进行刷盘。

5.正常关闭服务器时

6.做checkpoint时

问题:

1.写入过程中,还未提交,为了避免脏读,别的会话如何读取修改前的数据。

其他会话想要读取另一个会话正在修改还未提交的数据时,为了避免脏读,读取请求会被阻塞,直到另一个会话完成提交或回滚,这在高并发大事务下效率会很低。

2.写入后悔了怎么回退。

| 第四阶段 |

解决方案:undo

什么是undo呢?

Undo log又被称为撤销日志,主要记录数据修改前的旧值。

在内存中修改数据前,先将旧数据写入到undo中,可以通过undo中的旧数据进行一致性查询和回滚等操作。

浅谈MySQL数据库持久化过程

问题:

由于mysql数据页大小16KB,操作系统页大小一般4KB,在执行一次mysql I/O写入时,对应4次OS  I/O,如果操作系统4次I/O没有写入完成被异常中断了,这种情况被称为写失效(partial page write)。此时重启后,磁盘上就是存在不完整的数据页,就算使用redo log也是无法进行恢复。

浅谈MySQL数据库持久化过程
浅谈MySQL数据库持久化过程

| 第五阶段 |

解决方案:Double Write

在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的Double write buffer。通过Double write buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后调用fsync函数,同步磁盘,避免缓冲写带来的问题。

浅谈MySQL数据库持久化过程

问题:

如果开启了binlog,是先写binlog还是先写redolog?

binlog属于逻辑日志,记录数据变化的SQL语句。

用途:主从复制、数据恢复、历史审计等。

例如:执行一条删除语句

mysql> delete from t12;

查看删除语句对应的binlog日志内容

mysqlbinlog --base64-output=decode-rows -vv mysql-bin.000035|grep -i t12#221206 14:49:49 server id 2038  end_log_pos 425 CRC32 0x516fa576   Table_map: `cjcdb`.`t12` mapped to number 546### DELETE FROM `cjcdb`.`t12`

场景1:先写redo log后写binlog

假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。由于我们前面说过的,redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来。

但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。

如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,与原库的值不同。

场景2:先写binlog后写redo log

如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,值更新失败。但是binlog里面已经记录了修改值的日志。所以,在之后用binlog来恢复的时候就多了一个事务,恢复出来的这一行的值与原库的值不同。

可以看到,无论是先写binlog在写redo还是先写redo在写binlog都存在问题。

| 第六阶段 |

解决方案:二阶段提交

两个场景都有问题,所以引入了“二阶段提交”,将redo log 的提交分为 prepare 和 commit 两个阶段。

浅谈MySQL数据库持久化过程

通过二阶段提交,可以解决如下场景问题:

1.redo log(prepare)执行失败,由于redo log没有commit标识,并且binlog没有写入,对应的事务直接回滚。

2.redo log(prepare)执行成功,binlog还没开始写入或只写入一部分,此时系统发生故障,由于redo log没有commit标识,并且binlog不完整,对应的事务直接回滚。

3.redo log(prepare)执行成功,binlog写入成功,redo log(commit)写入失败,检查redo log(prepare) 成功,并且binlog是完整的,直接提交事务。

问题:

如果mysql服务器或硬件故障,无法及时启动数据库,如何减少服务中断和数据损失。

浅谈MySQL数据库持久化过程

| 第七阶段 |

解决方案:异步复制

如果存在从库,主库会将新增的数据产生的binlog日志通过binlog dump线程传给从库,从库通过I/O线程接收传来的日志并写入到relay log日志中,最后SQL线程解析relay log日志进行数据重放。

浅谈MySQL数据库持久化过程

问题:

主从复制默认是异步复制的,在异步复制中,主库并不关心从库是否接收到完整的日志,直接会进行后面的提交操作,如果在从库还没接收完主库传来的binlog,这时主库故障,从库切换为主库,那么在新主库上读取的数据可能会有缺失,导致数据不一致。

| 第八阶段 |

解决方案:半同步复制

浅谈MySQL数据库持久化过程

主库将新增的数据产生的的binlog日志通过binlog dump线程传给从库,从库通过I/O线程接收传来的日志并写入到relay log日志中,最后SQL线程解析relay log日志进行数据重放。

其中在从库将日志写入到本地relay log后,会给主库返回ack消息,告知主库可以提交事务了,之后主库才会继续提交事务。

这在一定情况下解决了异步复制的问题,提高了数据的安全性,但是半同步复制还是有一些缺陷:

1.从库将日志并写入到本地relay log后,主库提交事务,这时主库故障,从库切换为主库,如果relay log很大,SQL线程还没有重放完成,读取新主库的数据是滞后的,数据也不是强一致的,而是最终一致的。

2.由于安全性和性能总是对立的,安全级别越高,性能通常最差,配置半同步时需要指定超时参数rpl_semi_sync_master_timeout默认10秒,也就是主从连接超时后,主库会卡住10秒等待从库响应,10秒以后半同步就会降级到异步复制,之后如果主从连接恢复,又会自动恢复到半同步,如果主从连接一直不恢复,主从复制类型就会一直是异步复制,同样存在异步复制的缺点。

浅谈MySQL数据库持久化过程

总·结

数据持久化过程:

例如:执行下面语句,将name为b行的name列更新为a。

update t1 set name=\'a\' where name=\'b\';

1.检查待修改页是否在内存中,如果不在,将页从磁盘读取到内存中,如果已经在内存中,准备修改内存数据。

2.修改内存数据之前,先将原值name=\’b\’写入到undo,用于一致性读或回滚事务,当然涉及undo页修改的操作也会生成对应的redo log,用来保护生成的undo数据。

3.开始修改内存数据,将name为b行的name列更新为a。

4.生成修改数据对应的redo log buffer,并根据一定触发条件落盘到redo log file,更新prepare标识。

5.将数据修改操作写入到binlog cache中。

6.binlog写入完成后会传到从库,从库I/O线程将接收到的日志写入到本地relay log日志中,写入完成后向主库返回ack信息,从库SQL线程读取relay log日志,进行数据重放。

7.更新redo日志的commit标识,事务更新完成,客户端可以正常返回。

8.buffer pool中脏数据会根据特定触发条件写入到Double write buffer中,Double write buffer分两次写,每1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘。

浅谈MySQL数据库持久化过程

文章作者:陈举超

排版设计:王蔚棋

手绘插画:岳   媛

原创文章,作者:EBCloud,如若转载,请注明出处:https://www.sudun.com/ask/33626.html

Like (0)
EBCloud的头像EBCloud
Previous 2024年4月2日 下午3:28
Next 2024年4月2日 下午3:28

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注