MySQL(Undo日志)

后面也会持续更新,学到新东西会在其中补充。
建议按顺序食用,欢迎批评或者交流!
缺什么东西欢迎评论!我都会及时修改的!
大部分截图和文章采用该书,谢谢这位大佬的文章,在这里真的很感谢让迷茫的我找到了很好的学习文章。我只是加上了自己的拙见我只是记录学习没有任何抄袭意思
MySQL 是怎样运行的:从根儿上理解 MySQL - 小孩子4919 - 掘金小册

先啰嗦一下之前说的表空间

前言

通用链表结构
在写入undo日志的过程中会使用到多个链表,很多链表都有同样的节点结构
在这里插入图片描述
想定位表空间内的某一个位置的话,只需指定页号以及该位置在指定页号中的页内偏移量即可。

  • Pre Node Page NumberPre Node Offset的组合就是指向前一个XDES Entry的指针
  • Next Node Page NumberNext Node Offset的组合就是指向后一个XDES Entry的指针。

整个List Node占用12个字节的存储空间。
链表的基节点。这个结构中包含了链表的头节点尾节点指针以及这个链表中包含了多少节点的信息。
在这里插入图片描述

  • List Length表明该链表一共有多少节点
  • First Node Page NumberFirst Node Offset表明该链表的头节点表空间中的位置。
  • Last Node Page NumberLast Node Offset表明该链表的尾节点表空间中的位置。

整个List Base Node占用16个字节的存储空间。
在这里插入图片描述

FIL_PAGE_UNDO_LOG页面

表空间其实是由许许多多的页面构成的,页面默认大小为16KB
这些页面有不同的类型
比如:
类型为FIL_PAGE_INDEX页面用于存储聚簇索引以及二级索引
类型为FIL_PAGE_TYPE_FSP_HDR页面用于存储表空间头部信息
类型为FIL_PAGE_UNDO_LOG页面是专门用来存储undo日志
在这里插入图片描述
FIL_PAGE_UNDO_LOG简称为Undo页面File HeaderFile Trailer是各种页面都有的通用结构
在这里插入图片描述
先就此打住!让我介绍一下其他东西加以理解!

事务回滚的需求

事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。
但是也有意外出现

  • 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
  • 情况二:程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前的事务的执行

这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,需要把东西改回原先的样子,这个过程就称之为回滚(英文名:rollback)
每当我们要对一条记录做改动时(这里的改动可以指INSERTDELETEUPDATE),都需要留一手 —— 把回滚时所需的东西都给记下来

  • insert语句,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
  • delete语句,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
  • update语句,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。

为了回滚而记录这种操作称之为撤销日志,英文名为undo log,也就是undo 日志
由于查询操作(SELECT并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志

事务id

给事务分配id的时机

一个事务可以是一个只读事务,或者是一个读写事务

  • 通过START TRANSACTION READ ONLY语句开启一个只读事务。

只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对临时表增、删、改操作。

  • START TRANSACTION READ WRITE语句开启一个读写事务,或者使用BEGIN、START TRANSACTION语句开启的事务默认也算是读写事务

读写事务中可以对表执行增删改查操作。
如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id,分配方式如下:

  • 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。
  • 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。
    有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id

trx_id隐藏列

在这里插入图片描述
对这个聚簇索引记录做改动的语句所在的事务对应的事务id而已(此处的改动可以是INSERTDELETEUPDATE操作)。

undo日志的格式

为了实现事务的原子性InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志
一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志第1号undo日志、…、第n号undo日志等,这个编号也被称之为undo no
这些undo日志是被记录到类型为FIL_PAGE_UNDO_LOG的页面中。
在这里插入图片描述

这些页面可以从系统表空间中分配,也可以从一种专门存放undo日志表空间,也就是所谓的undo tablespace中分配。

CREATE TABLE undo_demo (id INT NOT NULL,key1 VARCHAR(100),col VARCHAR(100),PRIMARY KEY (id),KEY idx_key1 (key1)
)Engine=InnoDB CHARSET=utf8;

id列主键,我们为key1列建立了一个二级索引col列是一个普通的列
InnoDB的数据字典,每个表都会被分配一个唯一的table id

information_schema.innodb_sys_tables在mysql5.7中有但在mysql9.0没有mysql> SELECT * FROM information_schema.innodb_sys_tables WHERE name = 'test/undo_demo';
+----------+----------------+------+--------+-------+-------------+------------+---------------+------------+
| TABLE_ID | NAME           | FLAG | N_COLS | SPACE | FILE_FORMAT | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE |
+----------+----------------+------+--------+-------+-------------+------------+---------------+------------+
|      233 | test/undo_demo |   33 |      6 |   253 | Barracuda   | Dynamic    |             0 | Single     |
+----------+----------------+------+--------+-------+-------------+------------+---------------+------------+
1 row in set (0.00 sec)

undo_demo表对应的table id233

INSERT操作对应的undo日志

当我们向表中插入一条记录时会有乐观插入悲观插入的区分,但是不管怎么插入,最终导致的结果就是这条记录被放到了一个数据页中。
如果希望回滚这个插入操作,那么把这条记录删除就好了,也就是说在写对应的undo日志时,主要是把这条记录的主键信息记上。
类型为TRX_UNDO_INSERT_RECundo日志,它的完整结构如下图所示:
在这里插入图片描述

  • undo no在一个事务中是从0开始递增的,也就是说只要事务没提交,每生成一条undo日志,那么该条日志的undo no就增1
  • 如果记录中的主键只包含一个列,那么在类型为TRX_UNDO_INSERT_RECundo日志中只需要把该列占用的存储空间大小真实值记录下来,如果记录中的主键包含多个列,那么每个列占用的存储空间大小和对应的真实值都需要记录下来(图中的len就代表列占用的存储空间大小value就代表列的真实值)。

向某个表中插入一条记录时,实际上需要向聚簇索引和所有的二级索引都插入一条记录。只需要考虑向聚簇索引插入记录时的情况就好了,因为其实聚簇索引记录二级索引记录是一一对应的,在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。
DELETE操作和UPDATE操作对应的undo日志也都是针对聚簇索引记录而言的

BEGIN;  # 显式开启一个事务,假设该事务的id为100INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');

因为记录的主键只包含一个id列,所以在对应的undo日志中只需要将待插入记录id列占用的存储空间长度id列的类型为INT,INT类型占用的存储空间长度为4个字节)和真实值记录下来。
本例中插入了两条记录,所以会产生两条类型为TRX_UNDO_INSERT_RECundo日志:

  • 第一条undo日志undo no0,记录主键占用的存储空间长度4真实值1
  • 第二条undo日志undo no1,记录主键占用的存储空间长度4真实值2
    在这里插入图片描述

为了最大限度的节省undo日志占用的存储空间,和redo日志类似

roll_pointer隐藏列的含义

roll_pointer占用7个字节的字段,本质上就是一个指向记录对应的undo日志的一个指针
undo_demo表里插入了2条记录,每条记录都有与其对应的一条undo日志
记录被存储到了类型为FIL_PAGE_INDEX(数据页)的页面中。
undo日志被存放到了类型为FIL_PAGE_UNDO_LOG的页面中。
在这里插入图片描述

DELETE操作对应的undo日志

插入到页面中的记录会根据记录头信息中的next_record属性组成一个单向链表,这个链表称之为正常记录链表;被删除的记录其实也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表
Page Header部分有一个称之为PAGE_FREE的属性,它指向由被删除记录组成的垃圾链表中的头节点
在这里插入图片描述
在这里插入图片描述
正常记录链表中包含了3条正常记录垃圾链表里包含了2条已删除记录
垃圾链表中的这些记录占用的存储空间可以被重新利用
页面的Page Header部分的PAGE_FREE属性的值代表指向垃圾链表头节点的指针
假设现在准备使用DELETE语句把正常记录链表中的最后一条记录给删除掉,其实这个删除的过程需要经历两个阶段

  • 阶段一:仅仅将记录的delete_mask标识位设置为1,其他的不做修改(其实会修改记录的trx_idroll_pointer这些隐藏列的值)。这个阶段称之为delete mark
    在这里插入图片描述

正常记录链表中的最后一条记录的delete_mask值被设置为1,但是并没有被加入到垃圾链表,也就是此时记录处于一个中间状态
删除语句所在的事务提交之前,被删除的记录一直都处于这种所谓的中间状态

为啥会有这种奇怪的中间状态呢?其实主要是为了实现一个称之为MVCC的功能

  • 阶段二:当该删除语句所在的事务提交之后,会有专门的线程后来真正的把记录删除掉。所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,然后还要调整一些页面的其他信息

比如页面中的用户记录数量PAGE_N_RECS、上次插入记录的位置PAGE_LAST_INSERT垃圾链表头节点的指针PAGE_FREE、页面中可重用的字节数量PAGE_GARBAGE、还有页目录的一些信息等等。
这个阶段称之为purge

阶段二执行完了,这条记录就算是真正的被删除掉了。这条已删除记录占用的存储空间也可以被重新利用了
在这里插入图片描述
将被删除记录加入到垃圾链表时,实际上加入到链表的头节点处,会跟着修改PAGE_FREE属性的值。

页面的Page Header部分有一个PAGE_GARBAGE属性,该属性记录着当前页面中可重用存储空间占用的总字节数
每当有已删除记录被加入到垃圾链表后,都会把这个PAGE_GARBAGE属性的值加上该已删除记录占用的存储空间大小
PAGE_FREE指向垃圾链表的头节点,之后每当新插入记录时,首先判断PAGE_FREE指向的头节点代表的已删除记录占用的存储空间是否足够容纳这条新插入的记录。
如果不可以容纳,就直接向页面中申请新的空间来存储这条记录(并不会尝试遍历整个垃圾链表,找到一个可以容纳新记录的节点
如果可以容纳,那么直接重用这条已删除记录的存储空间,并且把PAGE_FREE指向垃圾链表中的下一条已删除记录
因此出现了问题:
如果新插入的那条记录占用的存储空间大小小于垃圾链表的头节点占用的存储空间大小,那就意味头节点对应的记录占用的存储空间里有一部分空间用不到,这部分空间就被称之为碎片空间
这些碎片空间占用的存储空间大小会被统计到PAGE_GARBAGE属性中。
这些碎片空间在整个页面快使用完前并不会被重新利用,不过当页面快满时,如果再插入一条记录,此时页面中并不能分配一条完整记录的空间
这时候会首先看一看PAGE_GARBAGE的空间和剩余可利用的空间加起来是不是可以容纳下这条记录,如果可以的话:
InnoDB会尝试重新组织页内的记录,重新组织的过程就是先开辟一个临时页面,把页面内的记录依次插入一遍,因为依次插入时并不会产生碎片,之后再把临时页面的内容复制到本页面,这样就可以把那些碎片空间都解放出来(很显然重新组织页面内的记录比较耗费性能)。

在删除语句所在的事务提交之前,只会经历阶段一,也就是delete mark阶段(提交之后我们就不用回滚了,所以只需考虑对删除操作阶段一做的影响进行回滚)。
TRX_UNDO_DEL_MARK_REC类型的undo日志
在这里插入图片描述

  • 在对一条记录进行delete mark操作前,需要把该记录的旧的trx_idroll_pointer隐藏列的值都给记到对应的undo日志中来,就是图中显示的old trx_idold roll_pointer属性。
    那就是可以通过undo日志old roll_pointer找到记录在修改之前对应的undo日志

比方说在一个事务中,先插入了一条记录,然后又执行对该记录的删除操作
在这里插入图片描述
执行完delete mark操作后,它对应的undo日志INSERT操作对应的undo日志就串成了一个链表。这个链表就称之为版本链

  • 与类型为TRX_UNDO_INSERT_RECundo日志不同。
    类型为TRX_UNDO_DEL_MARK_RECundo日志还多了一个索引列各列信息的内容
    如果某个被包含在某个索引中,那么它的相关信息就应该被记录到这个索引列各列信息部分,所谓的相关信息包括该列在记录中的位置(用pos表示)该列占用的存储空间大小(用len表示),该列实际值(用value表示)。
    索引列各列信息存储的内容实质上就是<pos, len, value>的一个列表。
    这部分信息主要是用在事务提交后,对该中间状态记录做真正删除的阶段二,也就是purge阶段中使用的。

现在继续在上边那个事务id100的事务中删除一条记录,比如我们把id1的那条记录删除掉:

start transaction;# 插入两条记录
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');delete from undo_demo where id = 1;

delete mark操作对应的undo日志的结构就是这样:
在这里插入图片描述

  • 因为这条undo日志id100的事务中产生的第3undo日志,所以它对应的undo no就是2
  • 在对记录做delete mark操作时,记录的trx_id隐藏列的值是100(也就是说对该记录最近的一次修改就发生在本事务中),所以把100填入old trx_id属性中。
    然后把记录的roll_pointer隐藏列的值取出来,填入old roll_pointer属性中,这样就可以通过old roll_pointer属性值找到最近一次对该记录做改动时产生的undo日志
  • undo_demo表中有2个索引:一个是聚簇索引,一个是二级索引idx_key1

只要是包含在索引中的列,那么这个列在记录中的位置pos),占用存储空间大小len)和实际值value)就需要存储到undo日志中。

对于主键来说,只包含一个id列,存储到undo日志中的相关信息分别是:

  • posid列是主键,也就是在记录的第一个列,它对应的pos值0pos占用1个字节来存储。
  • lenid列的类型为INT,占用4个字节,所以len的值为4len占用1个字节来存储。
  • value:在被删除的记录中id列的值为1,也就是value的值为1value占用4个字节来存储。
    在这里插入图片描述

对于id列来说,最终存储的结果就是<0, 4, 1>,存储这些信息占用的存储空间大小为1 + 1 + 4 = 6个字节。
对于idx_key1来说,只包含一个key1列,存储到undo日志中的相关信息分别是:

  • poskey1列是排在id列trx_id列roll_pointer列之后的,它对应的pos值为3。pos占用1个字节来存储。
    在这里插入图片描述
  • lenkey1列的类型为VARCHAR(100),使用utf8字符集,被删除的记录实际存储的内容是AWM,所以一共占用3个字节,也就是所以len的值为3len占用1个字节来存储。
  • value:在被删除的记录中key1列的值为AWM,也就是value的值为AWMvalue占用3个字节来存储。
    在这里插入图片描述

所以对于key1列来说,最终存储的结果就是<3, 3, 'AWM'>,存储这些信息占用的存储空间大小为1 + 1 + 3 = 5个字节。
从上边的叙述中可以看到,<0, 4, 1><3, 3, 'AWM'>共占用11个字节。然后index_col_info len本身占用2个字节,所以加起来一共占用13个字节,把数字13就填到了index_col_info len的属性中。

UPDATE操作对应的undo日志

不更新主键的情况

不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化发生变化的情况

  • 就地更新in-place update

更新记录时,对于被更新的每个列来说,如果更新后的列更新前的列占用的存储空间都一样大,那么就可以进行就地更新,也就是直接在原记录的基础上修改对应列的值。
是每个列在更新前后占用的存储空间一样大,有任何一个被更新的列更新前更新后占用的存储空间大,或者更新前比更新后占用的存储空间小都不能进行就地更新
比方说现在undo_demo表里还有一条id值2记录,它的各个列占用的大小如图所示(因为采用utf8字符集,所以'步枪'两个字符占用6个字节):
在这里插入图片描述

UPDATE undo_demo SET key1 = 'P92', col = '手枪' WHERE id = 2;

col列步枪被更新为手枪,前后都占用6个字节,也就是占用的存储空间大小未改变;
key1列M416被更新为P92,也就是从4个字节被更新为3个字节,这就不满足就地更新需要的条件了,所以不能进行就地更新

UPDATE undo_demo SET key1 = 'M249', col = '机枪' WHERE id = 2;

由于各个被更新的列在更新前后占用的存储空间是一样大的,所以这样的语句可以执行就地更新

  • 先删除掉旧记录,再插入新记录

在不更新主键的情况下,如果有任何一个被更新的列更新前更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中
这里所说的删除并不是delete mark操作,而是真正的删除掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中,并且修改页面中相应的统计信息(比如PAGE_FREEPAGE_GARBAGE等这些信息)。
这里做真正删除操作的线程并不是DELETE语句中做purge操作时使用的,而是由用户线程同步执行真正的删除操作真正删除之后紧接着就要根据各个列更新后的值创建的新记录插入

这里如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则的话需要在页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用的空间的话,那就需要进行页面分裂操作,然后再插入新记录

针对UPDATE不更新主键的情况(包括上边所说的就地更新先删除旧记录再插入新记录
类型为TRX_UNDO_UPD_EXIST_RECundo日志,它的完整结构如下:
在这里插入图片描述

  • n_updated属性表示本条UPDATE语句执行后将有几个列被更新。
    <pos, old_len, old_value>分别表示被更新列在记录中的位置更新前该列占用的存储空间大小更新前该列的真实值
  • UPDATE语句中更新的列包含索引列,那么也会添加索引列各列信息这个部分,否则的话是不会添加这个部分的。

事务id100的事务中更新一条记录,比如我们把id2的那条记录更新一下:

BEGIN;  # 显式开启一个事务,假设该事务的id为100# 插入两条记录
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');# 删除一条记录    
DELETE FROM undo_demo WHERE id = 1; # 更新一条记录
UPDATE undo_demoSET key1 = 'M249', col = '机枪'WHERE id = 2;

这个UPDATE语句更新的列大小都没有改动,所以可以采用就地更新的方式来执行
真正改动页面记录时,会先记录一条类型为TRX_UNDO_UPD_EXIST_RECundo日志
在这里插入图片描述

  • 因为这条undo日志id100的事务中产生的第4undo日志,所以它对应的undo no就是3
  • 这条日志的roll_pointer指向undo no1的那条日志,也就是插入主键值为2的记录时产生的那条undo日志,也就是最近一次对该记录做改动时产生的undo日志
  • UPDATE语句中更新了索引列key1的值,所以需要记录一下索引列各列信息部分,也就是把主键key1列更新前的信息填入。

更新主键的情况

聚簇索引中,记录是按照主键值的大小连成了一个单向链表的,如果我们更新了某条记录的主键值,意味着这条记录在聚簇索引中的位置将会发生改变。
如果还有非常多的记录的主键值分布在1 ~ 10000之间的话,那么这两条记录聚簇索引中就有可能离得非常远,甚至中间隔了好多个页面
针对UPDATE语句中更新了记录主键值的这种情况,InnoDB聚簇索引中分了两步处理:

  • 将旧记录进行delete mark操作
    这里是delete mark操作!!!!!!!!!!!!!!!!!!!!
    UPDATE语句所在的事务提交前,对旧记录只做一个delete mark操作,在事务提交后才由专门的线程做purge操作,把它加入到垃圾链表中。
    这里一定要和我们上边所说的在不更新记录主键值时,先真正删除旧记录,再插入新记录的方式区分开!

只对旧记录做delete mark操作,是因为别的事务同时也可能访问这条记录
如果把它真正的删除加入到垃圾链表后,别的事务就访问不到了
这个功能就是所谓的MVCC

请添加图片描述
针对UPDATE语句更新记录主键值的这种情况,在对该记录进行delete mark操作前,会记录一条类型为TRX_UNDO_DEL_MARK_RECundo日志
之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_RECundo日志,也就是说每对一条记录的主键值做改动时,会记录2条undo日志

TRX_UNDO_UPD_DEL_RECundo日志的类型(待续

INSERT操作对应的undo日志TRX_UNDO_INSERT_REC日志
DELETE操作对应的undo日志TRX_UNDO_DEL_MARK_REC日志
UPDATE非主键对应的undo日志TRX_UNDO_UPD_EXIST_REC日志
UPDATE主键对应的undo日志TRX_UNDO_DEL_MARK_REC日志和TRX_UNDO_INSERT_REC日志

FIL_PAGE_UNDO_LOG页面(二周目)

在这里插入图片描述
TRX_UNDO_PAGE_TYPE:本页面准备存储什么种类的undo日志undo日志,它们可以被分为两个大类

  • TRX_UNDO_INSERT(使用十进制1表示):类型为TRX_UNDO_INSERT_RECundo日志属于此大类,一般由INSERT语句产生,或者在UPDATE语句中有更新主键的情况也会产生此类型的undo日志
  • TRX_UNDO_UPDATE(使用十进制2表示),除了类型为TRX_UNDO_INSERT_RECundo日志,其他类型的undo日志都属于这个大类,比如我们前边说的TRX_UNDO_DEL_MARK_RECTRX_UNDO_UPD_EXIST_REC,一般由DELETEUPDATE语句产生的undo日志属于这个大类。
    这个TRX_UNDO_PAGE_TYPE属性可选的值就是上边的两个,用来标记本页面用于存储哪个大类的undo日志
    不同大类的undo日志不能混着存储,比如一个Undo页面TRX_UNDO_PAGE_TYPE属性值为TRX_UNDO_INSERT,那么这个页面就只能存储类型为TRX_UNDO_INSERT_RECundo日志其他类型undo日志就不能放到这个页面中

之所以把undo日志分成两个大类,是因为类型为TRX_UNDO_INSERT_RECundo日志在事务提交后可以直接删除掉,而其他类型的undo日志还需要为所谓的MVCC服务,不能直接删除掉,对它们的处理需要区别对待。

  • TRX_UNDO_PAGE_START:表示在当前页面中是从什么位置开始存储undo日志的,或者说表示第一条undo日志在本页面中的起始偏移量
  • TRX_UNDO_PAGE_FREE:与上边的TRX_UNDO_PAGE_START对应,表示当前页面中存储的最后一条undo日志结束时的偏移量,或者说从这个位置开始,可以继续写入新的undo日志

向页面中写入了3undo日志,那么TRX_UNDO_PAGE_STARTTRX_UNDO_PAGE_FREE的示意图就是这样:
在这里插入图片描述
当然,在最初一条undo日志也没写入的情况下,TRX_UNDO_PAGE_STARTTRX_UNDO_PAGE_FREE的值是相同的。

  • TRX_UNDO_PAGE_NODE:代表一个List Node结构

Undo页面链表

单个事务中的Undo页面链表

因为一个事务可能包含多个语句,而且一个语句可能对若干条记录进行改动,而对每条记录进行改动前,都需要记录1条或2条的undo日志,所以在一个事务执行过程中可能产生很多undo日志,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过TRX_UNDO_PAGE_NODE属性连成了链表:
在这里插入图片描述
在这里插入图片描述
第一个Undo页面称之为first undo page,其余的Undo页面称之为normal undo page
在一个事务执行过程中,可能混着执行INSERTDELETEUPDATE语句,也就意味着会产生不同类型的undo日志
同一个Undo页面要么只存储TRX_UNDO_INSERT大类的undo日志,要么只存储TRX_UNDO_UPDATE大类的undo日志
所以在一个事务执行过程中就可能需要2个Undo页面的链表,一个称之为insert undo链表,另一个称之为update undo链表
在这里插入图片描述

另外,对普通表临时表的记录改动时产生的undo日志要分别记录。
一个事务中最多有4个以Undo页面为节点组成的链表
在这里插入图片描述
当然,并不是在事务一开始就会为这个事务分配这4链表,具体分配策略如下:

  • 刚刚开启事务时,一个Undo页面链表也不分配。
  • 当事务执行过程中向普通表插入记录或者执行更新记录主键的操作之后,就会为其分配一个普通表insert undo链表
  • 当事务执行过程中删除或者更新了普通表中的记录之后,就会为其分配一个普通表update undo链表
  • 当事务执行过程中向临时表插入记录或者执行更新记录主键的操作之后,就会为其分配一个临时表insert undo链表
  • 当事务执行过程中删除或者更新了临时表中的记录之后,就会为其分配一个临时表update undo链表

总结:按需分配,啥时候需要啥时候再分配,不需要就不分配。

多个事务中的Undo页面链表

为了尽可能提高undo日志的写入效率,不同事务执行过程中产生的undo日志需要被写入到不同的Undo页面链表中。
比方说现在有事务id分别为12两个事务,我们分别称之为trx 1trx 2,假设在这两个事务执行过程中:

  • trx 1普通表做了DELETE操作,对临时表做了INSERTUPDATE操作。
    InnoDB会为trx 1分配3个链表,分别是:
    针对普通表update undo链表
    针对临时表insert undo链表
    针对临时表update undo链表
  • trx 2普通表做了INSERTUPDATEDELETE操作,没有对临时表做改动。
    InnoDB会为trx 2分配2个链表,分别是:
    针对普通表insert undo链表
    针对普通表update undo链表。

trx 1trx 2执行过程中,InnoDB共需为这两个事务分配5个Undo页面链表
在这里插入图片描述

undo日志具体写入过程

段(Segment)的概念

是一个逻辑上的概念,本质上是由若干个零散页面和若干个完整的区组成的。
比如一个B+树索引被划分成两个段,一个叶子节点段,一个非叶子节点段,这样叶子节点就可以被尽可能的存到一起,非叶子节点被尽可能的存到一起。
每一个段对应一个INODE Entry结构,这个INODE Entry结构描述了这个段的各种信息,比如段的ID段内的各种链表基节点零散页面的页号
为了定位一个INODE Entry,设计了一个Segment Header的结构:
在这里插入图片描述
整个Segment Header占用10个字节大小,各个属性的意思如下:

  • Space ID of the INODE EntryINODE Entry结构所在的表空间ID

  • Page Number of the INODE EntryINODE Entry结构所在的页面页号

  • Byte Offset of the INODE EntryINODE Entry结构在该页面中的偏移量

知道了表空间ID页号页内偏移量,就可以唯一定位一个INODE Entry的地址。

Undo Log Segment Header

每一个Undo页面链表都对应着一个,称之为Undo Log Segment
链表中页面都是从这个段里边申请的,所以在Undo页面链表的第一个页面,也就是之前提到的first undo page中设计了一个称之为Undo Log Segment Header的部分
这个部分中包含了该链表对应的segment header信息以及其他的一些关于这个的信息,所以Undo页面链表第一个页面其实长这样:
在这里插入图片描述
Undo Log Segment Header,看一下它的结构
在这里插入图片描述

  • TRX_UNDO_STATE:本Undo页面链表处在什么状态。
    一个Undo Log Segment可能处在的状态包括:

    • TRX_UNDO_ACTIVE活跃状态,也就是一个活跃的事务正在往这个段里边写入undo日志
    • TRX_UNDO_CACHED被缓存的状态。处在该状态的Undo页面链表等待着之后被其他事务重用。
    • TRX_UNDO_TO_FREE:对于insert undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态
    • TRX_UNDO_TO_PURGE:对于update undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态
    • TRX_UNDO_PREPARED:包含处于PREPARE阶段的事务产生的undo日志
  • TRX_UNDO_LAST_LOG:本Undo页面链表中最后一个Undo Log Header的位置。

  • TRX_UNDO_FSEG_HEADER:本Undo页面链表对应的段的Segment Header信息,通过这个信息可以找到该段对应的INODE Entry
    在这里插入图片描述

  • TRX_UNDO_PAGE_LISTUndo页面链表的基节点。
    Undo页面Undo Page Header部分有一个12字节大小的TRX_UNDO_PAGE_NODE属性,这个属性代表一个List Node结构。
    每一个Undo页面都包含Undo Page Header结构,这些页面就可以通过这个属性连成一个链表
    这个TRX_UNDO_PAGE_LIST属性代表着这个链表的基节点,当然这个基节点只存在于Undo页面链表的第一个页面,也就是first undo page中。
    在这里插入图片描述

Undo Log Header

Undo页面链表的第一个页面在真正写入undo日志前,其实都会被填充Undo Page HeaderUndo Log Segment HeaderUndo Log Header这3个部分。
在这里插入图片描述
Undo Log Header具体的结构如下:
在这里插入图片描述

  • TRX_UNDO_TRX_ID:生成本组undo日志事务id
  • TRX_UNDO_TRX_NO事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。
  • TRX_UNDO_DEL_MARKS:标记本组undo日志中是否包含由于Delete mark操作产生的undo日志
  • TRX_UNDO_LOG_START:表示本组undo日志中第一条undo日志的在页面中的偏移量
  • TRX_UNDO_XID_EXISTS:本组undo日志是否包含XID信息。
  • TRX_UNDO_DICT_TRANS:标记本组undo日志是不是由DDL语句产生的。
  • TRX_UNDO_TABLE_ID:如果TRX_UNDO_DICT_TRANS为真,那么本属性表示DDL语句操作的表的table id
  • TRX_UNDO_NEXT_LOG下一组undo日志在页面中开始的偏移量。
  • TRX_UNDO_PREV_LOG上一组undo日志在页面中开始的偏移量。

一个Undo页面链表只存储一个事务执行过程中产生的一组undo日志,但是在某些情况下,可能会在一个事务提交之后,之后开启的事务重复利用这个Undo页面链表,这样就会导致一个Undo页面中可能存放多组Undo日志TRX_UNDO_NEXT_LOGTRX_UNDO_PREV_LOG就是用来标记下一组和上一组Undo日志在页面中的偏移量的。

  • TRX_UNDO_HISTORY_NODE:一个12字节List Node结构,代表一个称之为History链表的节点。

总结

对于没有被重用的Undo页面链表来说,链表的第一个页面,也就是first undo page在真正写入undo日志前,会填充Undo Page Header、Undo Log Segment Header、Undo Log Header3个部分,之后才开始正式写入undo日志
对于其他的页面来说,也就是normal undo page在真正写入undo日志前,只会填充Undo Page Header
链表的List Base Node存放到first undo pageUndo Log Segment Header部分,List Node信息存放到每一个Undo页面undo Page Header部分。

在这里插入图片描述

重用Undo页面

为了能提高并发执行多个事务写入undo日志的性能,每个事务单独分配相应的Undo页面链表(最多可能单独分配4个链表)。
其实大部分事务执行过程中可能只修改了一条或几条记录,针对某个Undo页面链表只产生了非常少的undo日志,这些undo日志可能只占用一丢丢存储空间,这样就很浪费
一个Undo页面链表是否可以被重用的条件很简单:

  • 链表中只包含一个Undo页面
    如果一个事务执行过程中产生了非常多undo日志,那么它可能申请非常多的页面加入到Undo页面链表中。
    在该事务提交后,如果将整个链表中的页面都重用,那就意味着即使新的事务并没有向该Undo页面链表中写入很多undo日志,那该链表中也得维护非常多的页面,那些用不到的页面也不能被别的事务所使用,这样就造成了另一种浪费
  • Undo页面已经使用的空间小于整个页面空间3/4

Undo页面链表按照存储的undo日志所属的大类可以被分为insert undo链表和update undo链表两种:

  • insert undo链表
    insert undo链表中只存储类型为TRX_UNDO_INSERT_RECundo日志,这种类型的undo日志事务提交之后就没用了,就可以被清除掉。
    所以在某个事务提交后,重用这个事务的insert undo链表这个链表中只有一个页面)时,可以直接把之前事务写入的一组undo日志覆盖掉,从头开始写入新事务的一组undo日志,如下图所示:

在这里插入图片描述
如图所示,假设有一个事务使用的insert undo链表,到事务提交时,只向insert undo链表中插入了3条undo日志,这个insert undo链表只申请了一个Undo页面。假设此刻该页面已使用的空间小于整个页面大小3/4,那么下一个事务就可以重用这个insert undo链表链表中只有一个页面)。假设此时有一个新事务重用了该insert undo链表,那么可以直接把旧的一组undo日志覆盖掉,写入一组新的undo日志

当然,在重用Undo页面链表写入新的一组undo日志时,不仅会写入新的Undo Log Header,还会适当调整Undo Page Header、Undo Log Segment Header、Undo Log Header中的一些属性,比如TRX_UNDO_PAGE_START、TRX_UNDO_PAGE_FREE等。

  • update undo链表
    在一个事务提交后,它的update undo链表中的undo日志也不能立即删除掉(这些日志用于MVCC)。所以如果之后的事务想重用update undo链表时,就不能覆盖之前事务写入的undo日志。这样就相当于在同一个Undo页面中写入了多组的undo日志,效果看起来就是这样:

在这里插入图片描述

回滚段的概念

回滚段的概念

一个事务在执行过程中最多可以分配4个Undo页面链表,在同一时刻不同事务拥有的Undo页面链表是不一样的,所以在同一时刻系统里其实可以有许许多多个Undo页面链表存在。
为了更好的管理这些链表Rollback Segment Header页面出现了。
在这个页面中存放了各个Undo页面链表frist undo page的页号,他们把这些页号称之为undo slot
每个Undo页面链表都相当于是一个班,这个链表的first undo page就相当于这个班的班长,找到了这个班的班长,就可以找到班里的其他同学(其他同学相当于normal undo page)。有时候学校需要向这些班级传达一下精神,就需要把班长都召集在会议室,这个Rollback Segment Header就相当于是一个会议室

Rollback Segment Header页面(以默认的16KB为例):
在这里插入图片描述
每一个Rollback Segment Header页面都对应着一个,这个段就称为Rollback Segment,翻译过来就是回滚段Rollback Segment里其实只有一个页面

  • TRX_RSEG_MAX_SIZE:本Rollback Segment中管理的所有Undo页面链表中的Undo页面数量之和的最大值。
    Rollback Segment中所有Undo页面链表中的Undo页面数量之和不能超过TRX_RSEG_MAX_SIZE代表的值。
    该属性的值默认为无限大,也就是我们想写多少Undo页面都可以。
  • TRX_RSEG_HISTORY_SIZEHistory链表占用的页面数量
  • TRX_RSEG_HISTORYHistory链表基节点
  • TRX_RSEG_FSEG_HEADER:本Rollback Segment对应的10字节大小的Segment Header结构,通过它可以找到本段对应的INODE Entry
  • TRX_RSEG_UNDO_SLOTS:各个Undo页面链表first undo page页号集合,也就是undo slot集合
    在这里插入图片描述
    一个页号占用4个字节,对于16KB大小的页面来说,这个TRX_RSEG_UNDO_SLOTS部分共存储了1024undo slot,所以共需1024 × 4 = 4096字节

从回滚段中申请Undo页面链表

初始情况下,由于未向任何事务分配任何Undo页面链表,所以对于一个Rollback Segment Header页面来说,它的各个undo slot都被设置成了一个特殊的值:FIL_NULL(对应的十六进制就是0xFFFFFFFF),表示该undo slot不指向任何页面

开始有事务需要分配Undo页面链表了,就从回滚段的第一个undo slot开始,看看该undo slot的值是不是FIL_NULL

  • 如果是FIL_NULL,那么在表空间中新创建一个(也就是Undo Log Segment),然后从里申请一个页面作为Undo页面链表first undo page,然后把该undo slot的值设置为刚刚申请的这个页面的页号,这样也就意味着这个undo slot被分配给了这个事务。

在这里插入图片描述
在这里插入图片描述

  • 如果不是FIL_NULL,说明该undo slot已经指向了一个undo链表,也就是说这个undo slot已经被别的事务占用了,那就跳到下一个undo slot,判断该undo slot的值是不是FIL_NULL重复上边的步骤

一个Rollback Segment Header页面中包含1024个undo slot,如果这1024个undo slot的值都不为FIL_NULL,这就意味着这1024个undo slot都已经被分配给了某个事务,此时由于新事务无法再获得新的Undo页面链表,就会回滚这个事务并且给用户报错

Too many active concurrent transactions

用户看到这个错误,可以选择重新执行这个事务(可能重新执行时有别的事务提交了,该事务就可以被分配Undo页面链表了)。
当一个事务提交时,它所占用的undo slot有两种命运

  • 如果该undo slot指向的Undo页面链表符合被重用的条件(就是我们上边说的Undo页面链表只占用一个页面并且已使用空间小于整个页面的3/4)。
    undo slot就处于被缓存的状态,该Undo页面链表TRX_UNDO_STATE属性(该属性在first undo pageUndo Log Segment Header部分)会被设置为TRX_UNDO_CACHED
    在这里插入图片描述

被缓存的undo slot都会被加入到一个链表,根据对应的Undo页面链表类型不同,也会被加入到不同的链表

  • 如果对应的Undo页面链表insert undo链表,则该undo slot会被加入insert undo cached链表

  • 如果对应的Undo页面链表update undo链表,则该undo slot会被加入update undo cached链表
    一个回滚段就对应着上述两个cached链表,如果有新事务要分配undo slot时,先从对应的cached链表中找。如果没有被缓存undo slot,才会到回滚段Rollback Segment Header页面中再去找。

  • 如果该undo slot指向的Undo页面链表不符合被重用的条件,那么针对该undo slot对应的Undo页面链表类型不同,也会有不同的处理:

    • 如果对应的Undo页面链表insert undo链表,则该Undo页面链表TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_FREE,之后该Undo页面链表对应的会被释放掉(也就意味着段中的页面可以被挪作他用),然后把该undo slot的值设置为FIL_NULL
    • 如果对应的Undo页面链表update undo链表,则该Undo页面链表TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_PRUGE,则会将该undo slot的值设置为FIL_NULL,然后将本次事务写入的一组undo日志放到所谓的History链表中(需要注意的是,这里并不会将Undo页面链表对应的给释放掉,因为这些undo日志还有用)。

多个回滚段

一个事务执行过程中最多分配4个Undo页面链表,而一个回滚段里只有1024个undo slot,很显然undo slot的数量有点少啊。即使假设一个读写事务执行过程中只分配1个Undo页面链表,那1024个undo slot也只能支持1024个读写事务同时执行,再多了就崩溃了
因此多定义回滚段就行!定义了128回滚段,也就相当于有了128 × 1024 = 131072个undo slot
假设一个读写事务执行过程中只分配1个Undo页面链表,那么就可以同时支持131072个读写事务并发执行

每个回滚段都对应着一个Rollback Segment Header页面,有128个回滚段,自然就要有128个Rollback Segment Header页面,在系统表空间第5号页面的某个区域包含了128个8字节大小的格子:
在这里插入图片描述
每个8字节的格子的构造就像这样:
在这里插入图片描述
如果所示,每个8字节的格子其实由两部分组成:

  • 4字节大小的Space ID,代表一个表空间的ID
  • 4字节大小的Page number,代表一个页号

也就是说每个8字节大小的格子相当于一个指针,指向某个表空间中的某个页面,这些页面就是Rollback Segment Header。要定位一个Rollback Segment Header还需要知道对应的表空间ID,这也就意味着不同的回滚段可能分布在不同的表空间中。

系统表空间的第5号页面中存储了128Rollback Segment Header页面地址,每个Rollback Segment Header就相当于一个回滚段。在Rollback Segment Header页面中,又包含1024undo slot,每个undo slot都对应一个Undo页面链表

在这里插入图片描述

回滚段的分类

把这128个回滚段给编一下号,最开始的回滚段称之为第0号回滚段,之后依次递增,最后一个回滚段就称之为第127号回滚段。这128个回滚段可以被分成两大类

  • 0号、第33~127回滚段属于一类。其中第0号回滚段必须在系统表空间中(就是说第0号回滚段对应的Rollback Segment Header页面必须在系统表空间中),第33~127号回滚段既可以在系统表空间中,也可以在自己配置的undo表空间中。
    如果一个事务在执行过程中由于对普通表的记录做了改动需要分配Undo页面链表时,必须从这一类的中分配相应的undo slot
  • 1~32号回滚段属于一类。这些回滚段必须在临时表空间(对应着数据目录中的ibtmp1文件)中。
    如果一个事务在执行过程中由于对临时表的记录做了改动需要分配Undo页面链表时,必须从这一类的中分配相应的undo slot

也就是说如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段,再分别到这两个回滚段中分配对应的undo slot
待续。。。

为事务分配Undo页面链表详细过程

事务执行过程中分配Undo页面链表时的完整过程:

  • 事务在执行过程中对普通表的记录首次做改动之前,首先会到系统表空间第5号页面中分配一个回滚段(其实就是获取一个Rollback Segment Header页面的地址)。一旦某个回滚段被分配给了这个事务,那么之后该事务中再对普通表的记录做改动时,就不会重复分配了。
    使用传说中的round-robin(循环使用)方式来分配回滚段。比如当前事务分配了第0号回滚段,那么下一个事务就要分配第33号回滚段,下下个事务就要分配第34号回滚段,简单一点的说就是这些回滚段被轮着分配给不同的事务。
  • 在分配到回滚段后,首先看一下这个回滚段的两个cached链表有没有已经缓存了的undo slot,比如如果事务做的是INSERT操作,就去回滚段对应的insert undo cached链表中看看有没有缓存的undo slot;如果事务做的是DELETE操作,就去回滚段对应的update undo cached链表中看看有没有缓存的undo slot。如果有缓存的undo slot,那么就把这个缓存的undo slot分配给该事务。
  • 如果没有缓存的undo slot可供分配,那么就要到Rollback Segment Header页面中找一个可用的undo slot分配给当前事务。
    Rollback Segment Header页面中分配可用的undo slot的方式我们上边也说过了,就是从第0个undo slot开始,如果该undo slot的值为FIL_NULL,意味着这个undo slot是空闲的,就把这个undo slot分配给当前事务,否则查看第1个undo slot是否满足条件,依次类推,直到最后一个undo slot。如果这1024个undo slot都没有值为FIL_NULL的情况,就直接报错喽
  • 找到可用的undo slot后,如果该undo slot是从cached链表中获取的,那么它对应的Undo Log Segment已经分配了,否则的话需要重新分配一个Undo Log Segment,然后从该Undo Log Segment中申请一个页面作为Undo页面链表first undo page
  • 然后事务就可以把undo日志写入到上边申请的Undo页面链表了!

临时表的记录做改动的步骤和上述的一样,就不赘述了。不过需要再次强调一次,如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段。并发执行的不同事务其实也可以被分配相同的回滚段,只要分配不同的undo slot就可以了。

回滚段相关配置

配置回滚段数量

系统中一共有128个回滚段,其实这只是默认值,我们可以通过启动参数innodb_rollback_segments来配置回滚段的数量,可配置的范围是1~128

mysql> show variables like '%innodb_rollback_segments%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_rollback_segments | 128   |
+--------------------------+-------+
1 row in set (0.02 sec)

但是这个参数并不会影响针对临时表回滚段数量,针对临时表回滚段数量一直是32

  • 如果把innodb_rollback_segments的值设置为1,那么只会有1个针对普通表的可用回滚段,但是仍然有32个针对临时表可用回滚段
  • 如果把innodb_rollback_segments的值设置为2~33之间的数,效果和将其设置为1是一样的。
  • 如果把innodb_rollback_segments设置为大于33的数,那么针对普通表可用回滚段数量就是该值减去32

配置undo表空间

默认情况下,针对普通表设立的回滚段(第0号以及第33~127回滚段)都是被分配到系统表空间的。其中的第0回滚段是一直在系统表空间的,但是第33~127号回滚段可以通过配置放到自定义undo表空间中。但是这种配置只能在系统初始化(创建数据目录时)的时候使用,一旦初始化完成,之后就不能再次更改了。

  • 通过innodb_undo_directory指定undo表空间所在的目录,如果没有指定该参数,则默认undo表空间所在的目录就是数据目录
mysql> show variables like 'innodb_undo_directory';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_undo_directory | ./    |
+-----------------------+-------+
1 row in set (0.00 sec)
#数据目录
mysql> show variables like '%datadir%';
+---------------+----------------------------+
| Variable_name | Value                      |
+---------------+----------------------------+
| datadir       | /soft/mysql/mysql9.0/data/ |
+---------------+----------------------------+
1 row in set (0.00 sec)
  • 通过innodb_undo_tablespaces定义undo表空间的数量。该参数的默认值为0,表明不创建任何undo表空间
mysql> show variables like '%innodb_undo_tablespaces%';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_undo_tablespaces | 2     |
+-------------------------+-------+
1 row in set (0.00 sec)

33~127号回滚段可以平均分布到不同的undo表空间中。

比如我们在系统初始化时指定的innodb_rollback_segments35innodb_undo_tablespaces2,这样就会将第33、34回滚段分别分布到一个undo表空间中。

设立undo表空间的一个好处就是在undo表空间中的文件大到一定程度时,可以自动的将该undo表空间截断(truncate)成一个小文件。而系统表空间的大小只能不断的增大,却不能截断

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/70054.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全面剖析 XXE 漏洞:从原理到修复

目录 前言 XXE 漏洞概念 漏洞原理 XML 介绍 XML 结构语言以及语法 XML 结构 XML 语法规则 XML 实体引用 漏洞存在原因 产生条件 经典案例介绍分析 XXE 漏洞修复方案 结语 前言 网络安全领域暗藏危机&#xff0c;各类漏洞威胁着系统与数据安全。XXE 漏洞虽不常见&a…

初级数据结构:栈和队列

目录 一、栈 (一)、栈的定义 (二)、栈的功能 (三)、栈的实现 1.栈的初始化 2.动态扩容 3.压栈操作 4.出栈操作 5.获取栈顶元素 6.获取栈顶元素的有效个数 7.检查栈是否为空 8.栈的销毁 9.完整代码 二、队列 (一)、队列的定义 (二)、队列的功能 (三&#xff09…

C++STL(一)——string类

目录 一、string的定义方式二、 string类对象的容量操作三、string类对象的访问及遍历操作四、string类对象的修改操作五、string类非成员函数 一、string的定义方式 string是个管理字符数组的类&#xff0c;其实就是字符数组的顺序表。 它的接口也是非常多的。本章介绍一些常…

与,|与||的区别

按位运算符 | 和 & 功能与运算规则 |&#xff08;按位或运算符&#xff09;&#xff1a;对两个操作数的对应二进制位进行逻辑或运算。只要对应的两个二进制位中有一个为 1&#xff0c;则该位的结果为 1&#xff1b;只有当两个二进制位都为 0 时&#xff0c;结果才为 0。&…

轮转数组-三次逆置

题目 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 void rotate(int* nums, int numsSize, int k){}示例&#xff1a; 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] …

登录认证(5):过滤器:Filter

统一拦截 上文我们提到&#xff08;登录认证&#xff08;4&#xff09;&#xff1a;令牌技术&#xff09;&#xff0c;现在大部分项目都使用JWT令牌来进行会话跟踪&#xff0c;来完成登录功能。有了JWT令牌可以标识用户的登录状态&#xff0c;但是完整的登录逻辑如图所示&…

C++11新特性之constexpr

1.介绍 constexpr是C11标准引入的关键字&#xff0c;用于声明常量表达式&#xff0c;其目的是让一些计算在编译时就能完成&#xff0c;从而提高程序的性能与安全性。&#xff08;因为只需要执行一次&#xff09; 在介绍其用法前&#xff0c;先解释一下常量表达式的含义。 常量…

JavaScript 中的 CSS 与页面响应式设计

JavaScript 中的 CSS 与页面响应式设计 JavaScript 中的 CSS 与页面响应式设计1. 引言2. JavaScript 与 CSS 的基本概念2.1 CSS 的作用2.2 JavaScript 的作用 3. 动态控制样式&#xff1a;JavaScript 修改 CSS 的方法3.1 使用 document.styleSheets API3.2 使用 classList 修改…

Python 网络爬虫实战:从基础到高级爬取技术

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 1. 引言 网络爬虫&#xff08;Web Scraping&#xff09;是一种自动化技术&#xff0c;利用程序从网页中提取数据&#xff0c;广泛…

MySQL锁类型(详解)

锁的分类图&#xff0c;如下&#xff1a; 锁操作类型划分 读锁 : 也称为共享锁 、英文用S表示。针对同一份数据&#xff0c;多个事务的读操作可以同时进行而不会互相影响&#xff0c;相互不阻塞的。 写锁 : 也称为排他锁 、英文用X表示。当前写操作没有完成前&#xff0c;它会…

Java中的常见对象类型解析

在Java开发中&#xff0c;数据的组织和传递是一个重要的概念。为了确保代码的清晰性、可维护性和可扩展性&#xff0c;我们通常会根据不同的用途&#xff0c;设计和使用不同类型的对象。这些对象的作用各不相同&#xff0c;但它们共同为构建高效、模块化的软件架构提供支持。 …

93,【1】buuctf web [网鼎杯 2020 朱雀组]phpweb

进入靶场 页面一直在刷新 在 PHP 中&#xff0c;date() 函数是一个非常常用的处理日期和时间的函数&#xff0c;所以应该用到了 再看看警告的那句话 Warning: date(): It is not safe to rely on the systems timezone settings. You are *required* to use the date.timez…

怀旧经典:1200+款红白机游戏合集,Windows版一键畅玩

​沉浸在怀旧的海洋中&#xff0c;体验经典红白机游戏的魅力&#xff01;我们为您精心准备了超过1200款经典游戏的合集&#xff0c;每一款都是时代的印记&#xff0c;每一场都是回忆的旅程。这个合集不仅包含了丰富的游戏资源&#xff0c;还内置了多个Windows版的NES模拟器&…

51单片机 01 LED

一、点亮一个LED 在STC-ISP中单片机型号选择 STC89C52RC/LE52RC&#xff1b;如果没有找到hex文件&#xff08;在objects文件夹下&#xff09;&#xff0c;在keil中options for target-output- 勾选 create hex file。 如果要修改编程 &#xff1a;重新编译-下载/编程-单片机重…

C语言实现库函数strlen

size_t是 unsigned int fgets会读入\n&#xff0c;用strcspn函数除去 assert判读指针是否为空指针&#xff0c;使用前要引头文件<assert.h> #include <stdio.h> #include <assert.h> size_t mystrlen(const char* str) {assert(str);size_t count 0;while …

【Rust自学】19.2. 高级trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait和newtype

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 19.2.1. 在trait定义中使用关联类型来指定占位类型 我们首先在第10章的10.3. trait Pt.1&a…

自动化测试框架搭建-封装requests-优化

目的 1、实际的使用场景&#xff0c;无法避免的需要区分GET、POST、PUT、PATCH、DELETE等不同的方式请求&#xff0c;以及不同请求的传参方式 2、python中requests中&#xff0c;session.request方法&#xff0c;GET请求&#xff0c;只支持params传递参数 session.request(me…

Elasticsearch:如何搜索含有复合词的语言

作者&#xff1a;来自 Elastic Peter Straer 复合词在文本分析和标记过程中给搜索引擎带来挑战&#xff0c;因为它们会掩盖词语成分之间的有意义的联系。连字分解器标记过滤器等工具可以通过解构复合词来帮助解决这些问题。 德语以其长复合词而闻名&#xff1a;Rindfleischetik…

41【语言的编码架构】

不同语言采用的编码架构不一样 火山采用&#xff1a;UTF-16 易语言采用&#xff1a;GBK php采用&#xff1a;UTF-8 这个编码架构指的就是文本所代表的字节集&#xff0c;比如易语言中“你好”表示的就是{196,227,186,195} 窗口程序集名保 留 保 留备 注窗口程序集_启动窗口 …

web-SQL注入-CTFHub

前言 在众多的CTF平台当中&#xff0c;作者认为CTFHub对于初学者来说&#xff0c;是入门平台的不二之选。CTFHub通过自己独特的技能树模块&#xff0c;可以帮助初学者来快速入门。具体请看官方介绍&#xff1a;CTFHub。 作者更新了CTFHub系列&#xff0c;希望小伙伴们多多支持…