Mysql原理篇之表空间---05


前言

通过前边儿的内容大家知道,表空间是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名.ibd的实际文件。

在我们目前的认知中: 一个表的索引和数据信息由一颗聚簇索引树提供,这颗聚簇索引树每个节点都对应一个数据页,数据越多,树越高,节点越多,当前索引树占据数据页越多,而表空间就是负责管理当前表占用的一堆数据页的。

今天我们就来聊聊表空间是如何管理这一堆数据页的。


回顾

在正式开始之前,我们先来回顾一下之前的内容:

页面类型

InnoDB是以页为单位管理存储空间的,我们的聚簇索引(也就是完整的表数据)和其他的二级索引都是以B+树的形式保存到表空间的,而B+树的节点就是数据页。这个数据页的类型是:FIL_PAGE_INDEX,除了这种存放索引数据的页面类型之外,InnoDB也为了不同的目的设计了若干种不同类型的页面,为了唤醒大家的记忆,我们再一次把各种常用的页面类型提出来:

在这里插入图片描述
因为页面类型前边都有个FIL_PAGE或者FIL_PAGE_TYPE的前缀,为简便起见我们后边唠叨页面类型的时候就把这些前缀省略掉了,比方说FIL_PAGE_TYPE_ALLOCATED类型称为ALLOCATED类型,FIL_PAGE_INDEX类型称为INDEX类型。


页面通用部分

我们前边说过数据页,也就是INDEX类型的页由7个部分组成,其中的两个部分是所有类型的页面都通用的。当然我不能寄希望于你把我说的话都记住,所以在这里重新强调一遍,任何类型的页面都有下边这种通用的结构:

在这里插入图片描述
从上图中可以看出,任何类型的页都会包含这两个部分:

  • File Header:记录页面的一些通用信息
  • File Trailer:校验页是否完整,保证从内存到磁盘刷新时内容的一致性。

我们这里再强调一遍File Header的各个组成部分:

在这里插入图片描述
现在除了名称里边儿带有LSN的两个字段大家可能看不懂以外,其他的字段肯定都是倍儿熟了,不过我们仍要强调这么几点:

  • 表空间中的每一个页都对应着一个页号,也就是FIL_PAGE_OFFSET,这个页号由4个字节组成,也就是32个比特位,所以一个表空间最多可以拥有2³²个页,如果按照页的默认大小16KB来算,一个表空间最多支持64TB的数据。表空间的第一个页的页号为0,之后的页号分别是1,2,3…依此类推
  • 某些类型的页可以组成链表,链表中的页可以不按照物理顺序存储,而是根据FIL_PAGE_PREV和FIL_PAGE_NEXT来存储上一个页和下一个页的页号。需要注意的是,这两个字段主要是为了INDEX类型的页,也就是我们之前一直说的数据页建立B+树后,为每层节点建立双向链表用的,一般类型的页是不使用这两个字段的。
  • 每个页的类型由FIL_PAGE_TYPE表示,比如像数据页的该字段的值就是0x45BF,我们后边会介绍各种不同类型的页,不同类型的页在该字段上的值是不同的。

独立表空间结构

我们知道InnoDB支持许多种类型的表空间,这里重点关注独立表空间和系统表空间的结构。它们的结构比较相似,但是由于系统表空间中额外包含了一些关于整个系统的信息,所以我们先挑简单一点的独立表空间来唠叨,稍后再说系统表空间的结构。

区(extent)的概念

表空间中的页实在是太多了,为了更好的管理这些页面,设计InnoDB的大叔们提出了区(英文名:extent)的概念。对于16KB的页来说,连续的64个页就是一个区,也就是说一个区默认占用1MB空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。画个图表示就是这样:
在这里插入图片描述
其中extent 0 ~ extent 255这256个区算是第一个组,extent 256 ~ extent 511这256个区算是第二个组,extent 512 ~ extent 767这256个区算是第三个组(上图中并未画全第三个组全部的区,请自行脑补),依此类推可以划分更多的组。这些组的头几个页面的类型都是类似的,就像这样:

在这里插入图片描述
从上图中我们能得到如下信息:

第一个组最开始的3个页面的类型是固定的,也就是说extent 0这个区最开始的3个页面的类型是固定的,分别是:

  • FSP_HDR类型:这个类型的页面是用来登记整个表空间的一些整体属性以及本组所有的区,也就是extent 0 ~ extent 255这256个区的属性,稍后详细唠叨。需要注意的一点是,整个表空间只有一个FSP_HDR类型的页面。
  • IBUF_BITMAP类型:这个类型的页面是存储本组所有的区的所有页面关于INSERT BUFFER的信息。当然,你现在不用知道啥是个INSERT BUFFER,后边会详细说到你吐。
  • INODE类型:这个类型的页面存储了许多称为INODE的数据结构,还是那句话,现在你不需要知道啥是个INODE,后边儿会说到你吐。

其余各组最开始的2个页面的类型是固定的,也就是说extent 256、extent 512这些区最开始的2个页面的类型是固定的,分别是:

  • XDES类型:全称是extent descriptor,用来登记本组256个区的属性,也就是说对于在extent 256区中的该类型页面存储的就是extent 256 ~ extent 511这些区的属性,对于在extent 512区中的该类型页面存储的就是extent 512 ~ extent 767这些区的属性。上边介绍的FSP_HDR类型的页面其实和XDES类型的页面的作用类似,只不过FSP_HDR类型的页面还会额外存储一些表空间的属性。
  • IBUF_BITMAP类型:上边介绍过了。

好了,宏观的结构介绍完了,里边儿的名词大家也不用记清楚,只要大致记得:表空间被划分为许多连续的区,每个区默认由64个页组成,每256个区划分为一组,每个组的最开始的几个页面类型是固定的就好了。


段(segment)的概念

为啥好端端的提出一个区(extent)的概念呢?我们以前分析问题的套路都是这样的:表中的记录存储到页里边儿,然后页作为节点组成B+树,这个B+树就是索引,然后吧啦吧啦一堆聚簇索引和二级索引的区别。这套路也没啥不妥的呀~

是的,如果我们表中数据量很少的话,比如说你的表中只有几十条、几百条数据的话,的确用不到区的概念,因为简单的几个页就能把对应的数据存储起来,但是你架不住表里的记录越来越多呀。

B+树的每一层中的页都会形成一个双向链表呀,File Header中的FIL_PAGE_PREV和FIL_PAGE_NEXT字段不就是为了形成双向链表设置的么?

从理论上说,不引入区的概念只使用页的概念对存储引擎的运行并没啥影响,但是我们来考虑一下下边这个场景:

  • 我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据。而B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。我们介绍B+树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机I/O。再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O。

尽可能让每一层中用双向链表串联起来的页,在物理位置上也是相邻的。

所以,所以,所以才引入了区(extent)的概念,一个区就是在物理位置上连续的64个页。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配,甚至在表中的数据十分非常特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足填充满整个区),但是从性能角度看,可以消除很多的随机I/O,功大于过嘛!

事情到这里就结束了么?太天真了,我们提到的范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以设计InnoDB的大叔们对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。

默认情况下一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M存储空间,所以默认情况下一个只存了几条记录的小表也需要2M的存储空间么?以后每次添加一个索引都要多申请2M的存储空间么?这对于存储记录比较少的表简直是天大的浪费。设计InnoDB的大叔们都挺节俭的,当然也考虑到了这种情况。这个问题的症结在于到现在为止我们介绍的区都是非常纯粹的,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用。现在为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,设计InnoDB的大叔们提出了一个碎片(fragment)区的概念,也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。所以此后为某个段分配存储空间的策略是这样的:

  • 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
  • 当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间。

当某个段占用的存储空间小于2/1个区大小的时候,就默认会在碎片区分配空间进行存储,如果大于2/1个区的话,就会以完整的区为单位进行分配。

所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面以及一些完整的区的集合。除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段,当然我们现在并不关心别的类型的段,现在只需要知道段是一些零散的页面以及一些完整的区的集合就好了。

碎片区是为了节省空间和访问效率的折中方案


区的分类

通过上边一通唠叨,大家知道了表空间的是由若干个区组成的,这些区大体上可以分为4种类型:

  • 空闲的区:现在还没有用到这个区中的任何页面。
  • 有剩余空间的碎片区:表示碎片区中还有可用的页面。
  • 没有剩余空间的碎片区:表示碎片区中的所有页面都被使用,没有空闲页面。
  • 附属于某个段的区。每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。

这4种类型的区也可以被称为区的4种状态(State),设计InnoDB的大叔们为这4种状态的区定义了特定的名词儿:

在这里插入图片描述
需要再次强调一遍的是,处于FREE、FREE_FRAG以及FULL_FRAG这三种状态的区都是独立的,算是直属于表空间;而处于FSEG状态的区是附属于某个段的。

碎片区内存放的都是一些占用存储空间小于2/1区大小的段,因此碎片区内可能会与多个段关联,因此碎片区是属于表空间的,而非被某个段完全占用。

为了方便管理这些区,设计InnoDB的大叔设计了一个称为XDES Entry的结构(全称就是Extent Descriptor Entry),每一个区都对应着一个XDES Entry结构,这个结构记录了对应的区的一些属性。我们先看图来对这个结构有个大致的了解:

在这里插入图片描述
从图中我们可以看出,XDES Entry是一个40个字节的结构,大致分为4个部分,各个部分的释义如下:

  • Segment ID(8字节)

    每一个段都有一个唯一的编号,用ID表示,此处的Segment ID字段表示就是该区所在的段。当然前提是该区已经被分配给某个段了,不然的话该字段的值没啥意义。

  • List Node(12字节)

    这个部分可以将若干个XDES Entry结构串联成一个链表,大家看一下这个List Node的结构:

在这里插入图片描述

索引树上每一层的页都是通过双向链表串联起来的,一个表空间下可能会存在多个区,同样也可以采用双向链表串联。

  • 如果我们想定位表空间内的某一个位置的话,只需指定页号以及该位置在指定页号中的页内偏移量即可。所以:

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

    把一些XDES Entry结构连成一个链表有啥用?稍安勿躁,我们稍后唠叨XDES Entry结构组成的链表问题。

  • State(4字节)

    这个字段表明区的状态。可选的值就是我们前边说过的那4个,分别是:FREEFREE_FRAGFULL_FRAGFSEG。具体释义就不多唠叨了,前边说的够仔细了。

  • Page State Bitmap(16字节)

    这个部分共占用16个字节,也就是128个比特位。我们说一个区默认有64个页,这128个比特位被划分为64个部分,每个部分2个比特位,对应区中的一个页。比如Page State Bitmap部分的第1和第2个比特位对应着区中的第1个页面,第3和第4个比特位对应着区中的第2个页面,依此类推,Page State Bitmap部分的第127和128个比特位对应着区中的第64个页面。这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。

bitmap可以看做是页位图,用来记录当前区内页面的使用状态,这一点和操作系统文件系统中块位图和Inode位图思想一致。


整理

上面吧啦吧啦了那么多,继续下面内容之前,我们先来把思路整理一下:

Innodb提供的聚簇索引叶子层存放了完整的用户记录数据,叶子层每一个节点都对应一个数据页,叶子层数据页通过双向链表串联了起来。

在进行范围查询的时候,需要先定位好最大和最小范围:

在这里插入图片描述
然后,需要扫描这个范围内所有的数据页,那么如果这些数据页在物理内存上的离散程度很大,那么就意味着会产生很多的随机IO。

一会需要移动磁臂到磁道1,下一个数据页又要移动磁臂到磁道五,这样的开销是非常大滴老兄!磁盘读取相关知识

因此,如果我们能够尽可能让同一层上数据页在物理位置上尽可能相邻,那么就可以减少随机IO带来的性能损耗,

操作系统层面也有对应的处理,就是让相邻的扇区尽量排列在同一个磁道上,从而减少寻道带来的性能损耗。

如何让这些数据页尽可能相邻呢?

  • 找一块固定大小空间专门用来存放当前索引树,每当当前索引树需要分配一块新的数据页时,就从这块空间申请一个空闲内存页,这样就能确保分配的数据页之间在物理空间上是相邻的,这个固定大小的空间就称为区,一个区掌管着64个空闲页。

索引树分为目录层和叶子层,如果让目录层和叶子层都挤在一个区中存放,那么查询的时候就会不太方便,因此采用叶子层和目录层分离的方式来进行存储,叶子层单独分配一个区进行存储,而目录层也单独分配一个区进行存放。

由于叶子层和目录层可能需要不只一个区的大小来存储数据,因此叶子层的区集合称为叶子节点段,目录层的区集合称为目录段。

最后,为了优化给一个小表的叶子段和目录段直接分配一个完整区带来的空间浪费问题,于是引入了碎片区概念。

操作系统对于内存的管理也是按照页进行的,引入页的原因很大程度也是为了减少内存碎片,虽然也存在一个页只存放很少数据,从而造成当前页大部分存储空间浪费的现象,但是并没有引入碎片页的概念,可能是因为这样做会增加内存管理模块的复杂度,而Innodb由于也是以页作为最小分配单位,所以也存在上述问题,但是浪费一页空间还可以忍受,而浪费一个区的空间就难以忍受了,毕竟有64个页的大小,因此才有了碎片区的出现。


XDES Entry链表

经过上面的讲述,相信各位已经明白了区和段存在的意义是什么,以及为什么要有碎片区的存在,那么我们这里再回到最初的起点,捋一捋向某个段中插入数据的过程:

  • 当段中数据较少的时候,首先会查看表空间中是否有状态为FREE_FRAG的区,也就是找还有空闲空间的碎片区,如果找到了,那么从该区中取一些零散的页把数据插进去;否则到表空间下申请一个状态为FREE的区,也就是空闲的区,把该区的状态变为FREE_FRAG,然后从该新申请的区中取一些零散的页把数据插进去。之后不同的段使用零散页的时候都会从该区中取,直到该区中没有空闲空间,然后该区的状态就变成了FULL_FRAG。

现在的问题是你怎么知道表空间里的哪些区是FREE的,哪些区的状态是FREE_FRAG的,哪些区是FULL_FRAG的?要知道表空间的大小是可以不断增大的,当增长到GB级别的时候,区的数量也就上千了,我们总不能每次都遍历这些区对应的XDES Entry结构吧?这时候就是XDES Entry中的List Node部分发挥奇效的时候了,我们可以通过List Node中的指针,做这么三件事:

  • 把状态为FREE的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FREE链表。
  • 把状态为FREE_FRAG的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FREE_FRAG链表。
  • 把状态为FULL_FRAG的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FULL_FRAG链表。
    在这里插入图片描述

这样每当我们想找一个FREE_FRAG状态的区时,就直接把FREE_FRAG链表的头节点拿出来,从这个节点中取一些零散的页来插入数据,当这个节点对应的区用完时,就修改一下这个节点的State字段的值,然后从FREE_FRAG链表中移到FULL_FRAG链表中。同理,如果FREE_FRAG链表中一个节点都没有,那么就直接从FREE链表中取一个节点移动到FREE_FRAG链表的状态,并修改该节点的STATE字段值为FREE_FRAG,然后从这个节点对应的区中获取零散的页就好了。

这里的FREE区和碎片区相关的FREE_FRAG区和FULL_FRAG区都直属于表空间管理,因此上述三个链表与具体的段无关。


  • 当段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了。

我们知道一个段是由多个区组成的,那么某个段下的区也可以分为三种状态,如下图所示:
在这里插入图片描述
现在,我们眼前面临的问题有如下几个:

  • 如何快速区分出哪些区属于哪个段呢?
  • 如何管理某个段下三种不同状态区组成的集合呢?

区的状态如果为FSEG,那么表明当前区是属于某个段下的,并且如果某个区的状态为FSEG,那么我们就可以根据提供当前区信息的XDES Entry中获取到当前区从属的段ID。

为了快速得到属于当前段的区有哪些,我们可以根据段号来建立链表,如果直接粗暴的将所有属于当前段的区串联为一个单链表,那么就无法很好的区分出上面给出某个段下三种区状态集合。

因此仿照表空间对区的分类管理,我们也可以对每个段建立如下三种状态的链表集合:

  • FREE链表:同一个段中,所有页面都是空闲的区对应的XDES Entry结构会被加入到这个链表。注意和直属于表空间的FREE链表区别开了,此处的FREE链表是附属于某个段的。
  • NOT_FULL链表:同一个段中,仍有空闲空间的区对应的XDES Entry结构会被加入到这个链表。
  • FULL链表:同一个段中,已经没有空闲空间的区对应的XDES Entry结构会被加入到这个链表。

在这里插入图片描述
再次强调一遍,每一个索引都对应两个段,每个段都会维护上述的3个链表,比如下边这个表:

CREATE TABLE t (
    c1 INT NOT NULL AUTO_INCREMENT,
    c2 VARCHAR(100),
    c3 VARCHAR(100),
    PRIMARY KEY (c1),
    KEY idx_c2 (c2)
)ENGINE=InnoDB;

这个表t共有两个索引,一个聚簇索引,一个二级索引idx_c2,所以这个表共有4个段,每个段都会维护上述3个链表,总共是12个链表,加上我们上边说过的直属于表空间的3个链表,整个独立表空间共需要维护15个链表。所以段在数据量比较大时插入数据的话,会先获取NOT_FULL链表的头节点,直接把数据插入这个头节点对应的区中即可,如果该区的空间已经被用完,就把该节点移到FULL链表中。


链表基节点

上边光是介绍了一堆链表,可我们怎么找到这些链表呢,或者说怎么找到某个链表的头节点或者尾节点在表空间中的位置呢?设计InnoDB的大叔当然考虑了这个问题,他们设计了一个叫List Base Node的结构,翻译成中文就是链表的基节点。这个结构中包含了链表的头节点和尾节点的指针以及这个链表中包含了多少节点的信息,我们画图看一下这个结构的示意图:

在这里插入图片描述
我们上边介绍的每个链表都对应这么一个List Base Node结构,其中:

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

一般我们把某个链表对应的List Base Node结构放置在表空间中固定的位置,这样想找定位某个链表就变得so easy啦。


链表小结

综上所述,表空间是由若干个区组成的,每个区都对应一个XDES Entry的结构,直属于表空间的区对应的XDES Entry结构可以分成FREEFREE_FRAGFULL_FRAG这3个链表;每个段可以附属若干个区,每个段中的区对应的XDES Entry结构可以分成FREENOT_FULLFULL这3个链表。每个链表都对应一个List Base Node的结构,这个结构里记录了链表的头、尾节点的位置以及该链表中包含的节点数。正是因为这些链表的存在,管理这些区才变成了一件so easy的事情。


段的结构

我们前边说过,段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。像每个区都有对应的XDES Entry来记录这个区中的属性一样,设计InnoDB的大叔为每个段都定义了一个INODE Entry结构来记录一下段中的属性。大家看一下示意图:

在这里插入图片描述

它的各个部分释义如下:

  • Segment ID

    就是指这个INODE Entry结构对应的段的编号(ID)。

  • NOT_FULL_N_USED

    这个字段指的是在NOT_FULL链表中已经使用了多少个页面。

  • 3个List Base Node

    分别为段的FREE链表、NOT_FULL链表、FULL链表定义了List Base Node,这样我们想查找某个段的某个链表的头节点和尾节点的时候,就可以直接到这个部分找到对应链表的List Base Node。so easy!

  • Magic Number

    这个值是用来标记这个INODE Entry是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。如果这个数字是值的97937874,表明该INODE Entry已经初始化,否则没有被初始化。(不用纠结这个值有啥特殊含义,人家规定的)。

  • Fragment Array Entry

    我们前边强调过无数次段是一些零散页面和一些完整的区的集合,每个Fragment Array Entry结构都对应着一个零散的页面,这个结构一共4个字节,表示一个零散页面的页号。

结合着这个INODE Entry结构,大家可能对段是一些零散页面和一些完整的区的集合的理解再次深刻一些。

INODE Entry负责整合当前段拥有的资源和状态信息记录,那么大家思考一下: 一个独立表空间可能会存在多个段,每个段都对应一个INODE Entry,那么表空间按照道理也应该套娃式提供一个地方来存放这些INODE Entry资源信息和其他资源信息,以及自身状态信息记录等,mysql有没有这样做呢? —> 接着往下看就晓得了


各类型页面详细情况

到现在为止我们已经大概清楚了表空间、段、区、XDES Entry、INODE Entry、各种以XDES Entry为节点的链表的基本概念了,可是总有一种飞在天上不踏实的感觉,每个区对应的XDES Entry结构到底存储在表空间的什么地方?直属于表空间的FREE、FREE_FRAG、FULL_FRAG链表的基节点到底存储在表空间的什么地方?每个段对应的INODE Entry结构到底存在表空间的什么地方?我们前边介绍了每256个连续的区算是一个组,想解决刚才提出来的这些个疑问还得从每个组开头的一些类型相同的页面说起,接下来我们一个页面一个页面的分析,真相马上就要浮出水面了。


FSP_HDR类型

在这里插入图片描述

首先看第一个组的第一个页面,当然也是表空间的第一个页面,页号为0。这个页面的类型是FSP_HDR,它存储了表空间的一些整体属性以及第一个组内256个区的对应的XDES Entry结构,直接看这个类型的页面的示意图:

在这里插入图片描述
从图中可以看出,一个完整的FSP_HDR类型的页面大致由5个部分组成,各个部分的具体释义如下表:

在这里插入图片描述
File Header和File Trailer就不再强调了,另外的几个部分中,Empty Space是尚未使用的空间,我们不用管它,重点来看看File Space Header和XDES Entry这两个部分。


File Space Header部分

从名字就可以看出来,这个部分是用来存储表空间的一些整体属性的,废话少说,看图:

在这里插入图片描述
哇唔,字段有点儿多哦,不急一个一个慢慢看。下面是各个属性的简单描述:

在这里插入图片描述
在这里插入图片描述
这里头的Space ID、Not Used、Size这三个字段大家肯定一看就懂,其他的字段我们再详细瞅瞅,为了大家的阅读体验,我就不严格按照实际的字段顺序来解释各个字段了哈。

  • List Base Node for FREE List、List Base Node for FREE_FRAG List、List Base Node for FULL_FRAG List。

这三个大家看着太亲切了,分别是直属于表空间的FREE链表的基节点、FREE_FRAG链表的基节点、FULL_FRAG链表的基节点,这三个链表的基节点在表空间的位置是固定的,就是在表空间的第一个页面(也就是FSP_HDR类型的页面)的File Space Header部分。所以之后定位这几个链表就so easy啦。

在这里插入图片描述

  • FRAG_N_USED

这个字段表明在FREE_FRAG链表中已经使用的页面数量。

  • FREE Limit

我们知道表空间都对应着具体的磁盘文件,一开始我们创建表空间的时候对应的磁盘文件中都没有数据,所以我们需要对表空间完成一个初始化操作,包括为表空间中的区建立XDES Entry结构,为各个段建立INODE Entry结构,建立各种链表吧啦吧啦的各种操作。我们可以一开始就为表空间申请一个特别大的空间,但是实际上有绝大部分的区是空闲的,我们可以选择把所有的这些空闲区对应的XDES Entry结构加入FREE链表,也可以选择只把一部分的空闲区加入FREE链表,等啥时候空闲链表中的XDES Entry结构对应的区不够使了,再把之前没有加入FREE链表的空闲区对应的XDES Entry结构加入FREE链表,中心思想懒加载,设计InnoDB的大叔采用的就是后者,他们为表空间定义了FREE Limit这个字段,在该字段表示的页号之前的区都被初始化了,之后的区尚未被初始化。

  • Next Unused Segment ID

表中每个索引都对应2个段,每个段都有一个唯一的ID,那当我们为某个表新创建一个索引的时候,就意味着要创建两个新的段。那怎么为这个新创建的段找一个唯一的ID呢?去遍历现在表空间中所有的段么?我们说过,遍历是不可能遍历的,这辈子都不可能遍历,所以设计InnoDB的大叔们提出了这个名叫Next Unused Segment ID的字段,该字段表明当前表空间中最大的段ID的下一个ID,这样在创建新段的时候赋予新段一个唯一的ID值就so easy啦,直接使用这个字段的值就好了。

  • Space Flags

表空间对于一些布尔类型的属性,或者只需要寥寥几个比特位搞定的属性都放在了这个Space Flags中存储,虽然它只有4个字节,32个比特位大小,却存储了好多表空间的属性,详细情况如下表:

在这里插入图片描述

  • List Base Node for SEG_INODES_FULL List和List Base Node for SEG_INODES_FREE List

每个段对应的INODE Entry结构会集中存放到一个类型为INODE的页中,如果表空间中的段特别多,则会有多个INODE Entry结构,可能一个页放不下,这些INODE类型的页会组成两种列表:

  • SEG_INODES_FULL链表,该链表中的INODE类型的页面都已经被INODE Entry结构填充满了,没空闲空间存放额外的INODE Entry了。
  • SEG_INODES_FREE链表,该链表中的INODE类型的页面仍有空闲空间来存放INODE Entry结构。

由于我们现在还没有详细唠叨INODE类型页,所以等会说过INODE类型的页之后再回过头来看着两个链表。

Mysql大叔值得借鉴的做法: 为了区分元素的状态,而选择采用不同的链表存放不同状态的元素,并在进行状态转换时,将元素移动到对应的链表中存放。


XDES Entry部分

紧接着File Space Header部分的就是XDES Entry部分了,我们嘴上唠叨过无数次,却从没见过真身的XDES Entry就是在表空间的第一个页面中保存的。我们知道一个XDES Entry结构的大小是40字节,但是一个页面的大小有限,只能存放有限个XDES Entry结构,所以我们才把256个区划分成一组,在每组的第一个页面中存放256个XDES Entry结构。大家回看那个FSP_HDR类型页面的示意图,XDES Entry 0就对应着extent 0XDES Entry 1就对应着extent 1… 依此类推,XDES Entry255就对应着extent 255

因为每个区对应的XDES Entry结构的地址是固定的,所以我们访问这些结构就so easy啦,至于该结构的详细使用情况我们已经唠叨的够明白了,在这就不赘述了。


XDES类型

我们说过,每一个XDES Entry结构对应表空间的一个区,虽然一个XDES Entry结构只占用40字节,但你抵不住表空间的区的数量也多啊。在区的数量非常多时,一个单独的页可能就不够存放足够多的XDES Entry结构,所以我们把表空间的区分为了若干个组,每组开头的一个页面记录着本组内所有的区对应的XDES Entry结构。由于第一个组的第一个页面有些特殊,因为它也是整个表空间的第一个页面,所以除了记录本组中的所有区对应的XDES Entry结构以外,还记录着表空间的一些整体属性,这个页面的类型就是我们刚刚说完的FSP_HDR类型,整个表空间里只有一个这个类型的页面。除去第一个分组以外,之后的每个分组的第一个页面只需要记录本组内所有的区对应的XDES Entry结构即可,不需要再记录表空间的属性了,为了和FSP_HDR类型做区别,我们把之后每个分组的第一个页面的类型定义为XDES,它的结构和FSP_HDR类型是非常相似的:

在这里插入图片描述
FSP_HDR类型的页面对比,除了少了File Space Header部分之外,也就是除了少了记录表空间整体属性的部分之外,其余的部分是一样一样的。由于我们上边唠叨的已经够仔细了,对于XDES类型的页面也就不重复唠叨了哈。


IBUF_BITMAP类型

对比前边介绍表空间的图,每个分组的第二个页面的类型都是IBUF_BITMAP,这种类型的页里边记录了一些有关Change Buffer的东东,由于这个Change Buffer里又包含了贼多的概念,考虑到大家在一章中接受这么多新概念有点呼吸不适,怕大家心脏病犯了所以就把Change Buffer的相关知识放到后边的章节中,大家稍安勿躁哈。


INODE类型

再次对比前边介绍表空间的图,第一个分组的第三个页面的类型是INODE。我们前边说过设计InnoDB的大叔为每个索引定义了两个段,而且为某些特殊功能定义了些特殊的段。为了方便管理,他们又为每个段设计了一个INODE Entry结构,这个结构中记录了关于这个段的相关属性。而我们这会儿要介绍的这个INODE类型的页就是为了存储INODE Entry结构而存在的。好了,废话少说,直接看图:

在这里插入图片描述
从图中可以看出,一个INODE类型的页面是由这几部分构成的:

在这里插入图片描述
除了File HeaderEmpty SpaceFile Trailer这几个老朋友外,我们重点关注List Node for INODE Page ListINODE Entry这两个部分。

首先看INODE Entry部分,我们前边已经详细介绍过这个结构的组成了,主要包括对应的段内零散页面的地址以及附属于该段的FREENOT_FULLFULL链表的基节点。每个INODE Entry结构占用192字节,一个页面里可以存储85个这样的结构。

重点看一下List Node for INODE Page List这个玩意儿,因为一个表空间中可能存在超过85个段,所以可能一个INODE类型的页面不足以存储所有的段对应的INODE Entry结构,所以就需要额外的INODE类型的页面来存储这些结构。还是为了方便管理这些INODE类型的页面,设计InnoDB的大叔们将这些INODE类型的页面串联成两个不同的链表:

  • SEG_INODES_FULL链表:该链表中的INODE类型的页面中已经没有空闲空间来存储额外的INODE Entry结构了。
  • SEG_INODES_FREE链表:该链表中的INODE类型的页面中还有空闲空间来存储额外的INODE Entry结构了。

想必大家已经认出这两个链表了,我们前边提到过这两个链表的基节点就存储在File Space Header里边,也就是说这两个链表的基节点的位置是固定的,所以我们可以很轻松的访问到这两个链表。以后每当我们新创建一个段(创建索引时就会创建段)时,都会创建一个INODE Entry结构与之对应,存储INODE Entry的大致过程就是这样的:

  • 先看看SEG_INODES_FREE链表是否为空,如果不为空,直接从该链表中获取一个节点,也就相当于获取到一个仍有空闲空间的INODE类型的页面,然后把该INODE Entry结构放到该页面中。当该页面中无剩余空间时,就把该页放到SEG_INODES_FULL链表中。
  • 如果SEG_INODES_FREE链表为空,则需要从表空间的FREE_FRAG链表中申请一个页面,修改该页面的类型为INODE,把该页面放到SEG_INODES_FREE链表中,与此同时把该INODE Entry结构放入该页面。

Segment Header 结构的运用

我们知道一个索引会产生两个段,分别是叶子节点段和非叶子节点段,而每个段都会对应一个INODE Entry结构,那我们怎么知道某个段对应哪个INODE Entry结构呢?所以得找个地方记下来这个对应关系。希望你还记得我们在唠叨数据页,也就是INDEX类型的页时有一个Page Header部分,当然我不能指望你记住,所以把Page Header部分再抄一遍给你看:

Page Header部分(为突出重点,省略了好多属性)

在这里插入图片描述
其中的PAGE_BTR_SEG_LEAFPAGE_BTR_SEG_TOP都占用10个字节,它们其实对应一个叫Segment Header的结构,该结构图示如下:
在这里插入图片描述

各个部分的具体释义如下:

在这里插入图片描述
这样子就很清晰了,PAGE_BTR_SEG_LEAF记录着叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量,PAGE_BTR_SEG_TOP记录着非叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量。这样子索引和其对应的段的关系就建立起来了。不过需要注意的一点是,因为一个索引只对应两个段,所以只需要在索引的根页面中记录这两个结构即可。


真实表空间对应的文件大小

等会儿等会儿,上边的这些概念已经压的快喘不过气了。不过独立表空间有那么大么?我到数据目录里看了,一个新建的表对应的.ibd文件只占用了96K,才6个页面大小,上边的内容该不是扯犊子吧?

哈,一开始表空间占用的空间自然是很小,因为表里边都没有数据嘛!不过别忘了这些.ibd文件是自扩展的,随着表中数据的增多,表空间对应的文件也逐渐增大。


小结

到此,关于表空间前半部分独立表空间的内容基本就叙述完了,相信各位在第一次看的时候已经被绕晕了,那么就跟随我的步伐,来重头简单梳理一下流程吧:

  1. 索引树由目录层(有多层)和叶子层(一层)组成,每一层的数据页内部和用双向链表串联起来的数据页之间都是按照索引列进行有序排列。
  2. 当我们需要遍历用双向链表串联起来的数据页时,如果链表上相邻数据页在物理位置上不相邻,那么会产生大量随机IO,从而在范围查询这种场景下导致极低的遍历效率。
  3. 为了尽量让同一层用双向链表串联起来的数据页不仅在逻辑上相邻,并且物理页上也相邻,那么我们可以划分出一块物理区域单独用于分配给当前层使用,这块固定区域被称作区,一个区由64个页大小组成。
  4. 索引树分为目录层和叶子层,如果让目录层和叶子层都挤在一个区中存放,那么查询的时候就会不太方便,因此采用叶子层和目录层分离的方式来进行存储,叶子层单独分配一个区进行存储,而目录层也单独分配一个区进行存放。
  5. 由于叶子层和目录层可能需要不只一个区的大小来存储数据,因此叶子层的区集合称为叶子节点段,目录层的区集合称为目录段。
  6. 最后,为了优化给一个小表的叶子段和目录段直接分配一个完整区带来的空间浪费问题,于是引入了碎片区。
  7. 为了管理好处于不同状态的区集合,表空间采用了三条链表,每一条链表上单独存放一种状态的区集合
  8. 同样段为了管理了处于不同状态的区集合,也采用了三条链表,每一条链表上单独存放一种状态的区集合(这里不清楚还是需要回看上面)。
  9. 表空间下会存在多个段,表空间为了管理这些段,给每个段都弄了个INODE Entry记录每个段的信息,INODE Entry中保存了当前段管理的三条链表的基节点和零散页面集合。
  10. 由于表空间下可能会存在很多区,因此采用每256个区为一组的方式进行区划分管理,而在第一组的第一个页面中记录了当前表空间的一些信息和当前组256个区的信息。
  11. 其他组的第一个页面中主要负责保存的就只有当前组的256个区的信息。
  12. 第一组的第三个页面是INODE类型的页面,主要负责保存当前表空间下所有INODE信息,一个INODE记录的就是一个段的信息。
  13. 每个索引树上的根页面都记录着叶子节点段和非叶子节点段对应的INODE是哪一个,即建立好段和INODE的映射关系。

下一篇文章我们将来讲述表空间的下半部分: 系统表空间。

本篇文章整理至从根上理解Mysql,对其中内容作出了些许补充

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
├─新版MySQL DBA 课件ppt │ 第一课数据库介绍篇.pdf │ 第七课MySQL数据库设计.pdf │ 第三十一课percona-toolkits 的实战及自动化.pdf │ 第三课MySQL授权认证.pdf │ 第九课MySQL字符集.pdf │ 第二十一课MySQL常见错误-converted.pdf │ 第二十课MySQL索引和调优.pdf │ 第二课MySQL入门介绍.pdf │ 第五课MySQL常用函数介绍.pdf │ 第八课InnoDB内核.pdf │ 第六课SQL高级应用.pdf │ 第十一课MySQL表分区8.0.pdf │ 第十七课Elasticsearch分享-张亚V4.pdf │ 第十三课MySQL5.7高可用架构之Mycat.pdf │ 第十三课MySQL8.0高可用架构之Mycat.pdf │ 第十九课MySQL备份和恢复.pdf │ 第十二课MySQL5.7复制.pdf │ 第十二课MySQL8.0复制.pdf │ 第十五课MySQL8.0高可用架构之MHA和MMM.pdf │ 第十五课MySQL高可用架构之MHA和MMM.pdf │ 第十八课mongo分享-张亚V1.pdf │ 第十六课Redis分享-张亚V2.pdf │ 第十四课MySQL8.0高可用架构之Atlas.pdf │ 第十课MySQL8.0锁机制和事务.pdf │ 第十课MySQL锁机制和事务.pdf │ 第四课SQL基础语法.pdf │ ├─新版MySQL DBA综合实战班 第01天 │ 0_MySQL高级DBA公开课视频.avi │ 1_数据库通用知识介绍.avi │ 2_MySQL8常规安装.avi │ 3_MySQL8非常规安装.avi │ 4_MySQL8常见客户端和启动相关参数.avi │ ├─新版MySQL DBA综合实战班 第02天 │ 10_MySQL Update课堂练习.mp4 │ 1_课后作业讲解.mp4 │ 2_MySQL权限系统介绍.mp4 │ 3_MySQL授权用户和权限回收.mp4 │ 4_MySQL8新的密码认证方式和客户端链接.mp4 │ 5_MySQL Create命令.mp4 │ 6_MySQL CreateTable命令.mp4 │ 7_课堂练习1.mp4 │ 8_MySQL Insert命令.mp4 │ 9_MySQL Insert课堂练习和Update命令.mp4 │ ├─新版MySQL DBA综合实战班 第03天 │ 1_课堂作业讲解.mp4 │ 2_MySQL Delete语法讲解.mp4 │ 3_MySQL Select语法讲解.mp4 │ 4_MySQL Select多表连接讲解.mp4 │ 5_MySQL其他常用命令讲解.mp4 │ 6_MySQL操作符和常用函数.mp4 │ 7_MySQL常用字符串和日期函数.mp4 │ delete.txt │ MySQL高级DBA大作业1.docx │ 作业.docx │ ├─新版MySQL DBA综合实战班 第04天 │ 1_课后作业讲解.mp4 │ 2_SQL课堂强化练习1.mp4 │ 3_SQL课堂强化练习2.mp4 │ 4_存储过程函数概念和创建讲解.mp4 │ 5_存储过程函数流程控制语句讲解.mp4 │ ├─新版MySQL DBA综合实战班 第05天 │ 1_课后作业讲解.mp4 │ 2_MySQL游标讲解.mp4 │ 3_MySQL触发器.mp4 │ 4_MySQL触发器课堂强化练习.mp4 │ 5_MySQL数字和时间类型.mp4 │ 6_MySQL字符串类型.mp4 │ 7_MySQL存储引擎.mp4 │ 8_MySQL第三范式设计讲解.mp4 │ 9_MySQL数据库设计工具.mp4 │ ├─新版MySQL DBA综合实战班 第06天 │ 1_课堂作业讲解.mp4 │ 2_InnoDB内核之事务和多版本控制.mp4 │ 3_InnoDB底层文件存储和体系结构.mp4 │ 4_InnoDB体系结构.mp4 │ 5_InnoDB存储引擎配置.mp4 │ 6_InnoDB统计资料和其他配置.mp4 │ 7_InnoDB锁原理和锁等待问题定位.mp4 │ ├─新版MySQL DBA综合实战班 第07天 │ 1_课后作业讲解.mp4 │ 2_MySQL锁机制原理讲解.mp4 │ 3_MySQL锁相关参数设置.mp4 │ 4_InnoDB事务隔离级别详解.mp4 │ 5_InnoDB死锁发生原理和规避.mp4 │ 6_MySQL字符集和排序规则.mp4 │ 作业.docx │ 锁等待分析.txt │ ├─新版MySQL DBA综合实战班 第08天 │ 1_课堂作业讲解.mp4 │ 2_MySQL乱码原理讲解.mp4 │ 3_MySQL排序规则权重.mp4 │ 4_MySQL字符集空间消耗.mp4 │ 5_MySQL表分区介绍和优势.mp4 │ 6_MySQL表分区类型.mp4 │ 7_MySQL字表分区和NULL值特殊处理.mp4 │ 8_MySQL表分区管理.mp4 │ 作业.docx │ 作业及答案.docx │ ├─新版MySQL DBA综合实战班 第09天 │ 1_课堂作业讲解.mp4 │ 2_MySQL复制原理.mp4 │ 3_MySQL传统复制原理和搭建.mp4 │ 4_MySQL复制搭建part2.mp4 │ 5_MySQL复制相关参数.mp4 │ 6_MySQL复制状态和延迟复制.mp4 │ 7_MySQL半同步复制.mp4 │ 作业.docx │ ├─新版MySQL DBA综合实战班 第10天 │ │ 1_课后作业讲解.mp4 │ │ 2_MySQL传统复制手动切换和GTID复制原理及切换.mp4 │ │ 3_Mycat原理和schema配置讲解.mp4 │ │ 4_Mycat schema配置讲解.mp4 │ │ 5_Mycat企业高可用配置.mp4 │ │ 作业.docx │ │ │ └─MySQL DBA 课堂命令-复制和Mycat │ mysql-master.log │ mysql-master2.log │ mysql-mycat.log │ mysql-slave1.log │ mysql-slave2.log │ ├─新版MySQL DBA综合实战班 第11天 │ │ 1_课后作业讲解.mp4 │ │ 2_MyCat分库分表原理和常见方法.mp4 │ │ 3_MyCat管理操作.mp4 │ │ 4_Atlas配置和读写分离实现.mp4 │ │ 5_Atlas分库分表实现.mp4 │ │ 6_MHA搭建和故障切换原理剖析.mp4 │ │ │ └─MySQL DBA_课堂命令-Mycat和Atlas和MHA │ mysql-master.log │ mysql-mycat.log │ mysql-slave1.log │ mysql-slave2.log │ ├─新版MySQL DBA综合实战班 第12天 │ 01ES介绍.docx │ 01es介绍.mp4 │ 01redis介绍.mp4 │ 02es增删改查操作命令.mp4 │ 02ES的功能适用场景以及特点介绍.docx │ 02redis应用场景.mp4 │ 03ES的核心概念.docx │ 03redis单实例安装.mp4 │ 03集群分片副本操作.mp4 │ 04es集群运维.mp4 │ 04redis数据类型操作.mp4 │ 04安装search-guard.docx │ 05redis主从和哨兵操作.mp4 │ 06reids集群创建收缩扩容.mp4 │ 07redis运维工具.mp4 │ Elasticsearch分享V2.pdf │ Elasticsearch分享V4.pdf │ ES分享试验环境.docx │ ES操作.txt │ Redis分享-张亚V2.pdf │ 日志收集.txt │ 监控和分词.txt │ 防脑裂配置.txt │ ├─新版MySQL DBA综合实战班 第13天-mongo │ 01mongo介绍.mp4 │ 02mongo安装配置优化.mp4 │ 03mongo增删改查.mp4 │ 04授权认证和索引.mp4 │ 05mongo常用工具介绍.mp4 │ 06mongo副本集升级备份恢复.mp4 │ 07ELK模板收集mongo日志.mp4 │ mongodb.jpg │ Mongodb分享-贾海娇.pdf │ mongo数据库分享-张亚V1.pdf │ monogdb.conf │ ├─新版MySQL DBA综合实战班 第14天 │ │ 1_MHA手工切换和GTID支持.mp4 │ │ 2_MMM高可用架构.mp4 │ │ 3_MySQL备份概念.mp4 │ │ 4_Mysqldump备份原理.mp4 │ │ 5_Mysqldump基于表备份.mp4 │ │ 6_MySQL全量恢复和日志增量恢复.mp4 │ │ 7_xtrabackup全量和增量备份恢复.mp4 │ │ 作业及答案.docx │ │ │ └─MySQL DBA堂命令-mha和备份恢复 │ mysql-master_05-18_10-03-09.log │ mysql-master_05-18_14-02-01.log │ mysql-mycat_05-18_10-03-02.log │ mysql-slave1_05-18_10-03-14.log │ mysql-slave2_05-18_10-03-20.log │ ├─新版MySQL DBA综合实战班 第15天 │ │ 1_课后作业讲解.mp4 │ │ 2_MySQL索引原理介绍.mp4 │ │ 3_MySQL索引类型介绍.mp4 │ │ 4_MySQL索引底层结构和执行计划.mp4 │ │ 5_MySQL索引优化原则.mp4 │ │ 6_MySQL运维常见错误part1.mp4 │ │ 7_MySQL运维常见错误part2.mp4 │ │ ERROR1040_1917970.1.pdf │ │ ERROR1062_1593526.1.pdf │ │ ERROR1205_1911871.1.pdf │ │ ERROR2002_1023190.1.pdf │ │ How_to_Reset_the_RootPassword.pdf │ │ How_to_Reset_the_RootPassword5.7.pdf │ │ PacketTooLarge.pdf │ │ │ └─MySQL DBA课堂命令-索引调优和运维常见错误 │ mysql-master-05-25_11-10-39.log
《php和mysql web开发(原书第4版)》:开发人员专业技术丛书。 目录 读者反馈 译者序 前言 作者简介 第一篇 使用PHP 第1章 PHP快速入门教程 1.1 开始之前:了解PHP 1.2 创建一个示例应用:Bob汽车零部件商店 1.2.1 创建订单表单 1.2.2 表单处理 1.3 在HTML中嵌入PHP 1.3.1 使用PHP标记 1.3.2 PHP语句 1.3.3 空格 1.3.4 注释 1.4 添加动态内容 1.4.1 调用函数 1.4.2 使用date()函数 1.5 访问表单变量 1.5.1 简短、中等以及长风格的表单变量 1.5.2 字符串的连接 1.5.3 变量和文本 1.6 理解标识符 1.7 检查变量类型 1.7.1 PHP的数据类型 1.7.2 类型强度 1.7.3 类型转换 1.7.4 可变变量 1.8 声明和使用常量 1.9 理解变量的作用域 1.10 使用操作符 1.10.1 算术操作符 1.10.2 字符串操作符 1.10.3 赋值操作符 1.10.4 比较操作符 1.10.5 逻辑操作符 1.10.6 位操作符 1.10.7 其他操作符 1.11 计算表单总金额 1.12 理解操作符的优先级和结合性: 1.13 使用可变函数 1.13.1 测试和设置变量类型 1.13.2 测试变量状态 1.13.3 变量的重解释 1.14 根据条件进行决策 1.14.1 if语句 1.14.2 代码块 1.14.3 else语句 1.14.4 elseif语句 1.14.5 switch语句 1.14.6 比较不同的条件 1.15 通过迭代实现重复动作 1.15.1 while循环 1.15.2 for和foreach循环 1.15.3 do...while循环 1.16 从控制结构或脚本中跳出 1.17 使用可替换的控制结构语法 1.18 使用declare 1.19 下一章 第2章 数据的存储与检索 2.1 保存数据以便后期使用 2.2 存储和检索Bob的订单 2.3 文件处理 2.4 打开文件 2.4.1 选择文件模式 2.4.2 使用fopen()打开文件 2.4.3 通过FTP或HTTP打开文件 2.4.4 解决打开文件时可能遇到的问题 2.5 写文件 2.5.1 fwrite()的参数 2.5.2 文件格式 2.6 关闭文件 2.7 读文件 2.7.1 以只读模式打开文件:fopen() 2.7.2 知道何时读完文件:feof() 2.7.3 每次读取一行数据:fgets()、fgetss()和fgetcsv() 2.7.4 读取整个文件:readfile()、fpassthru()和file() 2.7.5 读取一个字符:fgetc() 2.7.6 读取任意长度:fread() 2.8 使用其他有用的文件函数 2.8.1 查看文件是否存在:file_exists() 2.8.2 确定文件大小:filesize() 2.8.3 删除一个文件:unlink() 2.8.4 在文件中定位:rewind()、fseek()和ftell() 2.9 文件锁定 2.10 更好的方式:数据库管理系统 2.10.1 使用普通文件的几个问题 2.10.2 RDBMS是如何解决这些问题的 2.11 进一步学习 2.12 下一章 第3章 使用数组 3.1 什么是数组 3.2 数字索引数组 3.2.1 数字索引数组的初始化 3.2.2 访问数组的内容 3.2.3 使用循环访问数组 3.3 使用不同索引的数组 3.3.1 初始化相关数组 3.3.2 访问数组元素 3.3.3 使用循环语句 3.4 数组操作符 3.5 多维数组 3.6 数组排序 3.6.1 使用sort()函数 3.6.2 使用asort()函数和ksort()函数对相关数组排序 3.6.3 反向排序 3.7 多维数组的排序 3.7.1 用户定义排序 3.7.2 反向用户排序 3.8 对数组进行重新排序 3.8.1 使用shuffle()函数 3.8.2 使用array_reverse()函数 3.9 从文件载入数组 3.10 执行其他的数组操作 3.10.1 在数组中浏览:each()、current()、reset()、end()、next()、pos()和prev() 3.10.2 对数组的每一个元素应用任何函数:array_walk() 3.10.3 统计数组元素个数:count()、sizeof()和array_count_values() 3.10.4 将数组转换成标量变量:extract() 3.11 进一步学习 3.12 下一章 第4章 字符串操作与正则表达式 4.1 创建一个示例应用程序:智能表单邮件 4.2 字符串的格式化 4.2.1 字符串的整理:chop()、ltrim()和trim() 4.2.2 格式化字符串以便显示 4.2.3 格式化字符串以便存储:addslashes()和stripslashes() 4.3 用字符串函数连接和分割字符串 4.3.1 使用函数explode()、implode()和join() 4.3.2 使用strtok()函数 4.3.3 使用substr()函数 4.4 字符串的比较 4.4.1 字符串的排序:strcmp()、strcasecmp()和strnatcmp() 4.4.2 使用strlen()函数测试字符串的长度 4.5 使用字符串函数匹配和替换子字符串 4.5.1 在字符串中查找字符串:strstr()、strchr()、strrchr()和stristr() 4.5.2 查找子字符串的位置:strpos()、strrpos() 4.5.3 替换子字符串:str_replace()、substr_replace() 4.6 正则表达式的介绍 4.6.1 基础知识 4.6.2 字符集和类 4.6.3 重复 4.6.4 子表达式 4.6.5 子表达式计数 4.6.6 定位到字符串的开始或末尾 4.6.7 分支 4.6.8 匹配特殊字符 4.6.9 特殊字符一览 4.6.10 在智能表单中应用 4.7 用正则表达式查找子字符串 4.8 使用正则表达式分割字符串 4.9 比较字符串函数和正则表达式函数 4.10 进一步学习 4.11 下一章 第5章 代码重用与函数编写 5.1 代码重用的好处 5.1.1 成本 5.1.2 可靠性 5.1.3 一致性 5.2 使用require()和include()函数 5.2.1 文件扩展名和require()语句 5.2.2 使用require()制作Web站点的模版 5.2.3 使用auto_prepend_file和auto_append_file 5.3 在PHP中使用函数 5.3.1 调用函数 5.3.2 调用未定义的函数 5.3.3 理解字母大小写和函数名称 5.4 理解为什么要定义自己的函数 5.5 了解基本的函数结构 5.5.1 函数命名 5.6 使用参数 5.7 理解作用域 5.8 参数的引用传递和值传递 5.9 使用Return关键字 5.9.1 从函数返回一个值 5.10 实现递归 5.10.1 名称空间 5.11 进一步学习 5.12 下一章 第6章 面向对象的PHP 6.1 理解面向对象的概念 6.1.1 类和对象 6.1.2 多态性 6.1.3 继承 6.2 在PHP中创建类、属性和操作 6.2.1 类的结构 6.2.2 构造函数 6.2.3 析构函数 6.3 类的实例化 6.4 使用类的属性 6.5 使用private和public关键字控制访问 6.6 类操作的调用 6.7 在PHP中实现继承 6.7.1 通过继承使用private和protected访问修饰符控制可见性 6.7.2 重载 6.7.3 使用final关键字禁止继承和重载 6.7.4 理解多重继承 6.7.5 实现接口 6.8 类的设计 6.9 编写类代码 6.10 理解PHP面向对象新的高级功能 6.10.1 使用Per-Class常量 6.10.2 实现静态方法 6.10.3 检查类的类型和类型提示 6.10.4 克隆对象 6.10.5 使用抽象类 6.10.6 使用__call()重载方法 6.10.7 使用__autoload()方法 6.10.8 实现迭代器和迭代 6.10.9 将类转换成字符串 6.10.10 使用Reflection(反射)API 6.11 下一章 第7章 错误和 异常处理 7.1 异常处理的概念 7.2 Exception类 7.3 用户自定义异常 7.4 Bob的汽车零部件商店应用程序的异常 7.5 异常和PHP的其他错误处理机制 7.6 进一步学习 7.7 下一章 第二篇 使用MySQL 第8章 设计Web数据库 8.1 关系数据库的概念 8.1.1 表格 8.1.2 列 8.1.3 行 8.1.4 值 8.1.5 键 8.1.6 模式 8.1.7 关系 8.2 如何设计Web数据库 8.2.1 考虑要建模的实际对象 8.2.2 避免保存冗余数据 8.2.3 使用原子列值 8.2.4 选择有意义的键 8.2.5 考虑需要询问数据库的问题 8.2.6 避免多个空属性的设计 8.2.7 表格类型的总结 8.3 Web数据库架构 8.4 进一步学习 8.5 下一章 第9章 创建Web数据库 9.1 使用MySQL监视程序 9.2 登录到MySQL 9.3 创建数据库和用户 9.4 设置用户与权限 9.5 MySQL权限系统的介绍 9.5.1 最少权限原则 9.5.2 创建用户:GRANT命令 9.5.3 权限的类型和级别 9.5.4 REVOKE命令 9.5.5 使用GRANT和REVOKE的例子 9.6 创建一个Web用户 9.7 使用正确的数据库 9.8 创建数据库表 9.8.1 理解其他关键字的意思 9.8.2 理解列的类型 9.8.3 用SHOW和DESCRIBE来查看数据库 9.8.4 创建索引 9.9 理解MySQL的标识符 9.10 选择列数据类型 9.10.1 数字类型 9.10.2 日期和时间类型 9.10.3 字符串类型 9.11 进一步学习 9.12 下一章 第10章 使用MySQL数据库 10.1 SQL是什么 10.2 在数据库中插入数据 10.3 从数据库中获取数据 10.3.1 获取满足特定条件的数据 10.3.2 从多个表中获取数据 10.3.3 以特定的顺序获取数据 10.3.4 分组与合计数据 10.3.5 选择要返回的行 10.3.6 使用子查询 10.4 更新数据库记录 10.5 创建后修改表 10.6 删除数据库中的记录 10.7 表的删除 10.8 删除整个数据库 10.9 进一步学习 10.10 下一章 第11章 使用PHP从Web访问MySQL数据库 11.1 Web数据库架构的工作原理 11.2 从Web查询数据库的基本步骤 11.2.1 检查与过滤用户输入数据 11.2.2 建立一个连接 11.2.3 选择使用的数据库 11.2.4 查询数据库 11.2.5 检索查询结果 11.2.6 从数据库断开连接 11.3 将新信息放入数据库 11.4 使用Prepared语句 11.5 使用PHP与数据库交互的其他接口 11.5.1 使用常规的数据库接口:PEAR MDB2 11.6 进一步学习 11.7 下一章 第12章 MySQL高级管理 12.1 深入理解权限系统 12.1.1 user表 12.1.2 db表和host表 12.1.3 tables_priv表,columns_priv表和procs_priv表 12.1.4 访问控制:MySQL如何使用Grant表 12.1.5 更新权限:修改什么时候生效 12.2 提高MySQL数据库的安全性 12.2.1 从操作系统角度来保护MySQL 12.2.2 密码 12.2.3 用户权限 12.2.4 Web问题 12.3 获取更多关于数据库的信息 12.3.1 使用SHOW获取信息 12.3.2 使用DESCRIBE获取关于列的信息 12.3.3 用EXPLAIN理解查询操作的工作过程 12.4 数据库的优化 12.4.1 设计优化 12.4.2 权限 12.4.3 表的优化 12.4.4 使用索引 12.4.5 使用默认值 12.4.6 其他技巧 12.5 备份MySQL数据库 12.6 恢复MySQL数据库 12.7 实现复制 12.7.1 设置主服务器 12.7.2 执行初始的数据传输 12.7.3 设置一个/多个从服务器 12.8 进一步学习 12.9 下一章 第13章 MySQL高级编程 13.1 LOAD DATA INFILE语句 13.2 存储引擎 13.3 事务 13.3.1 理解事务的定义 13.3.2 通过InnoDB使用事务 13.4 外键 13.5 存储过程 13.5.1 基本示例 13.5.2 局部变量 13.5.3 游标和控制结构 13.6 进一步学习 13.7 下一章 第三篇 电子商务与安全性 第14章 运营一个电子商务网站 14.1 我们要实现什么目标 14.2 考虑电子商务网站的类型 14.2.1 使用在线说明书公布信息 14.2.2 接收产品或服务的订单 14.2.3 提供服务和数字产品 14.2.4 为产品或服务增值 14.2.5 减少成本 14.3 理解风险和威胁 14.3.1 网络黑客 14.3.2 不能招揽足够的生意 14.3.3 计算机硬件故障 14.3.4 电力、通信、网络或运输故障 14.3.5 广泛的竞争 14.3.6 软件错误 14.3.7 不断变化的政府政策和税收 14.3.8 系统容量限制 14.4 选择一个策略 14.5 下一章 第15章 电子商务的安全问题 15.1 信息的重要程度 15.2 安全威胁 15.2.1 机密数据的泄露 15.2.2 数据丢失和数据破坏 15.2.3 数据修改 15.2.4 拒绝服务 15.2.5 软件错误 15.2.6 否认 15.3 易用性,性能、成本和安全性 15.4 建立一个安全政策 15.5 身份验证原则 15.6 加密技术基础 15.6.1 私有密钥加密 15.6.2 公有密钥加密 15.6.3 数字签名 15.7 数字证书 15.8 安全的Web服务器 15.9 审计与日志记录 15.10 防火墙 15.11 备份数据 15.11.1 备份常规文件 15.11.2 备份与恢复MySQL数据库 15.12 自然环境安全 15.13 下一章 第16章 Web应用的安全 16.1处理安全性问题的策略 16.1.1 以正确心态为开始 16.1.2 安全性和可用性之间的平衡 16.1.3 安全监视 16.1.4 基本方法 16.2 识别所面临的威胁 16.2.1 访问或修改敏感数据 16.2.2 数据丢失或破坏 16.2.3 拒绝服务 16.2.4 恶意代码注入 16.2.5 服务器被攻破 16.3了解与我们“打交道”的用户 16.3.1 破解人员 16.3.2 受影响机器的未知情用户 16.3.3 对公司不满的员工 16.3.4 硬件被盗 16.3.5 我们自身 16.4 代码的安全性 16.4.1 过滤用户输入 16.4.2 转义输出 16.4.3 代码组织 16.4.4 代码自身的问题 16.4.5 文件系统因素 16.4.6 代码稳定性和缺陷 16.4.7 执行引号和exec 16.5 Web服务器和PHP的安全性 16.5.1 保持软件的更新 16.5.2 查看php.ini文件 16.5.3 Web服务器配置 16.5.4 Web应用的商业主机服务 16.6 数据库服务器的安全性 16.6.1 用户和权限系统 16.6.2发送数据至服务器 16.6.3 连接服务器 16.6.4 运行服务器 16.7 保护网络 16.7.1 安装防火墙 16.7.2使用隔离区域(DMZ) 16.7.3应对DoS和DDoS攻击 16.8 计算机和操作系统的安全性 16.8.1 保持操作系统的更新 16.8.2只运行必须的软件 16.8.3 服务器的物理安全性 16.9 灾难计划 16.10 下一章 第17章 使用PHP和MySQL实现身份验证 17.1 识别访问者 17.2 实现访问控制 17.2.1 保存密码 17.2.2 密码的加密 17.2.3 保护多个网页 17.3 使用基本身份验证 17.4 在PHP中使用基本身份验证 17.5 在Apache的.htaccess文件中使用基本身份验证 17.6 使用mod_auth_mysql身份验证 17.6.1 安装mod_auth_mysql 17.6.2 使用mod_auth_mysql 17.7 创建自定义身份验证 17.8 进一步学习 17.9 下一章 第18章 使用PHP和MySQL实现安全事务 18.1 提供安全的事务处理 18.1.1 用户机器 18.1.2 Internet 18.1.3 我们的系统 18.2 使用加密套接字层(SSL) 18.3 屏蔽用户的输入 18.4 提供安全存储 18.5 存储信用卡号码 18.6 在PHP中使用加密技术 18.6.1 安装GPG 18.6.2 测试GPG 18.7 进一步学习 18.8 下一章 第四篇 PHP的高级技术 第19章 与文件系统和服务器的交互 19.1 文件上载 19.1.1 文件上载的HTML代码 19.1.2 编写处理文件的PHP 19.1.3 避免常见上载问题 19.2 使用目录函数 19.2.1 从目录读取 19.2.2 获得当前目录的信息 19.2.3 创建和删除目录 19.3 与文件系统的交互 19.3.1 获取文件信息 19.3.2 更改文件属性 19.3.3 创建、删除和移动文件 19.4 使用程序执行函数 19.5 与环境变量交互:getenv()和putenv() 19.6 进一步学习 19.7 下一章 第20章 使用网络函数和协议函数 20.1 了解可供使用的协议 20.2 发送和读取电子邮件 20.3 使用其他Web站点的数据 20.4 使用网络查找函数 20.5 备份或镜像一个文件 20.5.1 使用FTP备份或镜像一个文件 20.5.2 上传文件 20.5.3 避免超时 20.5.4 使用其他的FTP函数 20.6 进一步学习 20.7 下一章 第21章 日期和时间的管理 21.1 在PHP中获取日期和时间 21.1.1 使用date()函数 21.1.2 使用UNIX时间戳 21.1.3 使用getdate()函数 21.1.4 使用checkdate()函数检验日期有效性 21.1.5 格式化时间戳 21.2 在PHP日期格式和MySQL日期格式之间进行转换 21.3 在PHP中计算日期 21.4 在MySQL中计算日期 21.5 使用微秒 21.6 使用日历函数 21.7 进一步学习 21.8 下一章 第22章 创建图像 22.1 在PHP中设置图像支持 22.2 理解图像格式 22.2.1 JPEG 22.2.2 PNG 22.2.3 WBMP 22.2.4 GIF 22.3 创建图像 22.3.1 创建一个背景图像 22.3.2 在图像上绘图或打印文本 22.3.3 输出最终图形 22.3.4 清理 22.4 在其他页面中使用自动生成的图像 22.5 使用文本和字体创建图像 22.5.1 创建基本画布 22.5.2 将文本调整到适合按钮 22.5.3 放置文本 22.5.4 将文本写到按钮上 22.5.5 完成 22.6 绘制图像与用图表描绘数据 22.7 使用其他的图像函数 22.8 进一步学习 22.9 下一章 第23章 在PHP中使用会话控制 23.1 什么是会话控制 23.2 理解基本的会话功能 23.2.1 什么是cookie 23.2.2 通过PHP设置cookie 23.2.3 在会话中使用cookie 23.2.4 存储会话 ID 23.3 实现简单的会话 23.3.1 开始一个会话 23.3.2 注册一个会话变量 23.3.3 使用会话变量 23.3.4 注销变量与销毁会话 23.4 创建一个简单的会话例子 23.5 配置会话控制 23.6 通过会话控制实现身份验证 23.7 进一步学习 23.8 下一章 第24章 其他有用的特性 24.1 使用eval()函数对字符串求值 24.2 终止执行:die和exit 24.3 序列化变量和对象 24.4 获取PHP环境信息 24.4.1 找到所加载的PHP扩展部件 24.4.2 识别脚本所有者 24.4.3 确定脚本最近修改时间 24.5 暂时改变运行时环境 24.6 源代码加亮 24.7 在命令行中使用PHP 24.8 下一章 第五篇 创建实用的PHP和MySQL项目 第25章 在大型项目中使用PHP和MySQL 25.1 在Web开发中应用软件工程 25.2 规划和运行Web应用程序项目 25.3 重用代码 25.4 编写可维护代码 25.4.1 编码标准 25.4.2 分解代码 25.4.3 使用标准的目录结构 25.4.4 文档化和共享内部函数 25.5 实现版本控制 25.6 选择一个开发环境 25.7 项目的文档化 25.8 建立原型 25.9 将逻辑和内容分离 25.10 优化代码 25.10.1 使用简单优化 25.10.2 使用Zend产品 25.11 测试 25.12 进一步学习 25.13 下一章 第26章 调试 26.1 编程错误 26.1.1 语法错误 26.1.2 运行时错误 26.1.3 逻辑错误 26.2 使用变量帮助调试 26.3 错误报告级别 26.4 改变错误报告设置 26.5 触发自定义错误 26.6 巧妙地处理错误 26.7 下一章 第27章 建立用户身份验证机制和个性化设置 27.1 解决方案的组成 27.1.1 用户识别和个性化设置 27.1.2 保存书签 27.1.3 推荐书签 27.2 解决方案概述 27.3 实现数据库 27.4 实现基本的网站 27.5 实现用户身份验证 27.5.1 注册 27.5.2 登录 27.5.3 登出 27.5.4 修改密码 27.5.5 重设遗忘的密码 27.6 实现书签的存储和检索 27.6.1 添加书签 27.6.2 显示书签 27.6.3 删除书签 27.7 实现书签推荐 27.8 考虑可能的扩展 27.9 下一章 第28章 创建一个购物车 28.1 解决方案的组成 28.1.1 创建一个在线目录 28.1.2 在用户购买商品的时候记录购买行为 28.1.3 实现一个付款系统 28.1.4 创建一个管理界面 28.2 解决方案概述 28.3 实现数据库 28.4 实现在线目录 28.4.1 列出目录 28.4.2 列出一个目录中的所有图书 28.4.3 显示图书详细信息 28.5 实现购物车 28.5.1 使用show_cart.php脚本 28.5.2 浏览购物车 28.5.3 将物品添加到购物库 28.5.4 保存更新后的购物车 28.5.5 打印标题栏摘要 28.5.6 结账 28.6 实现付款 28.7 实现一个管理界面 28.8 扩展该项目 28.9 使用一个已有系统 28.10 下一章 第29章 创建一个基于Web的电子邮件服务系统 29.1 解决方案的组成 29.1.1 电子邮件协议:POP3和IMAP 29.1.2 PHP对POP3和IMAP的支持 29.2 解决方案概述 29.3 建立数据库 29.4 了解脚本架构 29.5 登录与登出 29.6 建立账户 29.6.1 创建一个新账户 29.6.2 修改已有账户 29.6.3 删除账户 29.7 阅读邮件 29.7.1 选择账户 29.7.2 查看邮箱内容 29.7.3 阅读邮件消息 29.7.4 查看消息标题 29.7.5 删除邮件 29.8 发送邮件 29.8.1 发送一则新消息 29.8.2 回复或转发邮件 29.9 扩展这个项目 29.10 下一章 第30章 创建一个邮件列表管理器 30.1 解决方案的组成 30.1.1 建立列表和订阅者数据库 30.1.2 上载新闻信件 30.1.3 发送带附件的邮件 30.2 解决方案概述 30.3 建立数据库 30.4 定义脚本架构 30.5 实现登录 30.5.1 新账户的创建 30.5.2 登录 30.6 用户函数的实现 30.6.1 查看列表 30.6.2 查看邮件列表信息 30.6.3 查看邮件列表存档 30.6.4 订阅与取消订阅 30.6.5 更改账户设置 30.6.6 更改密码 30.6.7 登出 30.7 管理功能的实现 30.7.1 创建新的邮件列表 30.7.2 上载新的新闻信件 30.7.3 多文件上载的处理 30.7.4 预览新闻信件 30.7.5 发送邮件 30.8 扩展这个项目 30.9 下一章 第31章 创建一个Web论坛 31.1 理解流程 31.2 解决方案的组成 31.3 解决方案概述 31.4 数据库的设计 31.5 查看文章的树型结构 31.5.1 展开和折迭 31.5.2 显示文章 31.5.3 使用treenode类 31.6 查看单个的文章 31.7 添加新文章 31.8 添加扩充 31.9 使用一个已有的系统 31.10 下一章 第32章 生成PDF格式的个性化文档 32.1 项目概述 32.1.1 评估文档格式 32.2 解决方案的组成 32.2.1 问题与回答系统 32.2.2 文档生成软件 32.3 解决方案概述 32.3.1 提问 32.3.2 给答题评分 32.3.3 生成RTF证书 32.3.4 从模板生成PDF证书 32.3.5 使用PDFlib生成PDF文档 32.3.6 使用PDFlib的一个“Hello World”程序 32.3.7 用PDFlib生成证书 32.4 处理标题的问题 32.5 扩展该项目 32.6 下一章 第33章 使用XML和SOAP来连接Web服务 33.1 项目概述:使用XML和Web服务 33.1.1 理解XML 33.1.2 理解Web服务 33.2 解决方案的组成 33.2.1 使用Amazon的Web服务接口 33.2.2 XML的解析:REST响应 33.2.3 在PHP中使用SOAP 33.2.4 缓存 33.3 解决方案概述 33.3.1 核心应用程序 33.3.2 显示特定种类的图书 33.3.3 获得一个AmazonResultSet类 33.3.4 使用REST发送和接收请求 33.3.5 使用SOAP发送和接收请求 33.3.6 缓存请求返回的数据 33.3.7 创建购物车 33.3.8 到Amazon付账 33.4 安装项目代码 33.5 扩展这个项目 33.6 进一步学习 第34 章使用Ajax构建Web 2.0应用 34.1 Ajax 是什么? 34.1.1 HTTP请求和响应 34.1. 2 DHTML和XHTML 34.1.3 级联样式单(CSS) 34.1.4 客户端编程 34.1.5 服务器端编程 34.1.6 XML和XSLT 34.2 Ajax基础 34.2.1 XMLHTTPRequest对象 34.2.2 与服务器通信 34.2.3 处理服务器响应 34.2.4 整合应用 34.3 在以前的项目添加Ajax元素 34.3.1在PHPBookmark应用中添加Ajax元素 34.4 进一步学习 34.4.1 进一步了解文档对象模型(DOM) 34.4.2 Ajax应用可用的JavaScript函数库 34.4.3 Ajax开发人员网站 第六篇 附录 附录A 安装PHP及MySQL 附录B Web资源 第1章 PHP快速入门教程 第2章 数据的存储与检索 第3章 使用数组 第4章 字符串操作与正则表达式 第5章 代码重用与函数编写 第6章 面向对象的PHP 第7章 错误和异常处理 ……

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值