数据持久化的方式有很多种,下面结合各个持久化阶段,来看看MySQL数据库是如何实现数据持久化的。
| 第一阶段 |
数据直接写入到磁盘。
问题:
速度慢,通常情况下,磁盘写入速度比内存写入速度慢很多,如果在短时间内积压大量I/O请求,很难保证数据库性能。
| 第二阶段 |
解决方案:
数据先写入内存,后批量异步刷新到磁盘。
问题:
内存数据异步刷到磁盘过程中,数据还没完全写入磁盘,此时内存或系统崩溃,数据丢失了怎么办?
| 第三阶段 |
解决方案:redo
写入内存后,为了提高速度,并不马上写磁盘,后面会延时批量写入磁盘,同时为了数据安全,引入redo log,在内存写入数据时,会同时生成redo log数据,记录数据修改操作,用于崩溃恢复。
什么是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数据页大小16KB,操作系统页大小一般4KB,在执行一次mysql I/O写入时,对应4次OS I/O,如果操作系统4次I/O没有写入完成被异常中断了,这种情况被称为写失效(partial page write)。此时重启后,磁盘上就是存在不完整的数据页,就算使用redo log也是无法进行恢复。
| 第五阶段 |
解决方案:Double Write
在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的Double write buffer。通过Double write buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后调用fsync函数,同步磁盘,避免缓冲写带来的问题。
问题:
如果开启了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 两个阶段。
通过二阶段提交,可以解决如下场景问题:
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服务器或硬件故障,无法及时启动数据库,如何减少服务中断和数据损失。
| 第七阶段 |
解决方案:异步复制
如果存在从库,主库会将新增的数据产生的binlog日志通过binlog dump线程传给从库,从库通过I/O线程接收传来的日志并写入到relay log日志中,最后SQL线程解析relay log日志进行数据重放。
问题:
主从复制默认是异步复制的,在异步复制中,主库并不关心从库是否接收到完整的日志,直接会进行后面的提交操作,如果在从库还没接收完主库传来的binlog,这时主库故障,从库切换为主库,那么在新主库上读取的数据可能会有缺失,导致数据不一致。
| 第八阶段 |
解决方案:半同步复制
主库将新增的数据产生的的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秒以后半同步就会降级到异步复制,之后如果主从连接恢复,又会自动恢复到半同步,如果主从连接一直不恢复,主从复制类型就会一直是异步复制,同样存在异步复制的缺点。
总·结
数据持久化过程:
例如:执行下面语句,将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函数,同步磁盘。
文章作者:陈举超
排版设计:王蔚棋
手绘插画:岳 媛
原创文章,作者:EBCloud,如若转载,请注明出处:https://www.sudun.com/ask/33626.html