InnoDB数据存储结构

1数据库的存储结构:页

索引结构给我们提供了高效的索引方式,虽然索引信息以及数据记录都是保存在ibd文件中,确切的说索引信息是存储在页结构中的。另一方面,索引是在存储引擎中实现的,MySQL服务器上的存储引擎负责对表中的数据的读取和写入工作。不同存储引擎存放格式是不一样的,比如Memory是存在内存中的,MyiSAM的表结构存放在frm文件,数据在MYD文件中,索引在MYI文件中。本文主要剖析InnoDB存储引擎的数据存储结构。

1.1磁盘与内存交互基本单位:页

InnoDB将数据划分为若干个页,InnoDB页的大小默认为16KB;
以页作为磁盘和内存之间交互的基本单位,意思就是无论是读取磁盘内容到内存中,还是把内存数据刷到磁盘上,至少是一页。也就是说,在数据库中,无论是读取一行还是多行,都是将这些行所在的页进行加载。数据库管理存储空间和I/O操作的最小单位是页。一页之中可以存多行数据。

1.2页结构概述

页a,页b,页c…这些页可以不在物理结构上连续,只要通过双向链表相关即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个单项链表。每个数据页都会为存储在它里面的记录生成一哥页目录,再通过主键查找某条记录的时候可以再页目录中使用二分法快速定位到对于的slot(槽),然后再遍历该槽对应分组中的记录既可快速找到指定的记录。

名称占用大小说明
File Header(文件头)38字节文件头,描述页的信息
Page Header(页头)56字节页头,页的状态信息
Infimum+Supremum(最大最小记录)26字节最大和最小记录,这是两个虚拟的行记录
User Records(用户记录)不确定用户记录,存储行记录内容
Free Space(空闲空间)不确定空闲记录,页中还没有被使用的空间
Page Directory(页目录)不确定页目录,存储用户记录的相对位置
File Trailer(文件尾)8字节文件尾,校验页是否完整

1.3页的大小

不同数据库管理系统的页大小不同。在MySQL中InnoDB存储引擎中,默认页的大小16KB。SQL Server中页的大小是8KB,而在Oracle中我们用“块”代表“页”,Oracle支持的块的大小为2,4,8,16,32,64KB。
我们可以通过一下命令来查看MySQL中页默认大小。

SHOW VARIABLES LIKE "%innodb_page_size%"
Variable_nameValue
innodb_page_size16384

2页的内部结构

我们可以把页的结构分成三个部分

名称占用大小说明
File Header(文件头)38字节文件头,描述页的信息
Page Header(页头)56字节页头,页的状态信息
Infimum+Supremum(最大最小记录)26字节最大和最小记录,这是两个虚拟的行记录
User Records(用户记录)不确定用户记录,存储行记录内容
Free Space(空闲空间)不确定空闲记录,页中还没有被使用的空间
Page Directory(页目录)不确定页目录,存储用户记录的相对位置
File Trailer(文件尾)8字节文件尾,校验页是否完整

2.1第一部分File Header(文件头)和File Trailer(文件尾)

文件头的作用:描述各种页的通用信息

名称占用大小说明
FIL_PAFE_SPACE_OR_CHKSUM4字节页的校验和
FIL_PAFE_OFFSET4字节页号
FIL_PAFE_PREV4字节上一页的页号
FIL_PAFE_NEXT4字节下一页的页号
FIL_PAFE_LSN8字节页面被最后修改时对应的日志序列位置
FIL_PAFE_TYPE2字节页的类型
FIL_PAFE_FILE_FLUSH_LSN8字节仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAFE_ARCH_LOG_NO_OR_SPACE_ID4字节页属于哪个表空间

2.1.1FIL_PAFE_TYPE 当前页的类型

类型名称十六进制描述
FIL_PAFE_TYPE_ALLOCATED0x0000最新分配还没有使用
FIL_PAFE_UNDO_LOG0x0002Undo日志页(用于事务的回滚)
FIL_PAFE_INODE0x0003段信息节点
FIL_PAFE_IBUF_FREE_LIST0x0004Insert Buffer空闲列表
FIL_PAFE_IBUF_BITMAP0x0005Insert Buffer位图
FIL_PAFE_TYPE_SYS0x0006系统页
FIL_PAFE_TYPE_TRX_SYS0x0007事务系统数据
FIL_PAFE_TYPE_FSP_HDR0x0008表空间头部信息
FIL_PAFE_TYPE_XDES0x0009扩展描述页
FIL_PAFE_TYPE_ALLOCATED0x000A溢出页
FIL_PAFE_TYPE_ALLOCATED0x45BF索引页,也就是我们所说的数据页

2.1.2FIL_PAFE_PREV和FIL_PAFE_NEXT

InnoDB都是以页为单位存放数据的,如果数据分散到多个不连续的页中存储的话需要把这个些页关联起来,
FIL_PAFE_PREV 和FIL_PAFE_NEXT就分别代表本页的上一页和下一页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,保证这些页之间不需要物理空间上的连续,而是逻辑上的连续。
在这里插入图片描述

2.1.3FIL_PAFE_SPACE_OR_CHKSUM

FIL_PAFE_SPACE_OR_CHKSUM代表当前页面的校验和。

2.1.3.1校验和简介

就是对于一个很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较段的值就称为校验和。(类似hash算法)
在比较两个很长的字节串之前,先比较两个长字节串的校验和,如果校验和都不一样,则两个长字节串长度肯定是不同的,省去了比较长字节串的时间损耗。

2.1.3.2校验和作用

InnoDB存储引擎以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在把数据从内存刷到磁盘中的时候断电了,造成了该页的传输不完整。
为了检测一个页是否完整,这时可以通过文件尾的校验和与文件头的校验和做比对,如果两个值不相等则证明页的传输有问题,需要重新传输,否则认为页的传输已经完成。因为File Header在页的前面,所以校验和会被首先同步到磁盘,当完全写完时,也会写File Trailer,如果成功首尾的校验和应当一致。这里校验方式就是采用hash算法进行校验。

2.1.4FIL_PAFE_LSN

页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number)。在File Trailer前四个节表示校验和,后四个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明数据从内存刷到磁盘的时候出现了问题。

2.2第二部分Infimum+Supremum(最大最小记录),User Records(用户记录) 和Free Space(空闲空间)

第二部分是记录部分,页的主要作用是存储记录,所以“最大和最小记录”和“用户记录”部分占了页结构的主要空间。
我们自己存储的记录会按照指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,还有新的记录插入的话,就需要去申请新的页。
在这里插入图片描述

2.2.1User Records(用户记录)

User Records中的这些记录按照指定的行格式一条一条摆在User Records部分,互相之间形成单链表。

2.2.2Infimum + Supremum(最大最小记录)

记录是可以比大小的,对于一条完整的记录来说,比较记录的大小就是比较主键的大小。
InnoDB规定的最小记录和最大记录这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成的,如图所示:
在这里插入图片描述
在这里插入图片描述
这两条记录不是我们自己定义的记录,所以他它们并不存放在页的User Records部分,他们被单独放在了一个称为Infimum + Supremum的部分。

2.3第三部分Page Directory(页目录)和Page Header(页面头部)

2.3.1Page Directory(页目录)

在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况需要遍历链表上的所有节点才能完成检索,因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索,提升效率。
方式一:顺序查找
从Infimum记录开始,沿着链表一直往后找,总有一天会找到,在找的时候还能投机取巧,因为链表中各个记录的值是按照从小到大顺序排序的,所以当链表的某个节点代表的记录的主键值大于你想要查找的主键值是,你就可以停止查找了,因为该节点后面的节点的主键值依次递增。
如果一个页中存储了非常多的记录,这么查找性能很差。
方式二:使用页目录,二分查找

  1. 将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。
  2. 第1组,也就是最小记录所在的分组只有一个记录;最后1组,就是最大记录所在的分组,会有1-8条记录;其余的组记录量在4-8条之间,这样做的好处是,除了第1组外,其余组的记录数会尽量平分
  3. 在每个组中最后一条记录的头信息会存储该组一共有多少条记录作为n_owned字段。
  4. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录
    举例1
    在这里插入图片描述
    举例2
    现在的page_demo表中正常的记录共有6条,InnoDB会把他们分成两组,第一组中只有一个最小记录,第二组种是剩下的五条记录。如图:
    在这里插入图片描述
    这是Page_Directory部分
    在这里插入图片描述
    小结:
    在一个数据页中查找指定主键值的记录的过程分为2步:
  5. 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
  6. 通过记录的next_record属性遍历该槽所在的组中的各个记录。

2.3.2Page Header(页面头部)

为了能得到一个数据页中存储的记录的状态信息,这个部分占用固定的56个字节,专门存储各种状态信息。

名称占用空间大小描述
PAGE_N_DIR_SLOTS2字节在页目录中的槽数量
PAGE_HEAP_TOP2字节还未使用的空间最小地址,也就是说从该地址之后就是Free Space
PAGE_N_HEAP2字节本页中的记录的数量(包括最小和最大记录以及标记为删除的记录)
PAGE_FREE2字节第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用)
PAGE_GARBAGE2字节已删除记录占用的字节数
PAGE_LAST_INSERT2字节最后插入记录的位置
PAGE_DIRECTION2字节记录插入的方向
PAGE_N_DIRECTION2字节一个方向连续插入的记录数量
PAGE_N_RECS2字节该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录)
PAGE_MAX_TRX_ID8字节修改当前页的最大事务ID,该值仅在二级索引中定义
PAGE_LEVEL2字节当前页在B+树中所处的层级
PAGE_INDEX_ID8字节索引ID,表示当前页数据哪个索引
PAGE_BTR_SEG_LEAF10字节B+树叶子段的头部信息,仅在B+树的Root页定义

3InnoDB行格式(或记录格式)

3.1指定行格式的语法

我们平时的数据以行为单位来向表中插入数据,这些记录在磁盘上的存放方式也被称为‘行格式’或者‘记录格式’。InnoDB存储引擎设计了四种不同类型的‘行格式’,分别是Compact,Redundant,Dynameic和Compressed行格式。
查看Mysql的默认行格式

SELECT @@INNODB_DEFAULT_ROW_FORMAT;
@@INNODB_DEFAULT_ROW_FORMAT
dynamic
也可以使用如下语法查看表具体使用的行格式
SHOW TABLE STATUS like '表名';

在这里插入图片描述
在创建或修改表的语句中指定行格式:

CREATE TABLE 表名 (列的信息) ROW_FORMAT = 行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称

3.2COMPACT行格式

在MySQL5.1版本中,默认设置为Compact行格式。一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。
行格式示意图
在这里插入图片描述

3.2.1变长字段长度列表

MySQL支持一些变长的数据类型,比如VARCHAR、VARBINARY、TEXT类型、BLOB类型。这些数据类型修饰列称为变长字段,变长字段中存储多少字节的数据不是固定的,所有我们在存储真实数据的时候需要顺便把这些数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。
注意:这里存储的变长长度和字段顺序是反过来的。比如两个varchar字段在表结构的顺序是a(15),b(10)。那么在变长字段长度列表中存储的长度顺序就是15,10,是反过来的。

CREATE TABLE record_test (
 col1 VARCHAR ( 8 ),
 col2 VARCHAR ( 8 ) NOT NULL,
 col3 CHAR ( 8 ),
 col4 VARCHAR ( 8 ) )
CHARSET = ASCII ROW_FORMAT = COMPACT;
#插入数据
INSERT INTO record_test (col1, col2,col3,col4) VALUES("zhangsan", "lisi","wangwu","zhaoliu")
("sunqi", "zhouba",NULL,NULL)

假设一张表的结构有col1,col2,col4三列都是VARCHAR(8)类型的,所有这三个列的值的长度都需要保存在记录开头处,注意record_test表中的各个列都使用的ASCII字符集(每个字符只需要一个字节来编码)
在这里插入图片描述
又因为这些长度值需要按照列的逆序存放,所以最后变长字段长度列表的字节串用十六进制标志的效果就是070408。

3.2.2NULL值列表

Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储NULL的列,则NULL值列表也不存在了。

之所以要存储NULL是因为数据都是需要对齐的,如果没有标注出来的NULL值的位置,就有可能在查询数据的时候出现混乱。如果使用一个特定的符号放到相应的数据位表示空置的话,虽然能达到效果,但是这样浪费空间,所有直接就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据,格式如下:
1.二进制位的值为1时,代表该列的值为NULL。
2.二进制位的值为0时,代表该列的值不为NULL。

例如:字段a、b、c,其中a是主键,在某一行中存储的数依次是a=1,b是null,c=2。那么Compact行格式中的NULL值列表中存储:01。第一个0表示c不为null,第二个1表示b是null。这里之所有没有a是因为a是主键会跳过。

上面record_test表插入了两条记录(“zhangsan”, “lisi”,“wangwu”,“zhaoliu”),
(“sunqi”, “zhouba”,NULL,NULL),两条记录对应的NULL值列表如下
第一条记录:
在这里插入图片描述第二条记录:
在这里插入图片描述

3.2.3记录头信息

在这里插入图片描述

名称大小(bit)描述
预留位11没有使用
预留位21没有使用
delete_mask1标记该记录是否被删除
min_rec_mask1B+树的每层非叶子结点中的最小记录都会添加该标记
n_owned4表示当前记录拥有的记录数
heap_no13表示当前记录在记录堆的位置信息
record_type3表示当前记录的类型,0表示普通记录,1表示B+树非叶子结点记录,2表示最小记录,3表示最大记录
next_record16表示下一条记录的相对位置

假设插入(1,100,‘zhang’),(2,200,‘li’),(3,300,‘wang’)
,(4,400,‘zhao’),在物理位置上是连续的
在这里插入图片描述

3.2.3.1Delete_mask

标记该记录是否被删除,值为0代表记录并没有被删除,为1代表记录被删除了。

实际意义上被删除的记录还是会存在磁盘之中,因为如果从磁盘上移除之后,其他的记录在磁盘上需要重写排列,导致性能消耗。所以只是打上一个删除标记而已,所以被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中记录占用的空间称为可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。

3.2.3.2Min_rec_mask

B+树的每层非叶子节点的最小记录都会添加该标记,min_rec_mask值为1。
我们自己插入的四条记录的min_rec_mask值都是0,意味着他们都不是B+树的非叶子节点中的最小记录。

3.2.3.3Record_type

这个属性代表当前记录的类型

描述
0普通记录
1B+树非叶节点记录
2最小记录
3最大记录
上图中我们可以看出来,自己插入的数据就是普通记录,record_type都是0,而最大最小记录分别是3和2,1的情况就是索引。
3.2.3.4Heap_no

这个属性表示当前记录在本页的位置。我们插入的分别是2,3,4,5。
为什么没有0和1?
MySQL会自动给每页里加两个记录,由于这个记录并不是我们自己插入的,也会称为伪记录或者虚拟记录。这两个伪记录0代表最小记录,1代表最大记录,也就是说他们的位置最靠前。
在这里插入图片描述

3.2.3.5N_owned

页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录。详情见page_direcctory

3.2.3.6Next_record

记录头信息里该属性非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
比如:3.2.2的图,第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址向后找32个字节便是下一条记录的真实数据。
注意:下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定Infimum记录的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是Supremum记录
next_record按照下图顺序连接,物理存储上面最大最小记录是放在一起的,所以最后一条记录负的往上面指。
在这里插入图片描述
如果从表中删除了第二条数据,链表也是会变换的。从图中可以看出来,删除第二条记录前后的变化:
1.第二条记录并没有从存储空间移除,而是把该条记录的delete_mask的值设置为1
2.第二条记录的next_record值变为0,意味着该记录没有下一条记录
3.第一条记录的next_record指向了第三条记录
4.最大的记录n_owned值从5变成了4
在这里插入图片描述
现在向表中添加主键值为2的记录,这条新插入的记录复用了被删除记录的存储空间。(当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后可以重用这部分的存储空间
在这里插入图片描述

3.2.4记录的真实数据

记录的真实数据除了我们自己的定义的列的数据之外,还会有三个隐藏列:
在这里插入图片描述实际上这几列的真正名称是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。
一个表没有手动定义主键,则会选取一个unique键作为主键,如果连Unique键都没有定义的话,则会为表默认添加一个名为row_id的隐藏列作为主键。所有row_id是在没有自定义主键以及Unique键的情况下才会存在。
事务ID和回滚指针在后面的文章中再讲解。

3.3Dynamic和Compressed行格式

3.3.1行溢出

MySQL中的 varchar 是有最大长度限制的,这个值是 65535 个字节,但是建表设置65535长度会报错。

CREATE TABLE varchar_size_demo(
	c VARCHAR(65535)
)CHARSET = ASCII ROW_FORMAT=COMPACT
> 1118 - Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
> 时间: 0.017s

正常运行的SQL语句

CREATE TABLE varchar_size_demo(
	c VARCHAR(65532) #65532 + 两个字节的变长字段的长度 + 1字节NULL值标识
)CHARSET = ASCII ROW_FORMAT=COMPACT;

如果加一个非空约束,则可以设置为65533正常运行的SQL语句

CREATE TABLE varchar_size_demo(
	c VARCHAR(65533) NOT NULL #65533 + 两个字节的变长字段的长度
)CHARSET = ASCII ROW_FORMAT=COMPACT;

通过这个例子,我们可以知道一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出。
在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中进行分页存储,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。
在这里插入图片描述

3.3.2Dynamic和Compressed行格式

在MySQL8.0中,默认行格式就是Dynamic。Dynamic、Compressed行格式和Compact行格式很像,只不过在处理行溢出数据时有分歧:
Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。如图,在数据也中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。
在这里插入图片描述
Compact和Redundant两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)。
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR着类大长度类型的数据能够进行非常有效的存储。

3.4Redundant行格式

Redundant是MySQL5.0版本之前InnoDB的行记录存储方式,MySQL5.0支持Redundant是为了兼容之前版本的页格式。
现在我们把表record_test的行格式修改为Redundant:
ALTER TABLE record_test ROW_FORMAT=Redundant;
在这里插入图片描述
从上图可以看到,不同于Compact行记录格式,Redundant行格式的首部是一个字段长度偏移列表,同样是按照列的顺序逆序放置的。

3.4.1字段长度偏移列表

注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同。
少了“变长”两个字:Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。
多了“偏移”两个字:意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
举例:比如第一条记录的字段长度偏移列表就是:
2B 25 1F 1B 13 0C 06
因为是逆序排放的,所以按照列的顺序排序就是:
06 0C 13 1B 1F 25 2B
按照两个相邻数值的差值来计算各个列值的长度的意思就是:
第一列(row_id)的长度就是0x06个字节,也就是6个字节。
第二列(transaction_id)的长度就是0x0C - 0x06个字节,也就是6个字节。
第三列(roll_pointer)的长度就是0x13 - 0x0C个字节,也就是7个字节。
第一列(col1)的长度就是0x1B - 0x13个字节,也就是8个字节。
第一列(col2)的长度就是0x1F - 0x1B个字节,也就是4个字节。

3.4.2记录头信息

不同于Compact行格式,Redundant行格式中的记录头信息固态占用6字节(48位),每位的含义见下表。

名称大小(bit)描述
()1未使用
()1未使用
deleted_mask1该行是否已被删除
min_rec-mask1B+树的每层非叶子结点中的最小记录都会添加该标记
n_owned4该记录拥有的记录数
heap_no13索引堆中该条记录的位置信息
n_fields10记录中列的数量
1byte_offs_flag1记录字段长度偏移列表中每个列对应的偏移量,使用1个字节还是2个字节表示
next_record16页中 下一条记录的绝对位置

与Compact行格式的记录头信息对比来看,有两处不同:
Redundant行格式多了n_field和1byte_offs_flag这两个属性。
Redundant行格式没有record_type这个属性。
其实,n_fields:代表一行中列的数量,占用10位,这也很好地解释了为什么MySQL一个行支持最多的列为1023。另一个值1byte_offs_flag,该值定义了偏移列表占用1个字节还是2个字节。当它的值为1时,表明使用1个字节存储。当它的值为0时,表明使用2个字节存储。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值