Bootstrap

[MySQL] Innodb底层原理与MySQL日志机制深入剖析

目录

Innodb底层原理与Mysql日志机制

redo log重做日志关键参数

redo log 写入磁盘过程分析

binlog二进制归档日志

binlog 的日志格式

binlog写入磁盘机制

删除 binlog 日志文件

查看 binlog 日志文件

binlog日志文件恢复数据

为什么会有redo log和binlog两份日志呢?

undo log回滚日志

undo log日志什么时候删除

为什么Mysql不能直接更新磁盘上的数据而设置这么一套复杂的机制来执行SQL了?

错误日志

通用查询日志


Innodb底层原理与Mysql日志机制

7.client收到server返回的commit成功标记(redo、binlog成功写入数据)

redo log重做日志关键参数

#innodb_log_buffer_size:设置redo log buffer大小参数,默认16M ,最大值是4096M,最小值为1M。
show variables like '%innodb_log_buffer_size%';

#innodb_log_group_home_dir:设置redo log文件存储位置参数,默认值为"./",即innodb数据文件存储位置,其中的 ib_logfile0 和 ib_logfile1 即为redo log文件。
show variables like '%innodb_log_group_home_dir%';

#innodb_log_files_in_group:设置redo log文件的个数,命名方式如: ib_logfile0, iblogfile1... iblogfileN。默认2个,最大100个。
show variables like '%innodb_log_files_in_group%';

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

redo log 写入磁盘过程分析

redo log 从头开始写,写完一个文件继续写另一个文件,写到最后一个文件末尾就又回到第一个文件开头循环写,如下面这个图所示。

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。

checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件里。

write pos checkpoint 之间的部分就是空着的可写部分,可以用来记录新的操作。如果 write pos 追上checkpoint,表示redo log写满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

innodb_flush_log_at_trx_commit:这个参数控制 redo log 的写入策略,它有三种可能取值:

  • 设置为0:表示每次事务提交时都只是把 redo log 留在 redo log buffer 中,数据库宕机可能会丢失数据。
  • 设置为1(默认值):表示每次事务提交时都将 redo log 直接持久化到磁盘,数据最安全,不会因为数据库宕机丢失数据,但是效率稍微差一点,线上系统推荐这个设置。
  • 设置为2:表示每次事务提交时都只是把 redo log 写到操作系统的缓存page cache里,这种情况如果数据库宕机是不会丢失数据的,但是操作系统如果宕机了,page cache里的数据还没来得及写入磁盘文件的话就会丢失数据。

InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用操作系统函数 write 写到文件系统的 page cache,然后调用操作系统函数 fsync 持久化到磁盘文件。

redo log写入策略参看下图:

#查看innodb_flush_log_at_trx_commit参数值:
show variables like 'innodb_flush_log_at_trx_commit';
#设置innodb_flush_log_at_trx_commit参数值(也可以在my.ini或my.cnf文件里配置):
set global innodb_flush_log_at_trx_commit=1;  

binlog二进制归档日志

binlog二进制日志记录保存了所有执行过的修改操作语句,不保存查询操作。如果 MySQL 服务意外停止,可通过二进制日志文件排查,用户操作或表结构操作,从而来恢复数据库数据。

启动binlog记录功能,会影响服务器性能,但如果需要恢复数据或主从复制功能,则好处则大于对服务器的影响。

#查看binlog相关参数
show variables like '%log_bin%';

MySQL5.7 版本中,binlog默认是关闭的,8.0版本默认是打开的。上图中log_bin的值是OFF就代表binlog是关闭状态,打开binlog功能,需要修改配置文件my.ini(windows)或my.cnf(linux),然后重启数据库。

在配置文件中的[mysqld]部分增加如下配置:

#log-bin设置binlog的存放位置,可以是绝对路径,也可以是相对路径,这里写的相对路径,则binlog文件默认会放在data数据目录下
log-bin=mysql-binlog
#Server Id是数据库服务器id,随便写一个数都可以,这个id用来在mysql集群环境中标记唯一mysql服务器,集群环境中每台mysql服务器的id不能一样,不加启动会报错
server-id=1
#其他配置
binlog_format = row # 日志文件格式,下面会详细解释
expire_logs_days = 15 # 执行自动删除距离当前15天以前的binlog日志文件的天数, 默认为0, 表示不自动删除
max_binlog_size = 200M # 单个binlog日志文件的大小限制,默认为 1GB

重启数据库后我们再去看data数据目录会多出两个文件,第一个就是binlog日志文件,第二个是binlog文件的索引文件,这个文件管理了所有的binlog文件的目录。

#当然也可以执行命令查看有多少binlog文件
show binary logs;

show variables like '%log_bin%';

log_bin:binlog日志是否打开状态
log_bin_basename:是binlog日志的基本文件名,后面会追加标识来表示每一个文件,binlog日志文件会滚动增加
log_bin_index:指定的是binlog文件的索引文件,这个文件管理了所有的binlog文件的目录。
sql_log_bin:sql语句是否写入binlog文件,ON代表需要写入,OFF代表不需要写入。如果想在主库上执行一些操作,但不复制到slave库上,可以通过修改参数sql_log_bin来实现。比如说,模拟主从同步复制异常。

binlog 的日志格式

用参数 binlog_format 可以设置binlog日志的记录格式,mysql支持三种格式类型:

  • STATEMENT:基于SQL语句的复制,每一条会修改数据的sql都会记录到master机器的bin-log中,这种方式日志量小,节约IO开销,提高性能,但是对于一些执行过程中才能确定结果的函数,比如UUID()、SYSDATE()等函数如果随sql同步到slave机器去执行,则结果跟master机器执行的不一样。
  • ROW:基于行的复制,日志中会记录成每一行数据被修改的形式,然后在slave端再对相同的数据进行修改记录下每一行数据修改的细节,可以解决函数、存储过程等在slave机器的复制问题,但这种方式日志量较大,性能不如Statement。举个例子,假设update语句更新10行数据,Statement方式就记录这条update语句,Row方式会记录被修改的10行数据。
  • MIXED:混合模式复制,实际就是前两种模式的结合,在Mixed模式下,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种,如果sql里有函数或一些在执行时才知道结果的情况,会选择Row,其它情况选择Statement,推荐使用这一种。

binlog写入磁盘机制

binlog写入磁盘机制主要通过 sync_binlog 参数控制,默认值是 0。

  • 为0的时候,表示每次提交事务都只 write 到page cache,由系统自行判断什么时候执行 fsync 写入磁盘。虽然性能得到提升,但是机器宕机,page cache里面的 binlog 会丢失。
  • 也可以设置为1,表示每次提交事务都会执行 fsync 写入磁盘,这种方式最安全。
  • 还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write 到page cache,但累积N个事务后才 fsync 写入磁盘,这种如果机器宕机会丢失N个事务的binlog。

发生以下任何事件时, binlog日志文件会重新生成:

  • 服务器启动或重新启动
  • 服务器刷新日志,执行命令flush logs
  • 日志文件大小达到 max_binlog_size 值,默认值为 1GB

删除 binlog 日志文件

删除当前的binlog文件
reset master;
#删除指定日志文件之前的所有日志文件,下面这个是删除6之前的所有日志文件,当前这个文件不删除
purge master logs to 'mysql-binlog.000006';
#删除指定日期前的日志索引中binlog日志文件
purge master logs before '2023-06-21 14:00:00';

查看 binlog 日志文件

可以用mysql自带的命令工具 mysqlbinlog 查看binlog日志内容

mysql5.7版本和8.0.33版本使用的命令有区别具体如下(无标注的是5.7版本)

#查看bin-log二进制文件(命令行方式,不用登录mysql)
#mysql5.7版本
mysqlbinlog --no-defaults -v --base64-output=decode-rows D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000007 
#mysql 8.0.33版本
mysqlbinlog.exe --no-defaults -v --base64-output=decode-rows "E:\MySQL\MySQLData\MySQL Server 8.0\Data\MSID-bin.000026"

#查看bin-log二进制文件(带查询条件)
mysqlbinlog --no-defaults -v --base64-output=decode-rows D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000007 start-datetime="2023-01-21 00:00:00" stop-datetime="2023-02-01 00:00:00" start-position="5000" stop-position="20000"

执行mysqlbinlog命令

#mysql 8.0.33版本
mysqlbinlog.exe --no-defaults -v --base64-output=decode-rows "E:\MySQL\MySQLData\MySQL Server 8.0\Data\MSID-bin.000026"

查出来的binlog日志文件内容如下:

C:\Users\BUBBLE>mysqlbinlog.exe --no-defaults -v --base64-output=decode-rows "E:\MySQL\MySQLData\MySQL Server 8.0\Data\MSID-bin.000026"
# The proper term is pseudo_replica_mode, but we use this compatibility alias
# to make the statement usable on server versions 8.0.24 and older.
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#230626 19:59:54 server id 1  end_log_pos 126 CRC32 0xa74ae237  Start: binlog v 4, server v 8.0.33 created 230626 19:59:54 at startup
ROLLBACK/*!*/;
# at 126
#230626 19:59:54 server id 1  end_log_pos 157 CRC32 0xc2cd0205  Previous-GTIDs
# [empty]
# at 157
#230626 22:55:23 server id 1  end_log_pos 236 CRC32 0xf7c508dd  Anonymous_GTID  last_committed=0        sequence_number=1       rbr_only=yes    original_committed_timestamp=1687791323514128   immediate_commit_timestamp=1687791323514128    transaction_length=305
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1687791323514128 (2023-06-26 22:55:23.514128 中国标准时间)
# immediate_commit_timestamp=1687791323514128 (2023-06-26 22:55:23.514128 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1687791323514128*//*!*/;
/*!80014 SET @@session.original_server_version=80033*//*!*/;
/*!80014 SET @@session.immediate_server_version=80033*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 236
#230626 22:55:23 server id 1  end_log_pos 315 CRC32 0x08ff3f06  Query   thread_id=9     exec_time=0     error_code=0
SET TIMESTAMP=1687791323/*!*/;
SET @@session.pseudo_thread_id=9/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1168113696/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8mb4 *//*!*/;
SET @@session.character_set_client=255,@@session.collation_connection=255,@@session.collation_server=255/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
/*!80011 SET @@session.default_collation_for_utf8mb4=255*//*!*/;
BEGIN
/*!*/;
# at 315
#230626 22:55:23 server id 1  end_log_pos 379 CRC32 0x32848300  Table_map: `learning`.`account` mapped to number 104
# has_generated_invisible_primary_key=0
# at 379
#230626 22:55:23 server id 1  end_log_pos 431 CRC32 0xf59afc94  Write_rows: table id 104 flags: STMT_END_F
### INSERT INTO `learning`.`account`
### SET
###   @1=7
###   @2='dadada'
###   @3=100
# at 431
#230626 22:55:23 server id 1  end_log_pos 462 CRC32 0x2daddf6f  Xid = 130
COMMIT/*!*/;
# at 462
#230626 22:55:37 server id 1  end_log_pos 541 CRC32 0xdc6f1bc1  Anonymous_GTID  last_committed=1        sequence_number=2       rbr_only=yes    original_committed_timestamp=1687791337214553   immediate_commit_timestamp=1687791337214553    transaction_length=303
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1687791337214553 (2023-06-26 22:55:37.214553 中国标准时间)
# immediate_commit_timestamp=1687791337214553 (2023-06-26 22:55:37.214553 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1687791337214553*//*!*/;
/*!80014 SET @@session.original_server_version=80033*//*!*/;
/*!80014 SET @@session.immediate_server_version=80033*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 541
#230626 22:55:37 server id 1  end_log_pos 620 CRC32 0xb3fdab34  Query   thread_id=9     exec_time=0     error_code=0
SET TIMESTAMP=1687791337/*!*/;
BEGIN
/*!*/;
# at 620
#230626 22:55:37 server id 1  end_log_pos 684 CRC32 0x639d3175  Table_map: `learning`.`account` mapped to number 104
# has_generated_invisible_primary_key=0
# at 684
#230626 22:55:37 server id 1  end_log_pos 734 CRC32 0x596aaefe  Write_rows: table id 104 flags: STMT_END_F
### INSERT INTO `learning`.`account`
### SET
###   @1=8
###   @2='hhhh'
###   @3=1000
# at 734
#230626 22:55:37 server id 1  end_log_pos 765 CRC32 0x253f05e5  Xid = 137
COMMIT/*!*/;
# at 765
#230626 22:55:48 server id 1  end_log_pos 844 CRC32 0x583da4da  Anonymous_GTID  last_committed=2        sequence_number=3       rbr_only=yes    original_committed_timestamp=1687791348974352   immediate_commit_timestamp=1687791348974352    transaction_length=329
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1687791348974352 (2023-06-26 22:55:48.974352 中国标准时间)
# immediate_commit_timestamp=1687791348974352 (2023-06-26 22:55:48.974352 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1687791348974352*//*!*/;
/*!80014 SET @@session.original_server_version=80033*//*!*/;
/*!80014 SET @@session.immediate_server_version=80033*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 844
#230626 22:55:48 server id 1  end_log_pos 932 CRC32 0xf6e66d28  Query   thread_id=10    exec_time=0     error_code=0
SET TIMESTAMP=1687791348/*!*/;
BEGIN
/*!*/;
# at 932
#230626 22:55:48 server id 1  end_log_pos 996 CRC32 0xc99d5a2f  Table_map: `learning`.`account` mapped to number 104
# has_generated_invisible_primary_key=0
# at 996
#230626 22:55:48 server id 1  end_log_pos 1063 CRC32 0x0999e782         Update_rows: table id 104 flags: STMT_END_F
### UPDATE `learning`.`account`
### WHERE
###   @1=6
###   @2='haha'
###   @3=1500
### SET
###   @1=6
###   @2='hahau'
###   @3=1500
# at 1063
#230626 22:55:48 server id 1  end_log_pos 1094 CRC32 0x45277817         Xid = 156
COMMIT/*!*/;
# at 1094
#230627  0:26:17 server id 1  end_log_pos 1117 CRC32 0xce33e23a         Stop
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

C:\Users\BUBBLE>

能看到里面有具体执行的修改伪sql语句以及执行时的相关情况。

binlog日志文件恢复数据

用binlog日志文件恢复数据其实就是回放执行之前记录在binlog文件里的sql,举一个数据恢复的例子

#先执行刷新日志的命令生成一个新的binlog文件mysql-binlog.000008,后面我们的修改操作日志都会记录在最新的这个文件里
flush logs;
#执行两条插入语句
INSERT INTO `account` (`id`, `name`, `balance`) VALUES (7, 'dadada', 100);
INSERT INTO `account` (`id`, `name`, `balance`) VALUES (8, 'hhhh', 1000);
#假设现在误操作执行了一条删除语句把刚新增的两条数据删掉了
delete from account where id > 6;

现在需要恢复被删除的两条数据,我们先查看binlog日志文件

#mysql 8.0.33版本
mysqlbinlog.exe --no-defaults -v --base64-output=decode-rows "E:\MySQL\MySQLData\MySQL Server 8.0\Data\MSID-bin.000026"

文件内容和上面执行相同命令的一致;

找到两条插入数据的sql,每条sql的上下都有BEGIN和COMMIT,我们找到第一条sql BEGIN前面的文件位置标识 at 236(这是文件的位置标识),再找到第二条sql COMMIT后面的文件位置标识 at 765

我们可以根据文件位置标识来恢复数据,执行如下sql:

mysqlbinlog  --no-defaults --start-position=219 --stop-position=701 --database=test D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000009 | mysql -uroot -p123456 -v test

#补充一个根据时间来恢复数据的命令,我们找到第一条sql BEGIN前面的时间戳标记 SET TIMESTAMP=1674833544,再找到第二条sql COMMIT后面的时间戳标记 SET TIMESTAMP=1674833663,转成datetime格式
mysqlbinlog  --no-defaults --start-datetime="2023-1-27 23:32:24" --stop-datetime="2023-1-27 23:34:23" --database=test D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000009 | mysql -uroot -p123456 -v test
#MySQL8.0.33版本恢复数据命令
mysqlbinlog.exe --no-defaults --start-position=1148 --stop-position=2085 --database=learning "E:\MySQL\MySQLData\MySQL Server 8.0\Data\MSID-bin.000026" | mysql -uroot -p123456 -v learning

执行完毕被删除数据被恢复!

注意:如果要恢复大量数据,比如程序员经常说的删库跑路的话题,假设我们把数据库所有数据都删除了要怎么恢复了,如果数据库之前没有备份,所有的binlog日志都在的话,就从binlog第一个文件开始逐个恢复每个binlog文件里的数据,这种一般不太可能,因为binlog日志比较大,早期的binlog文件会定期删除的,所以一般不可能用binlog文件恢复整个数据库的。

一般我们推荐的是每天(在凌晨后)需要做一次全量数据库备份,那么恢复数据库可以用最近的一次全量备份再加上备份时间点之后的binlog来恢复数据。

备份数据库一般可以用mysqldump 命令工具

#mysqldump 是 MySQL 提供的一个实用程序,用于备份和导出数据库。
#-u root 指定要使用的用户名为 "root",这是 MySQL 的超级用户。

mysqldump -u root 数据库名>备份文件名.sql dump;   #备份整个数据库
mysqldump -u root 数据库名 表名字>备份文件名.sql;  #备份整个表

mysql -u root 数据库名 < 备份文件名 #恢复整个数据库
mysql -u root -p test < 备份文件名 #恢复指定数据库

为什么会有redo log和binlog两份日志呢?

因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。

Redo log 的 crash-safe 能力指的是在数据库发生意外崩溃或故障的情况下,通过 Redo log 的持久化和恢复机制,确保数据的一致性和持久性。

undo log回滚日志

InnoDB对undo log文件的管理采用段的方式,也就是回滚段(rollback segment) 。每个回滚段记录了 1024 个 undo log segment ,每个事务只会使用一个undo log segment。

在MySQL5.5的时候,只有一个回滚段,那么最大同时支持的事务数量为1024个。在MySQL 5.6开始,InnoDB支持最大128个回滚段,故其支持同时在线的事务限制提高到了 128*1024 。

innodb_undo_directory:设置undo log文件所在的路径。该参数的默认值为"./",即innodb数据文件存储位置,目录下ibdata1文件就是undo log存储的位置。
innodb_undo_logs: 设置undo log文件内部回滚段的个数,默认值为128。
innodb_undo_tablespaces: 设置undo log文件的数量,这样回滚段可以较为平均地分布在多个文件中。设置该参数后,会在路径innodb_undo_directory看到undo为前缀的文件。

undo log日志什么时候删除

  • 新增类型的,在事务提交之后就可以清除掉了。
  • 修改类型的,事务提交之后不能立即清除掉,这些日志会用于mvcc。只有当没有事务用到该版本信息时才可以清除。

为什么Mysql不能直接更新磁盘上的数据而设置这么一套复杂的机制来执行SQL了?

因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。

因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。

Mysql这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。

更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。

正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干甚至上万的读写请求。

错误日志

Mysql还有一个比较重要的日志是错误日志,它记录了数据库启动和停止,以及运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。

在MySQL数据库中,错误日志功能是默认开启的,而且无法被关闭。

#查看错误日志存放位置
show variables like '%log_error%';

通用查询日志

通用查询日志记录用户的所有操作,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL 数据库服务器的所有 SQL 指令等,如select、show等,无论SQL的语法正确还是错误、也无论SQL执行成功还是失败,MySQL都会将其记录下来。

通用查询日志用来还原操作时的具体场景,可以帮助我们准确定位一些疑难问题,比如重复支付等问题。

general_log:是否开启日志参数,默认为OFF,处于关闭状态,因为开启会消耗系统资源并且占用磁盘空间。一般不建议开启,只在需要调试查询问题时开启。

general_log_file:通用查询日志记录的位置参数。

show variables like '%general_log%';
#打开通用查询日志
SET GLOBAL general_log=on;
;