《MySQL是怎样运行的》读书笔记(二) 从一条记录说起-InnoDB记录结构

前言

到现在为止, MySQL 还是一个黑盒,只知道使用客户端发送请求并等待服务器返回结果

那么表中的数据到底存到了哪里?以什么格式存放的? MySQL 是以什么方式来访问的这些数据?

相应的知识储备我只知道MySQL 服务器上负责对表中数据的读取和写入工作的部分是存储引擎 ,而服务器又支持不同类型的存储引擎,比如 InnoDB 、 MyISAM 、 Memory

那么就从这里开始使用 InnoDB 作为存储引擎来理解数据存储结构!

InnoDB

初学者の总览

InnoDB 是一个将表中的数据存储到磁盘上的存储引擎(关机保存)

真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。

我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB 存储引擎不需要一条一条的把记录从磁盘上读出来,而是将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB 内容刷新到磁盘中。

InnoDB行格式

总共四种:Compact(存储小型数据和索引,因为它将NULL和0值存储为一个位来节省空间) 、 Redundant(弃用了) 、 Dynamic(默认,能够根据数据的需要动态地调整存储空间) 和 Compressed(使用了压缩算法来减小数据的存储空间)

Compact行格式

从图中可以看出来,一条完整的记录其实可以被分为 记录的额外信息 和 记录的真实数据 两部分

记录的额外信息是服务器为了描述这条记录而不得不额外添加的一些信息,分别是 变长字段长度列表 、 NULL值列表 和 记录头信息

变长字段长度列表

MySQL 支持一些变长的数据类型,比如 VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型,各种 BLOB 类型,我们可以把拥有这些数据类型的列称为 变长字段 ,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来

所以这些变长字段占用的存储空间分为两部分: 真正的数据内容 + 占用的字节数

在 Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放

比如一条记录中有三列是变长字段,细节如下图:

那么实际效果是:

至于每个内容长度的编码,如果该可变字段允许存储的最大字节数( M×W )超过255字节并且真实存储的字节数( L ) 超过127字节,则使用2个字节,否则使用1个字节

此外,变长字段长度列表中只存储值为 NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的。如果这条记录没有变长字段,  这一行就没有变长字段长度列这部分。

NULL值列表

我们知道表中的某些列可能存储 NULL 值,如果把这些 NULL 值都放到 记录的真实数据 中存储会很占地方,所以 Compact 行格式把这些值为 NULL 的列统一管理起来,存储到 NULL 值列表中,它的处理过程是这样的:

1. 首先统计表中允许存储 NULL 的列有哪些(如果表中没有允许存储 NULL 的列,那么 NULL 也不存在了)

2. 将每个允许存储 NULL 的列对应一个二进制位,二进制位按照列的顺序逆序排列,值为1表示NULL,0表示非NULL

同时 NULL 值列表长度必须是字节长度的整数倍(8的倍数),位数不够的话高位补0

记录头信息

记录头信息由固定的 5 个字节组成,如图:

真实案例

现在实例表如下,设置了 ROW_FORMAT=COMPACT

对于 该表来说, 记录的真实数据 除了 c1 、 c2 、 c3 、 c4 这几个我们自己定义的列的数据以外, MySQL 会为每个记录默认的添加一些列(也称为隐藏列 ),具体的列如下:

这是因为InnoDB 表对主键的生成策略:

优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个 Unique 键作为主键,如果表中连 Unique 键都没有定义的话,则 InnoDB 会为表默认添加一个名为row_id 的隐藏列作为主键。所以我们从上表中可以看出:InnoDB存储引擎会为每条记录都添加 transaction_id 和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)

于是这两条记录的真实存储为:

Redundant行格式

该格式在现行的MySQL中已弃用,简单看看

刚才的案例在该格式下:

Redundant 行格式的开头是字段长度偏移列表 ,与Compact的变长字段长度列表有两处不同:

1. Redundant 行格式会把该条记录中所有列(包括隐藏列 )的长度信息都按照逆序存储到字段长度偏移列表 。
2. Redundant 行格式计算列值长度的方式不像 Compact 行格式那么直观,而是采用两个相邻数 值的差值来计算各个列值的长度。

比如第一条记录的字段长度偏移列表是: 25 24 1A 17 13 0C 06,因为它是逆序排放的,所以按照列的顺序排列为:06 0C 13 17 1A 24 25

按照偏移量差值计算长度:

第一列(`row_id`)的长度就是 0x06个字节,也就是6个字节。

第二列(`transaction_id`)的长度就是 (0x0C - 0x06)个字节,也就是6个字节。

第三列(`roll_pointer`)的长度就是 (0x13 - 0x0C)个字节,也就是7个字节。

第四列(`c1`)的长度就是 (0x17 - 0x13)个字节,也就是4个字节。

第五列(`c2`)的长度就是 (0x1A - 0x17)个字节,也就是3个字节。

第六列(`c3`)的长度就是 (0x24 - 0x1A)个字节,也就是10个字节。

第七列(`c4`)的长度就是 (0x25 - 0x24)个字节,也就是1个字节。

Redundant的记录头信息跟 Compact 的差距不大,就只对接了字段长度偏移列表

因为 Redundant 行格式并没有 NULL值列表 ,所以在 字段长度偏移列表中的各个列对应的偏移量处做了一些特殊处理 —— 将列对应的偏移量值的第一个比特位作为是否为 NULL 的依据,该比特位也可以被称之为 NULL比特位 。也就是说在解析一条记录的某个列时,首先看一下该列对应的 偏移量的 NULL比特位 是不是为 1 ,如果为 1 ,那么该列的值就是 NULL ,否则不是 NULL 。

行溢出数据

我们知道对于 VARCHAR(M) 类型的列最多可以占用 65535 (每个2字节==16位)个字节。其中的 M 代表该类型最多存储的字符数量,如果我们使用 ascii 字符集的话,一个字符就代表一个字节

如果该 VARCHAR 类型的列没有 NOT NULL 属性,那最多只能存储 65532 个字节的数据,因为真实数据的长度可能占用2个字节, NULL 值标识需要占用1个字节。

由于MySQL 是以页为基本单位来管理存储空间的,我们的记录都会被分配到某个 页 中存储。而一个页的大小一般是 16KB ,也就是 16384 字节,而一个 VARCHAR(M) 类型的列就最多可以存储 65532 个字节,这样就可能造成一个页存放不了一条记录的情况。

对于 Compact 和 Reduntant 行格式来说,如果某一列中的数据非常多的话,在本记录的真实数据处只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个 过程也叫做行溢出 ,存储超出 768 字节的那些页面也被称为溢出页 。

每个页除了存放我们的记录以外,也需要存储一些额外的信息,需要 136 个字节的空间,其他的空间都可以被用来存储记录。

而每个记录需要的额外信息是 27 字节,包括

2个字节用于存储真实数据的长度 

1个字节用于存储列是否是NULL值 

5个字节大小的头信息

6个字节的 row_id 列 

6个字节的 transaction_id 列 

7个字节的 roll_pointer 列

假设一个列中存储的数据字节数为n,那么发生行溢出现象时需要满足这个式子: 

136 + 2×(27 + n) > 16384,求解这个式子得出的解是: n > 8098 。也就是说如果一个列中存储的数据不大于 8098 个字节,那就不会发生行溢出 ,否则就会发生行溢出 。

Dynamic和Compressed行格式

这两个行格式和 Compact 行格式挺像,只不过在处理行溢出数据时有点儿分歧,它们不会在记 录的真实数据处存储字段真实数据的前 768 个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址

InnoDB页结构

数据页结构的快速浏览

数据页代表的这块 16KB 大小的存储空间可以被划分为多个部分,各个部分如图所示:

记录在页中的存储

在页的7个组成部分中,用户存储的记录会按照我们指定的行格式存储到 User Records 部分。

但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分。

当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下:

管理User Records的案例

为了更好的管理在 User Records 中的这些记录,InnoDB 有哪些机制呢?

下面创建一个表,有3个列,其中 c1 和 c2 列是用来存储整数的, c3 列是用来存储字符串的。

由于把 c1 列指定为主键,所以在具体的行格式中InnoDB就没必要为我们去创建row_id 隐藏列了

表中记录的行格式示意图:

记录头信息

下面我们试着向表中插入几条记录:

这些记录的示意图:

1. delete_mask标记着当前记录是否被删除,占用1个二进制位,值为 0 的时候代表记录并没有被删除,为 1 的时候代表记录被删除掉了(将这个delete_mask位设置为1和将被删除的记录加入到垃圾链表中是两个阶段)

2. min_rec_mask: B+树的每层非叶子节点中的最小记录都会添加该标记

3. n_owned:在页目录分组时使用,每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录

4. heap_no:表示当前记录在本页中的位置,从图中可以看出来,我们插入的4条记录在本页中的位置分别是 2 、 3 、 4 、 5 ,为什么没有1呢?

首先对于一条完整的记录来说,比较记录的大小就是比较主键的大小。InnoDB 自动给每个页里加了两个伪记录,一个代表最小记录 ,一 个代表最大记录

实际存储时这两部分是隔开的,从图中我们可以看出来,最小记录和最大记录的 heap_no 值分别是 0 和 1 ,也就是说它们的位置最靠前

5. record_type:表示当前记录的类型,一共有4种类型的记录, 0 表示普通记录, 1 表示B+树非叶节点记录, 2 表示最小记录, 3 表示最大记录。从图中我们也可以看出来,我们自己插入的记录就是普通记录,它们的record_type 值都是 0 ,而最小记录和最大记录的 record_type 值分别为 2 和 3 。

6. next_record:表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量(这样就可以用链表的方式处理)

但下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum

可以发现记录按照主键从小到大的顺序形成了一个单链表

如果从中删除掉一条记录,这个链表也是会跟着变化的,比如我们把第2条记录删掉:

这之后存储结构变成这样,可以发现就是链表的删除方式:

从图中可以看出来,删除第2条记录前后主要发生了这些变化:

1. 第2条记录并没有从存储空间中移除,而是把该条记录的 delete_mask 值设置为 1 。
2. 第2条记录的 next_record 值变为了0,意味着该记录没有下一条记录了。
3. 第1条记录的 next_record 指向了第3条记录。
4. 最大记录的 n_owned 值从 5 变成了 4 

如果这以后再插入刚刚删除的那条记录,就会复用删除的记录。当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。

Page Directory

现在我们了解了记录在页中按照主键值由小到大顺序串联成一个单链表,那如果我们想根据主键值查找页中的某条记录该咋办呢?比如说这样的查询语句:

暴力做法:从 Infimum 记录(最小记录)开始,沿着链表一直往后找,总会找到,或者找不到

因为链表中各个记录的值是按照从小到大顺序排列的,所以当链表的某个节点代表的记录的主键值大于想要查找的主键值时,就可以停止查找了,因为该节点后边的节点的主键值依次递增。

优化后的算法:制作页目录

1. 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。

2. 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记 录,也就是该组内共有几条记录。

3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个就是页目录。页目录中的这些地址偏移量被称为槽 (Slot ),所以这个页目录就是由槽组成的。

示意图如下:

对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录, 最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间

分组是按照下边的步骤进行的:

1. 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。

2. 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的 n_owned 值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。

3. 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一 个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。

下面往表中添加了12条记录,现在页里边就一共有18条记录了(包括最小和最大记录),这些记录被分成了5个组,如图所示:

现在看怎么从页目录中查找记录。

因为各个槽代表的记录的主键值都是从小到大排序的,所以我们可以使用所谓的 二分法 来进行 快速查找。4个槽的编号分别是: 0 、 1 、 2 、 3 、 4 ,所以初始情况下最低的槽就是 low=0 ,最高的槽就是high=4 。比方说我们想找主键值为 6 的记录,过程是这样的:

1. 计算中间槽的位置: (0+4)/2=2 ,所以查看 槽2 对应记录的主键值为 8 ,又因为 8 > 6 ,所以设置 high=2 , low 保持不变。

2. 重新计算中间槽的位置: (0+2)/2=1 ,所以查看 槽1 对应的主键值为 4 ,又因为 4 < 6 ,所以设置 low=1 , high 保持不变。

3. 因为 high - low 的值为1,所以确定主键值为 5 的记录在槽2对应的组中。此刻我们需要找到 槽2 中主键值最小的那条记录,然后沿着单向链表遍历槽2 中的记录。但是我们前边又说过,每个槽对应的记录都是该 组中主键值最大的记录,这里槽2对应的记录是主键值为 8 的记录,怎么定位一个组中最小的记录呢?别忘了各个槽都是挨着的,我们可以很轻易的拿到 槽1 对应的记录(主键值为 4 ),该条记录的下一条记录就是 槽2 中主键值最小的记录,该记录的主键值为 5 。所以我们可以从这条主键值为 5 的记录出发,遍历槽 2 中的各条记录,直到找到主键值为 6 的那条记录即可。由于一个组中包含的记录条数只能是1~8条,所以 遍历一个组中的记录的代价是很小的。

所以在一个数据页中查找指定主键值的记录的过程分为两步:

1. 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。

2. 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。

Page Header

为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第 一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫 Page Header 的部分,它是页结构的第二部分,这个部分占用固定的 56 个字节,专门存储各种状态信息

File Header

Page Header 是专门针对数据页记录的各种状态信息,比如页里有多少记录,有多少个槽。

我们现在描述的 File Header 针对各种类型的页都通用,也就是说不同类型的页都会以 File Header 作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、 下一个页是哪个。这个部分占用固定的 38 个字节,是由下边这些内容组成的:

File Trailer

我们知道 InnoDB 存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一 半的时候中断电了就寄了。为了检测一个页是否完整, InnoDB 在每个页的尾部都加了一个 File Trailer 部分,这个部分由 8 个字节组成,可以分成2个小部分:

1. 前4个字节代表页的校验和

这个部分是和 File Header 中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为 File Header 在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电 了,那么在 File Header 中的校验和就代表着已经修改过的页,而在 File Trialer 中的校验和代表着原先的页,二者不同则意味着同步中间出了错。

2. 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言:文件操作(下)

片头 嗨&#xff01;小伙伴们&#xff0c;在前2篇中&#xff0c;我们分别讲述了C语言&#xff1a;文件操作&#xff08;上&#xff09;和 C语言&#xff1a;文件操作&#xff08;中&#xff09;&#xff0c;今天我们将会学习文件操作&#xff08;下&#xff09;&#xff0c;准…

C语言异步编程

回调函数在异步编程中有着重要的作用&#xff0c;在接口编程应用广泛&#xff0c;实战中经常会注册一个回调函数来接收其他程序返回的数据&#xff0c;这样做的好处是调用者无需主动去获取被调用者的数据&#xff0c;仅仅需要回调&#xff0c;让被调用者去传送数据&#xff0c;…

VS code 同步odata服务

在做UI5得开发过程中&#xff0c;经常会出现odata需要更新 那么已经加载过得项目如何去跟新odata服务呢 可以通过如下步骤 1.右键打开应用信息 2.找到manage service models 3.点击编辑 4.选中 刷新并保存

C++ | Leetcode C++题解之第60题排列序列

题目&#xff1a; 题解&#xff1a; class Solution { public:string getPermutation(int n, int k) {vector<int> factorial(n);factorial[0] 1;for (int i 1; i < n; i) {factorial[i] factorial[i - 1] * i;}--k;string ans;vector<int> valid(n 1, 1);…

mysql主库delete一个没主键的表导致从库延迟很久问题处理

一 问题描述 发现线上环境一个从库出现延迟&#xff0c;延迟了2天了&#xff0c;还没追上主库。 查看当前运行的sql及事务&#xff0c;发现这个sql语句是在delete一个没主键的表。 二 问题模拟 这里在测试环境复现下这个问题。 2.1 在主库造数据 use baidd; CREATE TABL…

arthas watch怎么监控指定参数值?

watch com.codex.terry.controller.HelloWorldController getUserInJson2 "{params,returnObj}" params[0] "world" -v 要分析的代码&#xff1a; 浏览器请求http://localhost:8080/say/tt&#xff0c;arthas控制台打印的信息如下&#xff1a; 浏览器请…

LeetCode45:跳跃游戏Ⅱ

题目描述 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n …

C++成员初始化列表

我们在类的构造函数中使用成员初始化列表可以带来效率上的提升&#xff0c;那么成员初始化列表在编译后会发生什么就是这篇文章要探究的问题 文章目录 引入成员初始化列表用成员初始化列表优化上面的代码成员初始化列表展开成员初始化列表的潜在危险 参考资料 引入 考虑下面这…

TDengine写入2000万数据查询体验

最近在寻找时序数据库&#xff0c;想应用在公司的项目上。 上一篇文章实验了InfluxDB:windows上使用influx2.7学习,还学习了flux语言&#xff0c;最后发现宽表查询比较困难&#xff0c;就放弃了&#xff0c;于是决定试试国产时序数据库TDengine 参考 官方文档&#xff1a;htt…

有货源和分销单品爆款玩法课

该课程专注于教授如何利用有货源和分销渠道&#xff0c;打造单品爆款销售策略。学员将学习货源获取、产品定位、市场推广等关键技巧&#xff0c;通过实战案例和实操训练&#xff0c;掌握成功销售单品爆款的方法&#xff0c;提升销售业绩和市场竞争力。 课程大小&#xff1a;6.…

RabbitMq基础概念知识复习

消息拥有消息头和消息体&#xff0c;消息具有rounting key&#xff0c;主题交换机和扇形交换机都是发布与订阅的实现方式&#xff0c;主题交换机用于匹配接收的消息的rount key 动态匹配模式匹配到多个符合的队列&#xff0c;扇形fanout交换机则不会使用消息的路由key&#xff…

【AIGC调研系列】InternVL开源多模态模型与GPT-4V的性能对比

InternVL和GPT-4V都是多模态模型&#xff0c;但它们在性能、参数量以及应用领域上有所不同。 InternVL是一个开源的多模态模型&#xff0c;其参数量为60亿&#xff0c;覆盖了图像/视频分类、检索等关键任务&#xff0c;并在32个视觉-语言基准测试中展现了卓越性能[2]。InternV…

CYP76AKs的功能分化塑造了鼠尾草属中松香烷型二萜化合物的化学多样性-比较转录组-文献精读12

Functional divergence of CYP76AKs shapes the chemodiversity of abietane-type diterpenoids in genus Salvia "CYP76AKs的功能分化塑造了鼠尾草属中松香烷型二萜化合物的化学多样性"&#xff0c;一片比较转录组的文献&#xff0c;类似比较转录组分析揭示了116种…

数据仓库Data Warehouse

数据仓库Data Warehouse 数仓是一种思想,数仓是一种规范,数仓是一种解决方案 1. 数据处理方式 数据处理大致可以分成两大类: 联机事务处理OLTP(on-line transaction processing)联机分析处理OLAP(On-Line Analytical Processing)1.1. OLTP OLTP的全称是On-line Transa…

O2OA开发平台前端源码级二次开发(Vue3,React)

在使用O2OA进行项目定制化开发时&#xff0c;我们可以开发新的前端组件&#xff08;x_component&#xff09;以扩展O2OA来实现更多的业务。这种新增前端组件或者前端业务的开发通常会配合后端自定义应用实现的服务来完成系统内数据的交互。在当系统默认的界面不符合系统UI/UE设…

283. 移动零 11. 盛最多水的容器

283. 移动零 把非零的数全交换到前面去 11. 盛最多水的容器 双指针&#xff1a; 双指针一个在头 一个在尾部 计算两个边的盛水量并更新数值 左右两边 哪边矮就移动哪边

区块链快速参考(三)

原文&#xff1a;zh.annas-archive.org/md5/b5e57485b0609afbfba46ff759c5d264 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第十七章&#xff1a;去中心化应用程序 去中心化应用&#xff08;DApps&#xff09;是在去中心化网络上运行的应用程序&#xff0c;不受集…

2024五一杯数学建模B题思路分析 - 未来新城背景下的交通需求规划与可达率问题

文章目录 1 赛题选题分析 2 解题思路详细的思路过程放在文档中 ! ! &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;3 最新思路更新 1 赛题 B题 未来新城背景下的交通需求规划与可达率问题 随着城市化的持续发展&#xff0c;交通规划在新兴城市建设中显得尤为关…

Linux搭建靶场

提前准备&#xff1a; 文章中所使用到的Linux系统&#xff1a;Ubantu20.4sqlilabs靶场下载地址&#xff1a;GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. 一. 安装phpstudy phpstudy安装命令&#xff1a;wget -O install.sh h…

Map和Set基础

目录 一、导论 二、Map 三、Set 本文找先不涉及两种数据结构的底层&#xff0c;目标是&#xff1a; 理解Map和Set的大体框架&#xff0c;了解他们有什么用&#xff0c;用在哪里的&#xff0c;然后再从浅层深入底层。 小编认为&#xff1a; 先了解也下Map和Set大体是用来做…