当前位置: 首页 > news >正文

MySQL深度理解-Innodb底层原理

1.MySQL的内部组件结构

        大体来说,MySQL可以分为Server层和存储引擎层两部分。

2.Server层

        Server层主要包括连接器、查询缓存、分析器、优化器和执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数据和加密函数),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

        下面详细介绍一些Server层的这些组件。

2.1连接器

        我们知道由于MySQL是开源的,他有非常多种类的客户端:navicat,mysql,front,jdbc,SQLyog等非常丰富的客户端,包括各种编程语言实现的客户端连接程序,这些客户端要向MySQL发起通信都必须先跟Server端建立通信连接,而建立连接的工作就是有连接器完成的。

        第一步,客户端会先连接到这个数据库上,这时候首先处理连接的是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:

mysql -h host[数据库地址] -u root[用户] -p root [密码] -p 3306

        连接命令中的mysql是客户端工具,用来和服务端建立连接。在完成经典的TCP握手后,连接器就要开始认证你的身份,这个时候用到就是你输入的用户名和密码。

        连接过程中进行认证身份时,认证成功和失败会触发两种处理模式:

        1.如果用户名或者密码不对,就会收到一个'Access denied for user'的错误,然后客户端结束执行。

        2.如果用户名密码认证通过,连接器就会到权限表查询中当前认证通过的用户拥有的权限。随后,这个连接里面的权限判断逻辑,都依赖于此时读取到的权限。

        权限的认证处理逻辑意味着,一个用户成功建立连接之后,即使使用更高权限的账号对该账号进行权限修改之后,也不会影响已经存在的连接的权限。修改完成之后,只有再新建的连接才会使用新的权限设置。

2.2查询缓存

        连接建立完成之后,就可以执行SELECT语句了。Server层的执行逻辑就到了第二步:查询缓存。

        MySQL拿到一个查询请求后,会先到查询缓存查看是否执行过这条SQL语句。之前执行过的语句及其结果都会以key-value键值对的形式去缓存到内存中。key时查询的语句,value是查询的结果。如果查询的语句能够在缓存中直接找到,就会将查询语句对应的缓存值数据返回给客户端。

        如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成之后,执行结果会存入到查询缓存中。

        MySQL借助查询缓存来提高系统的执行效率,缓存中有的查询语句可以直接将对应的数据返回,无需再走后续的查询流程了。

        但是在MySQL8.0已经将查询缓存删除了,因为这个查询缓存比较鸡肋的。

        为什么大多数情况下这个查询缓存比较鸡肋呢?

        因为查询缓存往往弊大于利。查询缓存的失效是非常频繁的,只要有对一个表的更新,这个表上所有查询缓存都会被清空。因此很可能虽然花了一些额外操作将查询结果存储成功了,还没有进行使用呢,结果发生了一次数据更新,其中的数据全部被清空了。对于更新比较频繁的数据库来说,查询缓存的命中率是很低的,所以这个查询缓存是比较鸡肋的。

        一般建议大家在静态表中使用查询缓存。什么是静态表呢?就是一般我们极少更新的表。比如,一个系统配哦之表,字典表等。那这张表上的查询才适合使用查询缓存。

        MySQL也提供了按需使用的方式,在8.0版本之前,如果不想使用查询缓存,可以将my.cnf的参数query_cache_type设置为DEMAND。

        在my.cnf的配置文件中,query_cache_type可以设定三个值,0代表关闭查询缓存OFF,1代表开启ON,2(DEMAND)代表SQL语句中有SQL_CACHE关键词才缓存。

query_cache_type=2

        这样对于默认的SQL语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以使用SQL_CACHE关键字显式指定,像下面的这个语句一样:

SELECT SQL_CACHE * FROM test WHERE id = 5;

        可以使用下面的语句查看是否开启了缓存机制:

SHOW GLOBAL VARIABLES LIKE "%query_cache_type%";

        总体来看,查询缓存是一个比较鸡肋的功能,5.7版本的MySQL也建议关闭。

2.3分析器

        如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道SQL语句的含义是什么,因此需要对SQL语句进行解析,此时就需要使用词法分析器了。

        分析器会先进行“词法分析”。执行的SQL指令是由多个字符串和空格组成的一条SQL语句,MySQL需要识别出里面的字符串分别表示什么,代表什么。

        假设现在执行的是下面的SQL语句:

SELECT * FROM test WHERE id = 1;

        MySQL的词法分析器会通过SQL语句中的SELECT关键字识别出,这是一个查询语句。也要把字符串test识别为“数据表名称test”。将字符id识别为“数据列id”。

        进行了这些词法的识别之后,就要进行“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断执行的SQL语句是否满足MySQL的语法规则。

        如果输出的SQL语法不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个错误的SQL语句:

SELECT * FROME t1 WHERE id = 1;

        执行了这个错误的SQL语句,MySQL的分析器发现SQL语句语法出现了错误,便会抛出1064的错误:

[42000][1064] You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FROME t1 WHERE id = 1' at line 1

        下面是SQL分析的整体分析流程:

        开始的时候会先进入词法分析阶段,在词法分析的过程中会检测是否到达了终结符,如果没有到达终结符就会继续执行词法分析,如果到达了终结符,就会继续向下执行到语法分析阶段。

        语法分析阶段,会逐渐将词法分析的结果投喂给分析机,如果解析成功就会添加到AST树中,如果分析失败就会返回到语法分析部分并抛出相应的错误。

        构建好AST树后,整个分析的阶段便完成了。

        SQL语句经过分析器分析完毕后,会形成下面这样的语法AST树:

        下面是InnoDB整体的执行流程:接下来就进入到了优化器阶段。

2.4优化器

        经过了分析器,MySQL已经得知你需要做什么了。在开始执行之前,还需要经过优化器的处理。

        优化器是在表里面有很多个索引的时候,决定使用哪个索引。或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序,以及一些MySQL内部的优化机制。

        在优化器这个执行流程中会生成执行计划在,执行计划可以使用explain查看到执行计划。

2.5执行器

        开始执行的时候,要先判断一下你的数据表是否有查询权限,如果没有,就会返回没有权限的错误(如果命中了查询缓存,也会在查询缓存返回结果前执行权限校验)

        由于执行器部分需要调度的是执行引擎去执行SQL语句,现在主要使用的执行引擎是InnoDB,所以我们着重介绍的是InnoDB执行引擎的底层调度执行原理,MyISAM等其它执行引擎不再介绍。

3.InnoDB底层原理

3.1.1client和Server层

        client和Server层的如下:

        client就是发送SQL语句到MySQL服务器的客户端,这里发送了一条SQL语句是“UPDATE t1 SET name = 'XingHai666' WHERE id = 1”。

        Server层刚刚已经介绍过了,Server层是用于执行除了底层引擎以外的通用部分,引擎决定的是执行SQL语句的部分,这部分回因为底层引擎的不同产生差异性。Server层主要做的是连接鉴权,词法分析、语法分析、SQL优化和查询缓存的部分。

        当Server层将客户端发送来的SQL语句进行解析优化完成后,会交给执行器去调度底层执行引擎执行代码。

3.1.2数据页加载

        进行数据更新操作的时候,不会直接去修改磁盘ibd文件中的数据,而是会进行一系列的操作之后,才会真正去修改磁盘ibd文件中的数据。

        首先第一步进行的是数据页的加载,下面是该步操作的涉及的具体组件:

        在InnoDB引擎中,有一个Buffer Pool缓存池,该缓存池用于从ibd文件中将对应数据一页的数据都拉取到Buffer Pool缓存池中。

        当目标数据所在页数据都被拉取到Buffer Pool中之后,就会进行第二步undo回滚链的构建。

        Buffer Pool是在内存中的,所以其性能是比较高的。

3.1.3undo回滚链的构建

        在前面我们有讲过,undo回滚链主要是用于进行事务回滚和事务数据可见性的,依靠undo日志回滚链可以实现事务的回滚操作以及读已提交,可重复读的现象。

        第二步就是进行了undo回滚链的构建:

        当数据被加载到InnoDB存储引擎的Buffer Pool缓存池之后,就会将写入更新的数据的旧值写入到undo回滚日志文件中。

        当事务提交失败之后,进行rollback回滚操作时,可以通过undo日志链中的数据回滚Buffer Pool缓存池中的数据(因为此时ibd磁盘中的数据还没有进行修改,所以ibd文件中的数据是无需修改的)

3.1.4更新缓存池中的数据

        当undo回滚链构建成功后,InnoDB引擎就会开始执行第三步了,更新Buffer Pool中的数据:

        将需要更新的数据更新到InnoDB的缓存池中,此时需要更新的数据就在缓存池中更新完成了。

3.1.5redo Log Buffer的构建

        当Buffer Pool缓存池中的数据更新完成后,就要开始构建redo日志链,redo日志链的作用是负责当数据库/服务器down掉之后,对数据进行恢复的,这个组件只有在InnoDB引擎中才会有,是非常重要的部分,可以保障数据库中数据的安全。

        下面展示了了Redo Log Buffer缓存池的构建的组件部分:

        Redo Log Buffer是位于InnoDB存储引擎中的,是一个内存区域,执行器可以将redo日志写入到这块内存区域中,以提高整体日志的写入性能。

3.1.6redoLog顺序写入磁盘

        当执行器将redoLog写入到Redo Log Buffer之后,还需要将redoLog顺序写到磁盘中。之所以进行磁盘顺序写操作,是因为刚刚介绍过redoLog重做日志是用于在系统宕机时,帮助系统恢复没有写入到ibd文件中的数据的。但是如果将数据仅仅是存储在Redo Log Buffer内存区域中,系统宕机后,redoLog数据还是会消失,所以就需要将数据持久化到硬盘中,这样宕机之后数据就不会丢失了。

        下面展示的是从Redo Log Buffer中拉取数据磁盘顺序写到磁盘中,完成持久化。

        InnoDB引擎会从Redo Log Buffer拉取数据,顺序写入到磁盘已经准备好的相关文件中。其中磁盘中并不是存储了修改了xx数据,而是进行物理修改,在磁盘中存储的是在哪一页做了什么修改。当需要通过redo log进行恢复数据的时候,可以直接根据里面存储的数据,进行恢复物理上的数据。

        需要注意的是,redo log buffer写入数据到redo日志文件中,并不是直接将redo log buffer中的数据同步写入到redo日志文件中的,而是借助了一个Page Cache来提高整体数据同步的性能。整体的数据流转流程如下:

        PageCache在redo log buffer和redo日志文件中间作为中间层负责承载加速,提高数据的流转速度,这里的设计类似于CPU和内存通信的设计,CPU运算速度远大于内存加载数据的速度,为了可以更好的提高性能,使用高速缓存解决了该问题。对于Page Cache也是,buffer内存的速度远大于日志文件磁盘的速度,引入Page Cache可以提高一定的性能。

        需要注意的是,PageCache是建立在操作系统级别的,而不是数据库级别的。

        redo log日志文件数据恢复的机制?

        当事务提交了之后,buffer pool中的数据还没有同步写入到ibd文件中时,此时系统宕机了,可以使用redo log中的日志数据恢复磁盘ibd文件里的数据。

        为什么要使用redo log日志来实现数据的恢复?直接写入到ibd文件中不行吗?

        我们需要搞清楚的是,数据更改写入ibd文件和redo log日志文件磁盘写入机制的不同,首先磁盘写入ibd文件使用的是随机读写操作,redo log日志写入磁盘使用的是磁盘顺序写,对于机械硬盘来说,磁盘顺序写的性能是远高于磁盘随机写,因为机械硬盘是一种物理的存储结构,读取数据时是在机械圆盘上进行寻道查询物理地址,如果是磁盘顺序写,那寻道速度特别快,但是如果是磁盘随机写,寻道的时候非常费劲,所以对于机械硬盘来说,磁盘随机写的性能是远低于磁盘顺序写的。MySQL基于这个考虑,使用redo log可以很迅速的将更改的数据持久化存储下来,防止数据丢失。

        仔细思考一下,如果没有redo log机制,直接将数据写入到ibd文件中,由于要进行的是磁盘随机写,如果事务特别多,都进行提交操作,需要耗费的性能是比较高的,所以如果在写入到ibd文件的途中,由于花费的事件比较长,如果系统突然宕机了,则可能会导致数据丢失。但是如果是使用redo log日志,进行磁盘顺序写时,写入的速度是非常快的,这样就可以尽可能的减少更改数据丢失的可能性。

        但是现代化服务器中,大部分服务器都会采用SSD存储数据,对于SSD来说,磁盘随机写和磁盘顺序写的性能差距不大,这个优化其实是可以不做的,但是MySQL设计的时代,机械硬盘是主流,这个优化手段可以明显提高数据的安全性。

        为什么ibd数据写入磁盘是使用的磁盘随机写呢?

        ibd数据写入磁盘是使用磁盘顺序写的原因主要是:不同的数据表对应着不同的不同的ibd文件,比如执行10条SQL语句,修改不同的表,需要修改不同表的ibd文件,不能实现顺序写文件的效果。

        为什么redo log日志写入磁盘是使用的磁盘顺序写呢?

在InnoDB底层的设定出存储磁盘空间重,准备了专门存储redo log日志的文件,存储数据时,会将日志数据追加到日志文件的末尾,所以就实现了磁盘顺序写。

        总结:写redo log后刷新数据表文件的机制交WAL机制(Write-Ahead Logging),效率更高。

3.1.7binlog写入磁盘

        当redoLog被顺序写入磁盘之后,就要开始进行下一步操作,准备写入binlog数据,binlog数据是为了进行归档回滚,用于将数据恢复之前未修改的状态,可以防止删库跑路,进行数据版本回滚等。

        下面是binlog日志写入磁盘的操作:

        当redo log数据被顺序写入到磁盘之后,就开始了下一步操作,将binlog日志数据写入到磁盘中,这一步操作其实是属于Server层的操作,也就是说不是InnoDB独有的操作,所有的执行引擎,都会执行binlog日志归档操作。

        binlog归档日志里面一般存储的是SQL数据,可以借助里面的SQL语句进行相关的回滚操作。

        与redo log一样,binlog也使用了PageCache进行加速,执行器会先将数据写入到PageCache中,由PageCache将数据同步到binlog归档日志文件的磁盘中。

3.1.8写入commit标记到redo日志文件中

        在binlog归档日志写入完成之后,InnoDB引擎会将commit标记写入到redo日志文件中,提交事务完成之后,该标记可以保证事务提交后redo于binlog数据一致。

        下面是写入commit标记到redo日志文件中的操作:

        具体介绍一下为什么要在binlog日志文件写入成功之后,需要将commit标记写入redo log日志中:

        当事务提交成功的时候,才会响应回去commit成功的信息。具体在什么时候返回呢?为了保证数据可以回滚,需要等待redo log日志文件和binlog日志文件中的数据均写入成功之后,才会响应回去commit成功的信息,目的是保证redo log数据和binlog数据的一致性。

        当事务提交后,会检测redo日志中是否有commit标记,如果有这个标记才会响应提交成功,如果没有这个标记,就会等着redo log和binlog同步成功后再返回。

3.1.9异步IO线程更新数据

        InnoDB底层执行的最后一步是异步IO线程将Buffer Pool中存储的已经被更新过的数据,异步更新到磁盘的IBD存储文件中。

        下面是异步IO线程更新数据的流程:

        当redo log和binlog数据都被记录好之后,异步IO线程会去Buffer Pool中拉取数据,拉取数据时,是将更改数据整页的数据都拉取到,以页page为单位,将数据更新到磁盘ibd文件中。

        至此,InnoDB引擎的整体执行流程介绍完毕。

3.2梳理redoLog的关键参数和存储机理

3.2.1redoLog buffer大小参数设置

        MySQL的redoLog buffer的大小是可以通过参数进行配置的。

        这个参数可以使用innodb_log_buffer_size配置redo log buffer的大小参数,默认是16M,最大值是4096M,最小值是1M.

        可以使用下面的语句查询innodb_log_buffer_size参数对应的配置参数:

SHOW VARIABLES LIKE '%innodb_log_buffer_size%'

        查询的结果如下:

        可以从查询结果中看出redo log buffer的内存大小设置为16M。

3.2.2innodb_log_group_home_dir参数设置

        innodb_log_group_home可以设置redo log文件存储位置参数,默认值是"./",即innodb数据文件存储位置(mysql/data文件下面)。

        可以使用下面的语句查询innodb_log_group_home_dir参数的值:

SHOW VARIABLES LIKE '%innodb_log_group_home_dir%';

        查询出的数据如下:

        从查询出来的数据可以看出,innodb_log_group_home_dir设置的值是".\",即mysql/data文件夹下面。

        可以查看到redo log的磁盘文件存储在mysql/data下面。

3.2.3innodb_log_files_in_group参数设置

        innodb_log_files_in_group参数可以设置redo log文件的个数,命名方式为:ib_logfile0,ib_logfile1...ib_logfileN,默认是2个,最大100个。

        可以通过下面的SQL语句查看innodb_log_files_in_group的参数设置:

SHOW VARIABLES LIKE '%innodb_log_files_in_group%';

        查询结果如下:

        可以看到默认的redo log日志文件的默认值是两个。

3.2.4innodb_log_file_size参数设置

        innodb_lof_file_size参数可以设置单个redo log文件的大小,默认值是48M,最大值是512G,注意这里指的最大值是整个redo log系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size)不能大于最大值512G。

        可以通过下面的SQL语句查看innodb_log_file_size的参数设置:

SHOW VARIABLES LIKE '%innodb_log_file_size%'

        查询结果如下:

        可以看到innodb_log_file_size的默认配置就是48M。

3.2.5redo log写入磁盘过程分析

        前面已经将redo log所有的参数已经介绍完毕了,现在主要介绍一下redo log写入磁盘的整个过程。

        我们知道redo log是由多个日志文件组成的,那么它是如何完成磁盘顺序读取呢?文件之前是如何寻址的呢,接下来我们一探究竟。

        redo log是从头开始写的,写完一个文件继续写另一个文件,写到最后一个文件的末尾,就又回到第一个文件开头循环写,类似一个循环数组结构。

        假设redo log由四个文件组成,下面是redo log文件的整体读取流程图:

        ib_logfile所有的文件以前后相连的状态,组成一个环形数据存储状态。

        在整个体系中,由两个数据指针,一个指针是write pos,即写入位置;另一个指针式check point,即检查点。

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

        check point是当前要擦除的位置,也是往后推移并且循环的,擦除记录前需要将记录更新到数据文件上。

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

        接下来我们分析一下redo log的写入策略。

3.2.6redo log写入策略

        在MySQL的配置中,有一个参数innodb_flush_log_at_trx_commit。

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

        1.设置为0:表示每次事务提交时都只是将redo log留在redo log buffer中,数据库宕机可能会丢失数据。

        2.设置为1(默认值):表示每次事务员提交时都将redo log直接持久化到磁盘,数据最安全,不会因为数据库宕机就丢失数据,但是效率会稍微差一些,线上系统推荐这个设置。

        3.设置为2:每次事务提交时,都是将redo log日志直接写入到Page Cache中,这种情况如果数据库宕机是不会丢失数据的,但是操作系统如果宕机了,Page Cache中的数据还没有来得及写入磁盘文件的话就会丢失数据。

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

        下面是redo log写入策略的流程图:

        可以使用下面的语句查询innodb_flush_log_at_trx_commit的参数值:

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

        下面是查询的结果:

        通过查询的结果可以发现,innodb_flush_log_at_trx_commit的默认值是1,即可理解为当redo log写入redo log buffer之后,会立刻拉取到Page Cache中,并拉取到redo log磁盘文件中。

        可以通过以下语句设置innodb_flush_log_at_trx_commit的参数值,也可以在my.ini或者my.cnf文件中配置:

SET GLOBAL innodb_flush_log_at_trx_commit = 2;

        执行语句后,我们再去执行下面语句去查看innodb_flush_log_at_trx_commit的值:

        可以看到innodb_flush_log_at_trx_commit参数值已经被修改为2了,所以现在的redo log的写入策略已经被修改为将redo log日志写入到redo log buffer中,紧接着将redo log日志写入到Page Cache中,不会再将数据拉取到redo log磁盘文件了,需要等待异步线程定时拉取更新。

        但是还是建议大家将值修改为1(默认值),因为redo log写入策略为1时安全属性是最高的,即使是MySQL/操作系统宕机,都不会出现数据丢失的现象。

3.3梳理binlog二进制归档日志

        binlog归档日志用于数据修改后的回滚操作,所有引擎都会有这部分,归属于Server层。

3.3.1基础分析

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

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

        可以使用下面的语句查看binlog的相关日志:

-- 查看binlog相关参数
SHOW VARIABLES LIKE '%log_bin%'

        查询的结果如下:

        详细介绍这些参数:

        1.log_bin:binlog日志是否处于打开状态。

        2.log_bin_basename:binlog日志的基本文件名称,后面会追加标识来表示每一个文件,binlog日志文件会滚动增加。

        3.log_bin_index:指定的是binlog文件的索引文件,这个文件管理了所有的binlog文件的目录。

        4.sql_log_bin:sql语句语句是否写入binlog文件,ON代表需要写入,OFF代表不需要写入,如果想要在主库上执行一些操作,但不复制到slave从库上,可以通过修改sql_log_bin来实现。比如说,模拟主从同步复制异常。即开启sql_log_bin时,是可以正常执行主库的操作以及主从同步的操作的;关闭sql_log_bin是,是可以完成主库的相关的操作的,但是主从同步的时候会出现各种异常BUG。

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

        MySQL5.7必须使用修改配置文件的方式来修改binlog的相关配置,因为修改了配置之后,重启才会生效,使用SET GLOBAL这种语句,它并不会做一个持久化的修改,重启之后修改的配置就会失效了。但是MySQL8是可以直接通过SET GLOBAL的方式去修改配置,因为MySQL对于这种指令式全局修改参数之后,会做持久化处理,重启后修改的配置依旧生效,所以是可以使用全局指令的方式修改参数后重启的,这样参数也会生效的。

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

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

在数据库的data数据文件夹中可以看到我们所有的binlog相关的日志文件:

        前面这些binlog.xxxxx文件都是binlog日志文件,binlog.index文件是binlog文件的索引文件,在这个文件中管理了所有的binlog文件的目录。

        可以执行下面的命令查看有多少个binlog文件:

SHOW binary logs;

        查询结果如下:

        可以看到当前的数据库中还是又很多binlog文件的,可能是因为重启的次数较多,因为每次重启都会新建一个binlog文件。

3.3.2binlog的日志格式

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

        1.STATEMENT:基于SQL语句的复制,每一条修改数据的SQL都会被记录到master机器(主节点机器)的bin-log中,这种方式日志量小,节约IO开销,提升性能,但是对于一些执行过程中才能确定结果的函数,比如UUID(),SYSDATE()等函数如果随SQL同步到slave机器(从节点机器)去执行,则结果会和master机器执行的不一样。

        2.ROW:基于行的复制,日志中会记录每一行数据被修改的形式,然后在slave端再对相同的数据进行修改记录中每一行数据修改的细节,可以解决函数、存储过程在slave机器的复制问题,但是这种方式日志量较大,性能不如Statement。举个例子,假设UPDATE语句更新10行数据,Statement方式只会记录这条SQL数据,但是ROW的方式就记录被修改的10行数据。

        3.MIXED:混合模式复制,实际就是前两种模式的结合,在Mixed模式下,MySQL会根据执行的每一条具体的SQL语句来区分对待记录的日志形式,也就是再Statement和Row之间选择一种,如果SQL里面有函数或者一些执行时才知道结果的情况,会选择Row,其它情况选择Statemment,更加推荐这一种根据情况抉择的。

3.3.3binlog写入磁盘机制

        binlog写入磁盘机制主要通过sync_binlog参数控制,默认值是0,现在详细介绍一下sync_binlog的参数配置:

        1.sync_binlog为0的时候,表示每次提交事务,都只需要write到Page Cache中,由系统自行判断什么时候调用fsync写入磁盘。虽然性能得到提升,但是机器宕机,Page Caceh里面的binlog会丢失,数据的安全性会下降。

        2.sync_binlog为1的时候,表示每次提交事务都会调用操作系统的write函数将binlog写入到Page Cache中,紧接着会调用fsync函数将binlog从Page Cache中拉取到binlog磁盘文件中,这种方式是最安全的。

        3.sync为N时(N > 1),表示每次提交都调用操作系统的write函数将binlog写入到Page Cache中,累计了N个事务后,才会调用操作系统函数fsync将binlog从Page Cache拉取到磁盘中,这种如果机器宕机会丢失N个事务的binlog。

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

        1.服务器启动或者重新启动。

        2.服务器刷新日志,执行命令flush logs。

        3.日志文件大小达到max_binlog_size值,默认值为1GB。

3.3.4删除binlog日志文件

        MySQL提供了三种删除binlog日志文件:

        1.删除当前所有二进制日志文件:

RESET master;

        2.删除指定日志文件之前的所有日志文件,下面这个时删除6之前的所有日志文件,当前指定的文件不会删除:

PURGE master logs TO 'binlog.00006';

        3.删除指定日期前的日志索引中的binlog日志文件:

PURGE master logs before '2023-01-21 14:00:00';

3.3.5查看binlog日志文件

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

        查看binlog二进制文件(命令行方式,无需登录MySQL):

 mysqlbinlog --no-defaults -v --base64-output=decode-rows "D:\devlop\MySQL Server 8.0\data\binlog.000005"

        也可以在后面追加一些查询条件:

mysqlbinlog --no-defaults -v --base64-output=decode-rows "D:\devlop\MySQL Server 8.0\data\binlog.000005" start-datetime="2025-01-21 00:00:00" stop-datetime="2025-01-22 00:00:00" start-position="5000" stop-position="20000"

        执行mysqlbinlog指令的命令:

 mysqlbinlog --no-defaults -v --base64-output=decode-rows "D:\devlop\MySQL Server 8.0\data\binlog.000005"

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

# 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
#250722 11:14:43 server id 1  end_log_pos 126 CRC32 0xa6f14657  Start: binlog v 4, server v 8.0.29 created 250722 11:14:43 at startup
ROLLBACK/*!*/;
# at 126
#250722 11:14:43 server id 1  end_log_pos 157 CRC32 0xb4cfc451  Previous-GTIDs
# [empty]
# at 157
#250722 11:50:18 server id 1  end_log_pos 234 CRC32 0x041f4720  Anonymous_GTID  last_committed=0        sequence_number=1       rbr_only=no     original_committed_timestamp=1753156218872739   immediate_commit_timestamp=1753156218872739    transaction_length=246
# original_commit_timestamp=1753156218872739 (2025-07-22 11:50:18.872739 中国标准时间)
# immediate_commit_timestamp=1753156218872739 (2025-07-22 11:50:18.872739 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753156218872739*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 234
#250722 11:50:18 server id 1  end_log_pos 403 CRC32 0xd3415446  Query   thread_id=13    exec_time=0     error_code=0   Xid = 314
use `test`/*!*/;
SET TIMESTAMP=1753156218/*!*/;
SET @@session.pseudo_thread_id=13/*!*/;
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*//*!*/;
/*!80013 SET @@session.sql_require_primary_key=0*//*!*/;
/* ApplicationName=IntelliJ IDEA 2024.2.3 */ alter table t1add name int null
/*!*/;
# at 403
#250722 11:50:39 server id 1  end_log_pos 482 CRC32 0xa17a6bf0  Anonymous_GTID  last_committed=1        sequence_number=2       rbr_only=no     original_committed_timestamp=1753156239233279   immediate_commit_timestamp=1753156239233279    transaction_length=259
# original_commit_timestamp=1753156239233279 (2025-07-22 11:50:39.233279 中国标准时间)
# immediate_commit_timestamp=1753156239233279 (2025-07-22 11:50:39.233279 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753156239233279*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 482
#250722 11:50:39 server id 1  end_log_pos 662 CRC32 0x545f2147  Query   thread_id=13    exec_time=0     error_code=0   Xid = 396
SET TIMESTAMP=1753156239/*!*/;
/*!80013 SET @@session.sql_require_primary_key=0*//*!*/;
/* ApplicationName=IntelliJ IDEA 2024.2.3 */ alter table t1modify name varchar(32) null
/*!*/;
# at 662
#250722 11:50:56 server id 1  end_log_pos 741 CRC32 0x2195b99f  Anonymous_GTID  last_committed=2        sequence_number=3       rbr_only=yes    original_committed_timestamp=1753156256486964   immediate_commit_timestamp=1753156256486964    transaction_length=311
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1753156256486964 (2025-07-22 11:50:56.486964 中国标准时间)
# immediate_commit_timestamp=1753156256486964 (2025-07-22 11:50:56.486964 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753156256486964*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 741
#250722 11:50:56 server id 1  end_log_pos 825 CRC32 0xc8545129  Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1753156256/*!*/;
BEGIN
/*!*/;
# at 825
#250722 11:50:56 server id 1  end_log_pos 882 CRC32 0x386275fa  Table_map: `test`.`t1` mapped to number 114
# at 882
#250722 11:50:56 server id 1  end_log_pos 942 CRC32 0xca43cc93  Update_rows: table id 114 flags: STMT_END_F
### UPDATE `test`.`t1`
### WHERE
###   @1=1
###   @2=1
###   @3=NULL
### SET
###   @1=1
###   @2=1
###   @3='LiLei'
# at 942
#250722 11:50:56 server id 1  end_log_pos 973 CRC32 0xe055a72b  Xid = 480
COMMIT/*!*/;
# at 973
#250722 11:58:59 server id 1  end_log_pos 1052 CRC32 0xc7768802         Anonymous_GTID  last_committed=3        sequence_number=4       rbr_only=yes    original_committed_timestamp=1753156739427840   immediate_commit_timestamp=1753156739427840     transaction_length=292
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1753156739427840 (2025-07-22 11:58:59.427840 中国标准时间)
# immediate_commit_timestamp=1753156739427840 (2025-07-22 11:58:59.427840 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753156739427840*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 1052
#250722 11:58:59 server id 1  end_log_pos 1127 CRC32 0xc33076fc         Query   thread_id=12    exec_time=0     error_code=0
SET TIMESTAMP=1753156739/*!*/;
BEGIN
/*!*/;
# at 1127
#250722 11:58:59 server id 1  end_log_pos 1184 CRC32 0x236c879e         Table_map: `test`.`t1` mapped to number 114
# at 1184
#250722 11:58:59 server id 1  end_log_pos 1234 CRC32 0xedad9f38         Write_rows: table id 114 flags: STMT_END_F
### INSERT INTO `test`.`t1`
### SET
###   @1=2
###   @2=2
###   @3='NiuMa'
# at 1234
#250722 11:58:59 server id 1  end_log_pos 1265 CRC32 0x0f5ef106         Xid = 522
COMMIT/*!*/;
# at 1265
#250722 11:59:27 server id 1  end_log_pos 1344 CRC32 0x086cb601         Anonymous_GTID  last_committed=4        sequence_number=5       rbr_only=yes    original_committed_timestamp=1753156767438213   immediate_commit_timestamp=1753156767438213     transaction_length=292
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1753156767438213 (2025-07-22 11:59:27.438213 中国标准时间)
# immediate_commit_timestamp=1753156767438213 (2025-07-22 11:59:27.438213 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753156767438213*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 1344
......

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

3.3.6binlog日志文件恢复数据

        用binlog日志文件恢复数据其实就是回放执行之前记录在binlog里的SQL。

        举一个数据恢复的例子:

        1.先向数据表中插入相关的数据:

INSERT INTO `test`.`t1` (id, name, balance) VALUES (21, 'aa', 1000);
INSERT INTO `test`.`t1` (id, name, balance) VALUES (22, 'bb', 2000);

        2.然后将插入的新数据删除,模拟数据被不小心删除的场景:

DELETE FROM `test`.`t1` WHERE id IN (21, 22);

        3.查询binlog日志中记录的信息:

# at 1052
#250726 14:29:16 server id 1  end_log_pos 1131 CRC32 0x1edaff52         Anonymous_GTID  last_committed=3        sequence_number=4       rbr_only=yes    original_committed_timestamp=1753511356228725   immediate_commit_timestamp=1753511356228725     transaction_length=289
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1753511356228725 (2025-07-26 14:29:16.228725 中国标准时间)
# immediate_commit_timestamp=1753511356228725 (2025-07-26 14:29:16.228725 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753511356228725*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 1131
#250726 14:29:16 server id 1  end_log_pos 1206 CRC32 0xf51a85bb         Query   thread_id=9     exec_time=0     error_code=0
SET TIMESTAMP=1753511356/*!*/;
BEGIN
/*!*/;
# at 1206
#250726 14:29:16 server id 1  end_log_pos 1263 CRC32 0x544a85c4         Table_map: `test`.`t1` mapped to number 83
# at 1263
#250726 14:29:16 server id 1  end_log_pos 1310 CRC32 0xdca8dd01         Write_rows: table id 83 flags: STMT_END_F
### INSERT INTO `test`.`t1`
### SET
###   @1=21
###   @2='aa'
###   @3=1000
# at 1310
#250726 14:29:16 server id 1  end_log_pos 1341 CRC32 0xc84bf74e         Xid = 77
COMMIT/*!*/;
# at 1341
#250726 14:29:16 server id 1  end_log_pos 1420 CRC32 0x19c122ed         Anonymous_GTID  last_committed=4        sequence_number=5       rbr_only=yes    original_committed_timestamp=1753511356244868   immediate_commit_timestamp=1753511356244868     transaction_length=289
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
# original_commit_timestamp=1753511356244868 (2025-07-26 14:29:16.244868 中国标准时间)
# immediate_commit_timestamp=1753511356244868 (2025-07-26 14:29:16.244868 中国标准时间)
/*!80001 SET @@session.original_commit_timestamp=1753511356244868*//*!*/;
/*!80014 SET @@session.original_server_version=80029*//*!*/;
/*!80014 SET @@session.immediate_server_version=80029*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 1420
#250726 14:29:16 server id 1  end_log_pos 1495 CRC32 0x4ff035b9         Query   thread_id=9     exec_time=0     error_code=0
SET TIMESTAMP=1753511356/*!*/;
BEGIN
/*!*/;
# at 1495
#250726 14:29:16 server id 1  end_log_pos 1552 CRC32 0x20cce7f3         Table_map: `test`.`t1` mapped to number 83
# at 1552
#250726 14:29:16 server id 1  end_log_pos 1599 CRC32 0xce583b8c         Write_rows: table id 83 flags: STMT_END_F
### INSERT INTO `test`.`t1`
### SET
###   @1=22
###   @2='bb'
###   @3=2000
# at 1599
#250726 14:29:16 server id 1  end_log_pos 1630 CRC32 0xe6c91b72         Xid = 84
COMMIT/*!*/;
# at 1630

        4.找到两条插入数据的SQL,每条SQL的上下都有BEGIN和COMMIT,我们找到了一条sql BEGIN前面的文件位置标识at 1052(这里时文件的位置标识),再找到第二条SQL COMMIT后面的文件位置标识at 1630。

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

mysqlbinlog --no-defaults --start-position=1052 --stop-position=1630 --database=test "D:\devlop\MySQL Server 8.0\data\binlog.000015" | mysql -uroot -p123456 -v test

        使用如下的SQL查询数据是否被恢复了:

SELECT * FROM `test`.`t1` WHERE id IN (21, 22);

        查询结果如下:

        可以看到被删除的数据已经全部恢复了。

3.3.7binlog如何合理做到防止程序员删库跑路

        如果需要恢复大量的数据,比如程序员经常说到的删库跑路的话题,假设将数据库中的数据都删除了该如何恢复呢?如果数据库之前没有备份,所有binlog日志都在的化,就可以从binlog的第一个文件开始逐个恢复每个binlog里面的数据。但是这种情况一般是不太可能的,因为binlog日志文件比较大,早期的binlog文件是会定期删除的,所以一般不可能用binlog文件恢复整个数据库的。

        一般合理的方案是:

        1.每天凌晨对数据库做一次全量备份。

        2.开启binlog,并且设定一个定时删除binlog的时间,一般时间就是在备份的时间的附近删除。

        3.当需要恢复数据库时,可以使用最近一次全量备份再加上备份时间点之后的binlog来恢复数据。

        备份数据库一般可以使用mysqldump命令工具对数据库进行恢复:

        1.备份整个数据:

mysqldump -uroot -p test > test.sql
Enter password: ******

        可以发现此时数据库中所有的数据表数据都被备份了:

        2.备份整个表:

mysqldump -uroot -p test t1 > test-t1.sql
Enter password: ******

        可以发现此时数据库中t1数据表中的数据都被备份了:

        3.恢复整个库:

mysqldump -uroot -p test < test.sql

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

        binlog是早于redo log出现的,binlog位于Server层,是所有的执行引擎都会有的,redo log位于InnoDB的执行引擎层,只有InnoDB执行引擎才会有。

        在早期的MyISAM执行引擎中,是没有事务的概念的,即每一个SQL语句都是一个原子事务,不可以自己定义原子事务。

        只要原子事务提交成功之后,binlog就会记录日志,在原子事务执行成功前,系统崩溃了,binlog日志不会自动记录,数据也不会记录,即完成了自动回滚,但是如果原子事务执行成功了,并且进行了提交,但是磁盘的数据还没有修改成功,此时MySQL可以使用binlog日志中记录的数据来执行SQL恢复之前的数据。

        但是当另一个公司将InnoDB以插件的形式引入MySQL时,此时可以自定义多条SQL语句为原子事务了,开启一个事务之后,在事务中执行了多个数据修改操作之后,如果没有提交事务,宕机了,数据自然就不会被修改,因为根本没有提交,但是如果提交之后,宕机了,但是此时数据还没有刷新到数据库,但是因为binlog是做归档操作的,并不是用于做数据恢复工作的,所以InnoDB就引入了一套新的日志体系redo log,来解决在事务提交后没有刷新到硬盘时,因为数据库宕机导致的数据丢失问题。

        其中主要是保障了InnoDB的crash-safe能力。

3.4undo log回滚日志

3.4.1基础分析

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

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

        使用下面的语句查看undo log回滚日志的相关配置:

SHOW VARIABLES LIKE '%innodb_undo_%';

        查询结果如下:

        1.innodb_undo_directory:设置undo log文件所在的路径。该参数的默认值是‘.\’,即InnoDB数据文件存储位置,目录下ibdata1文件就是undo log存储的位置(当不设置innodb_undo_tablespaces的参数的时候,默认就是使用ibdata1文件来存储undolog日志的)

        2.innodb_undo_tablespaces:设置unlog log文件的数量,这样回滚段可以较为平均地分布多个文件中。设置该参数之后,会在路径innodb_undo_directory看到以undo为前缀的文件。

        在MySQL8.0.2之前可以使用innodb_undo_logs参数来设置MySQL支持的分段数,默认是128,在MySQL8被删除了,现在使用innodb_rollback_segments代替:

SHOW VARIABLES LIKE 'innodb_rollback_segments';

3.4.2undo log日志什么时候删除

        新增类型的,在事务提交之后就可以清除掉了。

        修改类型的,事务提交之后不能立即清除掉,这样日志需要用于MVCC机制,只有当没有事务被用到该版本信息时才可以清除。

3.5复杂日志设计面试题详解

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

        因为MySQL在执行SQL写入数据的时候,其写入磁盘的机理是磁盘随机写,对于MySQL当时的年代来说,磁盘随机写的性能太差了,所以直接更新磁盘文件是不能让MySQL数据库抗住很高的并发的。

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

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

        正式借助这套机制,MySQL才可以在配置较高的服务器上实现几千甚至上万的并发量。

3.6错误日志

        MySQL其中还有一个非常重要的机制是错误日志,它记录了数据库的启动和停止,以及运行过程中发生任何严重错误时的相关信息。

        当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。

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

        使用下面的语句可以查看MySQL错误日志存放位置:

SHOW VARIABLES LIKE '%log_error%';

        运行结果:

        找到log_error对应的错误日志存储地址,打开文件夹可以看到里面记录的一些错误日志:

3.7通用查询日志

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

        通用查询日志可以用来还原操作时的具体场景,帮助我们准确定位一些疑难问题,比如重复支付等问题。但是由于通用查询日志记录了所有的SQL指令及一些细节信息,所以其会消耗大量的系统资源并占用大量的磁盘空间,MySQL默认是关闭该日志记录的,建议只有当需要调试定位系统问题的时候才打开。

        使用下面的查询语句可以查看通用查询日志是否打开以及通用查询日甚至的存储位置:

SHOW VARIABLES LIKE '%general_log%';

        查询结果:

        根据配置变量general_log的值可以发现,默认情况下通用查询日志是关闭的,可以使用下面的语句开启通用查询日志:

SET GLOBAL general_log=on;

        根据配置变量general_log_file的值可以看到通讯查询日志存储在什么位置。

http://www.dtcms.com/a/334318.html

相关文章:

  • 设计模式之【快速通道模式】,享受VIP的待遇
  • Java基础 8.16
  • 【OpenGL】LearnOpenGL学习笔记09 - 材质、光照贴图
  • React手撕组件和Hooks总结
  • ★CentOS:MySQL数据备份
  • 学习安卓APP开发,10年磨一剑,b4a/Android Studio
  • CPP多线程2:多线程竞争与死锁问题
  • 企业级Java项目金融应用领域——银行系统
  • C#WPF实战出真汁09--【消费开单】--选择菜品
  • 驱动开发系列63 - 配置 nvidia 的 open-gpu-kernel-modules 调试环境
  • AI重构文化基因:从“工具革命”到“生态觉醒”的裂变之路
  • 【101页PPT】芯片半导体企业数字化项目方案汇报(附下载方式)
  • 在鸿蒙应用中快速接入地图功能:从配置到实战案例全解析
  • Nginx域名和IP兼容双方的API地址
  • GaussDB 数据库架构师修炼(十三)安全管理(3)-数据库审计
  • 使用npm/pnpm自身安装指定版本的pnpm
  • JavaWeb开发_Day14
  • 如何在 Ubuntu 24.04 Server 或 Desktop 上安装 XFCE
  • 我的世界Java版1.21.4的Fabric模组开发教程(十八)自定义传送门
  • 边缘计算及其特点
  • 学习日志35 python
  • Python Day30 CSS 定位与弹性盒子详解
  • CodeBuddy IDE深度体验:AI驱动的全栈开发新时代
  • 缓存一致性总线协议(Cache Coherence Protocols)的发展过程
  • LangChain4j:基于 SSE 与 Flux 的 AI 流式对话实现方案
  • Honor of Kings 100star (S40) 2025.08.16
  • 11-verilog的RTC驱动代码
  • 10-verilog的EEPROM驱动-单字节读写
  • OpenCV安装及配置
  • 机器学习核心概念精要:从定义到评估