技术架构

学习笔记之《打造扛得住的MySQL数据库架构》

2019-02-15  本文已影响242人  MrJealous

打造扛得住的MySQL数据库架构

第一章 实例和故事

1-1 什么决定了电商双11大促的成败

web服务器可以随意扩展

数据库无法随意扩展 数据库具有 完整性和一致性

在双11大促中的数据库服务器

历史的数据库架构

监控信息(影响数据库性能)

最高峰 35W次 QPS&TPS

最大值 700 并发量

磁盘IO 能力要高

最好不要在主库上数据库备份
有大型活动前取消这类计划

1-3 在大促中什么影响了数据库性能

影响数据库的因素|

效率低下的SQL
大量的并发和超高的CPU使用率

风险:
大量的并发:数据库连接数被占满(max_connections默认100 生产环境要改大)
超高的CPU使用率: 因CPU资源耗尽而出现宕机

磁盘IO

风险:
磁盘IO性能突然下降(使用更快的磁盘设备)
其他大量消耗磁盘性能的计划任务(调整计划任务,做好磁盘维护)

网卡流量

风险:
网卡IO被占满(1000Mb/8 ≈ 100MB)

如何避免无法连接数据库的情况

  1. 减少从服务器的数量
  2. 进行分级缓存
  3. 避免使用"select *"进行查询
  4. 分离业务网络和服务器网络

1-4 大表带来的问题

什么样的表称为大表 ?

大表对查询的影响

大表对DDL操作的影响

风险:
MySQL版本<5.5 建立索引会锁表
MySQL版本>=5.5 虽然不会锁表但会引起主从延迟

风险:
会造成长时间的主从延迟
影响正常的数据操作

如何处理数据库中的大表

难点:
分表主键的选择
分表后跨分区数据的查询和统计

难点:
归档时间点的选择
如何进行归档操作

1-5 大事务带来的问题

什么是事务

  1. 事务是数据库系统区别于其他一切文件系统的重要特性之一

  2. 事务是一组具有原子性的SQL语句,或是一个独立的工作单元

事务具有以下特性

事务的原子性(ATOMICITY)

定义:
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不可能只执行其中的一部分操作

例:

  1. 检查理财账户中余额是否高于2000元
  2. 从理财账户的余额中减去2000元
  3. 在活动存款账户上增加2000元
    这三步不管哪步崩溃都无法进行
事务的一致性(CONSISTENCY)

定义:
一致性是指事务将数据库从一种一致性状态转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中的数据完整性没有被破坏
例:
还是原来的例子,总的账户余额保持一致,就叫做一致性。

事务的隔离性(ISOLATION)

定义:
隔离性要求一个事务对数据库中数据的修改,在未提交完成前对于其它事务是不可见的

例:
还是原来的例子,在转账还未完成时,还是能看到理财账户中的2000元

     SQL标准定义的四种隔离级别
事务的持久性(DURABILITY)

定义:
一旦事务提交,这其所作的修改就会永久保存到数据库中。

此时即使系统崩溃,已经提交的修改数据也不会丢失。

什么是大事务

定义:
运行时间比较长,操作的数据比较多的事务

风险:
锁定太多的数据,造成大量的阻塞和锁超时
回滚时所需的时间比较长
执行时间长,容易造成主从延迟

如何处理大事务

  1. 避免一次处理太多的数据

  2. 移出不必要在事务中的SELECT操作

总结

第二章 什么影响了MySQL性能

2-1 影响性能的几个方面

  1. 硬件-------cpu 内存 磁盘IO

  2. 操作系统

  3. 数据库存储引擎的选择

MySQL的最大特点是插件式存储引擎
MyISAM:不支持事务,表级锁。

InnoDB:事务存储引擎,完美支持行级锁,事务ACID特性。

  1. 数据库参数配置 (DBA要懂) 前3项的影响加起来也许都没第4项的影响大

  2. 数据库结构设计和SQL语句(<font color="red">重点</font>)

2-2 CPU资源和可用内存大小

如何选择CPU?

MySQL不支持多CPU对同一SQL并发处理

1 * CPU -> 1 * SQL

40 * CPU -> 40 * SQL

QPS是秒级的,SQL一般是毫秒或者纳秒级别的

web应用中一般并发量比较高,核心数量比频率重要

老版本对多核CPU支持很差

5.6-5.7对多核CPU支持有所改善

现在想买32位CPU可能都买不到了

64位CPU使用32位的服务器版本

内存

目前内存速度还是大于SSD的

内存对MYISAM 内存对InnoDB

提示:
内存越多越好,但对性能影响有限,并不能无限的增加性能。数据库能使用的内存是有限,如果它所有的数据都缓存到内存中则再加内存也没有意义。

不过多余的内存增加操作系统等其他服务的性能

2-3 磁盘的配置和选择

  1. 使用传统机器硬盘

  2. 使用 RAID 增强传统机器硬盘的性能

  3. 使用固态存储 SSD 和 PCIe 卡

  4. 使用网络存储 NAS 和 SAN

传统磁盘

传统机器硬盘读取数据的过程
  1. 移动磁头到磁盘表面上等正确位置

  2. 等待磁盘旋转,使的所需的数据在磁头之下

  3. 等待磁盘旋转过去,所有所需的数据都被磁头读出

如何选择传统机器硬盘
  1. 存储容量

  2. 传输速度(也就是上面的第三步)

  3. 访问时间

  4. 主轴转速

  5. 物理尺寸

2-4 使用 RAID 增强传统机器硬盘的性能

什么是RAID

RAID是磁盘冗余队列的简称(Redundant arrays of Independent Disk)

简单来说RAID的作用就是可以把多个容量较小的磁盘组成一组容量更大的磁盘,并提供数据冗余来保证数据的完整性的技术

RAID 0---常用的RAID组别

RAID 0 是最早出现的RAID模式,也称为数据条带。是组建磁盘阵列中最简单的一种形式,只需要2块以上的磁盘即可,成本低,可以提高整个磁盘的性能和吞吐量。RAID 0 没有提供冗余或错误修复能力,但是实现成本是最低的。

RAID 1---常用的RAID组别

RAID 1 又称磁盘镜像,原理是把一个磁盘的数据镜像到另一个磁盘上,也就是说数据在写入一块磁盘的同时,会在另一块闲置的磁盘上生成镜像文件,在不影响性能情况下最大限度的保证系统的可靠性和可修复性

RAID 5---常用的RAID组别

RAID 5 又称为分布式奇偶校验的独立磁盘阵列

通过分布式奇偶校验块把数据分散到多个磁盘上,这样如果任何一个盘数据失效,都可以从奇偶校验块中重建。但是如果两块磁盘失效,则整个卷的数据都无法恢复。

RAID 10---常用的RAID组别

RAID 10 又称为分片的镜像

它是对磁盘先做 RAID 1 之后对两组 RAID 1 的磁盘再做 RAID 0 , 所以对读写都有良好的性能,相对于 RAID 5 重建起来更简单,速度也更快。

RAID 级别的选择

等级 特点 是否冗余 盘数
RAID0 便宜,快速,危险 N
RAID1 高速读,简单,安全 2N
RAID5 安全,成本折中 N+1 取决于最慢的盘
RAID10 贵,高速,安全 2N

主库一般使用 RAID 10 从库使用 RAID 0 或 RAID 5

2-5 使用固态存储 SSD 或 PCIe 卡

固态存储也称为闪存(Flash Memory)

特点

SSD(固态硬盘)的特点

  1. 使用 SATA 接口 可以替换传统磁盘而不需要任何改变

  2. SATA 接口的 SSD 同样支持 RAID 技术

PCIe SSD(Fusion-IO)卡的特点

  1. 无法使用 SATA 接口 需要独特的驱动和配置

  2. 价格相对于SSD更贵 性能比SSD更好

固态存储的使用场景

2-6 使用网络存储 SAN 和 NAS

SAN(Storage Area Network) 和 NAS(Network-Attached Storage)是两种外部文件存储设备加载到服务器上的方法

SAN

NAS

网络存储使用的场景

网络对性能的影响

建议

  • 采用高性能和高带宽的网络接口设备和交换机
  • 对多个网卡进行绑定,增强可用性和带宽
  • 尽可能的进行网络隔离

2-7 总结:服务器硬件对性能的影响

2-8 操作系统对性能的影响-MySQL适合的操作系统

MySQL适合的操作系统

2-9 CentOS 系统参数优化

影响重大的a tiile 优化参数

增加连接数


+ net.core.somaxconn=65535

+ net.core.netdev_max_backlog=65535

+ net.ipv4.tcp_max_syn_backlog=65535

加快TCP回收效率


+ net.ipv4.tcp_fin_timeout=10

+ net.ipv4.tcp_tw_reuse=1

+ net.ipv4.tcp_tw_recycle=1

缓冲区接受的默认值和最大值


+ net.core.wmem_default=87380

+ net.core.wmem_max=16777216

+ net.core.rmem_default=87380

+ net.core.rmem_max=16777216

失效连接所占用TCP系统资源,加快系统回收的效率


+ net.ipv4.tcp_keepalive_time=120

+ net.ipv4.tcp_keepalive_intvl=30

+ net.ipv4.tcp_keepalive_probes=3

内存相关的参数


+ kernel.shmmax=4294967295

Linux 内核参数中最重要的参数之一,用于定义单个共享内存段的最大值。

> 注意:

    1. 这个参数应该设置的足够大,以便能在一个共享内存段下容纳下整个

    的Innodb缓冲池的大小。

    2. 这个值的大小对于64位linux系统,可取的最大值为物理内存值-1byte,

    建议值为大于物理内存的一半,一半取值大于Innodb 缓着冲池的大小即可,

    可以去物理内存-1byte。

+ vm.swappiness=0

这个参数当内存不足时会对性能参数比较明显的影响

Linux系统内存交换区:

    在Linux系统安装时都会有一个特殊的磁盘分区,称之为系统交换分区。

    使用 free -m 在系统中可以看到类似下面内容其中swap就是交换分区。

    当操作系统因为没有足够内存时就会将一些<font color="red">虚拟内存

    </font>写到<font color="red">磁盘的交换区</font>中这样就会发生

    内存交换

在MySQL服务器上是否要使用交换分区有一些争议:

    在MySQL服务所在的Linux系统上完全禁用交换分区。

    带来的风险:

      1. 降低操作系统的性能

      2. 容易造成内存溢出、崩溃,或都被操作系统Kill掉

结论:

    在MySQL服务器上保留交换区还是很必要的,但是要控制何时使用交换分区

vm.swappiness=0就是告诉Linux内核除非虚拟内存完全满了,否则不要使用交换区。


  这个文件实际上是Linux PAM 也就是插入式认证模块的配置文件。

  打开文件数的限制。

  * soft nofile 65535

  * hard nofile 65535

  加到limit.conf 文件末尾就可以了

  *      表示对所有用户有效

  soft    指的是当前系统生效的设置

  hard    表明系统中能设定的最大值

  nofile  表示所限制的资源是打开文件的最大数目

  65535  就是限制的数量

  结论:

    把可打开的文件数量增加到了65535个以保证可以打开足够多的文件句柄。

  注意:

    这个文件的修改需要重启系统才可以生效。


cat /sys/block/sda/queue/scheduler

noop anticipatory deadline [cfq]

noop(电梯式调度策略)

    NOOP实现了一个FIFO队列,它像电梯的工作方法一样对I/O请求进行组织,当

    有一个新的请求到来时,它将强求合并最近的请求之后,以此来保证请求同一

    介质。NOOP倾向饿死读而利于写,因此NOOP对于闪存设备、RAM及嵌入式系统

    是最好的选择。

deadline(截止时间调度策略) 

    Deadline确保了再一个截止时间内服务请求,这个截止时间是可调整的,而

    默认读期限短语写期限。这样就防止了写操作因为不能被读取而饿死的现象,

    Deadline对数据库类应用是最好的选择。

anticipatory(预料I/O调度策略)

    本质上与Deadline一样,但在最后一次读操作后,要等6ms,才能继续进行对

    其它I/O请求进行调度。他会在每个6ms中插入行的I/O操作,而会将一些小写

    入流合并成一个大写入流,用写入延时换区最大的写入吞吐量。AS适合于*写

    入较多的环境*,比如文件服务器,AS对数据库环境表现很差。

2-10 文件系统对性能的影响

Windows

Linux

EXT3/4系统的挂载参数(/etc/fstab)

data=writeback | ordered | journal

noatime,nodiratime

/dev/sda1/ext4 noatime,nodiratime,data=writeback 1 1

2-11 MySQL体系结构

MySQL 插件式的存储引擎

客户端

存储引擎层

2-12 MySQL常用的存储引擎之MyISAM

frm文件存的是数据表结构信息
MYD存的是数据信息
MYI存的是索引信息

特性


check table tablename;

repair table tablename;

限制

适用场景

2-13 MySQL常用的存储引擎之InnoDB

把原来存在于系统表空间中的表转移到独立表空间的方法

步骤:

  1. 使用mysqldump导出所有数据库表数据

  2. 停止MySQL服务,修改参数,并删除InnoDB相关文件

  3. 重启MySQL服务,重建Innodb系统表空间

  4. 重新导入数据

2-14 InnoDB存储引擎的特性(1)

系统表空间和独立表空间要如何选择

Innodb存储引擎的特性

什么是锁

锁的类型

写锁 读锁
写锁 不兼容 不兼容
读锁 不兼容 兼容

锁的粒度

2-15 InnoDB存储引擎的特性(2)

阻塞和死锁

Innodb状态检查


show engine innodb status

适用场景

2-16 MySQL常用存储引擎之CSV

文件系统存储特点

特点

适用场景

2-17 MySQL常用存储引擎之Archive

文件系统存储特点

特点

适用场景

2-18 MySQL常用存储引擎之Memory

文件系统存储特点

特点

容易混淆的概念

Memory存储引擎表 VS 临时表

适用场景

Memory数据易丢失,所以要求数据可再生

2-19 MySQL常用存储引擎之Federated(MySQL默认禁止,性能不好)

文件系统存储特点

特点

如何使用

mysql://username[:password]@host_name[:port_num]/db_name/tbl_name

适用场景

2-20 如何选择存储引擎

参考条件

2-21 到 2-25 待续

2-26 数据库设计对性能的影响

什么影响了性能

2-27 总结

性能优化顺序

第三章 MySQL 基准测试

3-1 什么是基准测试

定义:
基准测试是一种测量和评估软件性能指标的活动用于建立某个时刻的性能基准,以便当系统发生软硬件变化时重新进行基准测试以评估变化对性能的影响

基准测试是针对系统设置的一种压力测试

eg:

3-2 如何进行基准测试

基准测试的目的

如何进行基准测试

  1. 对整个系统进行基准测试

    从系统入口进行测试(如网站Web前端,手机APP前端)

    • 优点:

      • 能够测试整个系统的性能,包括web服务器缓存、数据库等

      • 能反映出系统中各个组件接口间的性能问题体现真实性能状况

    • 缺点:

      • 测试设计复杂,消耗时间长
  2. 单独对MySQL进行基准测试

    • 优点:

      • 测试设计简单,所需要耗费时间短
    • 缺点:

      • 无法全面了解整个系统的性能基准线

MySQL基准测试的常见指标

3-3到3-6未完待续

第四章 MySQL数据库结构优化

4-1 数据库结构优化介绍

良好的数据库逻辑设计和物理设计是数据库获得高性能的基础

数据库结构优化的目的

4-2 数据库结构设计

数据库结构设计的步骤

  1. 需求分析:全面了解产品设计的存储需求

    • 存储需求

    • 数据处理需求

    • 数据的安全性和完整性

  2. 逻辑设计:设计数据的逻辑存储结构

    • 数据实体之间的逻辑关系,解决数据冗余和数据维护异常
  3. 物理设计:根据所使用的数据库特点进行表结构设计

    • 关系型数据库:Oralce,SQLServer,MySQL,postgresSQL

    • 非关系型数据库: mongo,Redis,Hadoop

  4. 维护优化:根据实际情况对索引、存储结构等进行优化

数据库设计范式

定义:设计出没有数据冗余和数据维护异常的数据库结构

数据库设计的第一范式

数据库设计的第二范式

数据库设计的第三范式

4-3 需求分析及逻辑设计

需求说明

按下面的需求设计一个电子商务网站的数据库结构

  1. 本网站只销售图书类商品

  2. 需要具有以下功能

    • 用户登录

    • 用户管理

    • 商品展示

    • 商品管理

    • 供应商管理

    • 在线销售

需求分析及逻辑设计

用户登录及用户管理功能
商品展示及商品管理功能
供应商管理功能
在线销售功能

select 下单用户名,sum(d.商品价格*b.商品数量) from 订单表 a join 订单商品关联表 b on a.订单编号=b.订单编号 join 商品分类关联表 c on c.商品名称=b.商品名称 and c.分类名称=b.订单商品分类 join 商品信息表 d on d.商品名称=c.商品名称 group by 下单用户名

  1. 关联表越多性能越差

  2. 如果商品价格变动那会发生什么变化

假设下单用户就是商品的收货人,我们在发货前一定要查询出每个订单的下单人信息,而这些信息全部记录在用户信息表中
编写SQL查询出下单用户和订单详情

select a.订单编号, e.用户名, e.手机号, d.商品名称, c .商品数量, d.商品价格 join 商品分类关联表 c on c .商品名称= b.商品名称 join 商品信息表 d on d.商品名称= c.商品名称 join 用户信息表 e on e.用户名= a.下单用户名

完全符合范式化的设计有时并不能得到良好的SQL查询性能

4-4 需求分析及逻辑设计-反范式化设计

什么叫做反范式化设计

反范式化是针对范式化而言的,在前面介绍了数据库设计的范式,所谓的反范式化就是为了性能和读取效率的考虑而适当的对数据库设计范式的要求进行违反,而允许存在少量的数据冗余,换句话来说反范式化就是使用空间来换取时间

图书在校销售网站数据库的反范式化改造

反范式化改造后的查询


select 下单用户名,sum(订单金额) from 订单表 group by 下单用户名


select a.订单编号, a.用户名, a.手机号, b.商品名称, b.商品单价, b.商品数量 from 订单表 a join 订单商品关联表 b on a.订单编号=b.订单编号

总结

4-5 范式化设计与反范式化设计优缺点

范式化设计的优缺点

反范式化设计的优缺点

4-6 物理设计介绍

物理设计设计的内容

定义数据库、表及字段的命名规范

选择合适的存储引擎

存储引擎 事务 锁粒度 主要应用 忌用
MyISAM 不支持 支持并发插入的表级锁 SELECT,INSERT 读写操作频繁
MRG_MYISAM 不支持 支持并发插入的表级锁 分段归档,数据仓库 全局查找过多的场景
Innodb 支持 支持MVCC的行级锁 事务处理
Archive 不支持 行级锁 日志记录,只支持insert,select 需要随机读取,更新,删除
Ndb 支持 行级锁 高可用性 大部分应用

4-7 物理设计-数据类型的选择

为表中的字段选择合适的数据类型

当一个列可以选择多种数据类型时,应该优先考虑素质类型,
其次是日期或二进制类型,最后是支付类型。
对于相同级别的数据类型,应该优先选择占用空间小的数据类型。

如何选择正确的整数类型

列类型 存储空间 SINGED UNSINGED
tinyint 1字节 -128~127 (-27~(27)-1) 0~255 ((2^8)-1)
smallint 2字节 -32768~32767 (-2^15 ~ (2^15)-1) 0~65535 ((2^16)-1)
mediumint 3字节 -8388608~8388607 (-2^23 ~ (2^23)-1) 0~16777215 ((2^24)-1)
int 4字节 -2147483648~2147483647 (-2^31 ~ (2^31)-1) 0~4294967295 ((2^32)-1))
bigint 8字节 -9223372036854775808
~ 9223372036854775807 (-2^63 ~ (2^63)-1)
0
~18446744073709551615 ((2^64)-1)

如何选择正确的实数类型

列类型 存储空间 是否精确类型
FLOAT 4个字节
DOUBLE 8个字节
DECIMAL 每4个字节存9个数字,小数点占一个字节

如何选择VARCHAR和CHAR类型

VARCHAR类型的存储特点

定义的都是字符长度不是字节长度,UTF8为例,一个字符占3个字节;

VARCHAR长度的选择问题
VARCHAR的适用场景
CHAR类型的存储特点
CHAR的适用场景

4-8 物理设计-如何存储日期类型

DATETIME类型

以YYYY-MM-DD HH:MM:SS[.fraction] 格式存储日期时间

datetime = YYYY-MM-DD HH:MM:SS

datetime(6) = YYYY-MM-DD HH:MM:SS.fraction(微秒需要datetime(6))

DATETIME类型与时区无关,占用8个字节的存储空间

时间范围1000-01-01 00:00:00到9999-12-31 23:59:59

TIMESTAMP类型

存储了由格林尼治时间1970年1月1日到当前时间的秒数

以YYYY-MM-DD HH:MM:SS.[fraction]的格式显示,占用4个字节(timestamp(6))

时间范围1970-01-01到2038-01-19

DATE类型和TIME类型

DATE类型的有点:
  1. 占用的字节数比使用字符串(8字节)、datetime(8字节)、int(4字节)存储要少,使用dete类型只需要3个字节

  2. 使用Date类型还可以利用日期时间函数进行日期之间的计算

    • date类型用于保存1000-01-01到9999-12-31之间的日期
TIME类型

time类型用于存储时间数据,格式为HH:MM:SS

存储日期时间数据的注意事项

4-9 物理设计-总结

物理设计

第五章 MySQL高可用架构设计

未完待续

第六章 数据库索引优化

6-1 Btree索引和Hash索引

MySQL支持的索引类型

B-tree索引的特点(通常说的索引就是B-tree索引)
在什么情况下可以用到B-tree索引
B-tree索引的使用限制
Hash索引的特点
Hash索引的限制
为什么要使用索引
索引是不是越多越好

6-2 安装演示数据库

6-3 索引优化策略(上)


CREATE INDEX index_name ON table(col_name(n));

索引的选择性是不重复的索引值和表的记录数的比值

6-4 索引优化策略(中)

使用索引来优化查询

6-5 索引优化策略(下)

利用索引优化锁

索引的维护和优化

第七章 SQL查询优化

7-1获取有性能问题的SQL的三种方法

查询优化,索引优化,库表结构优化需要齐头并进。

7-2 慢查询日志介绍

主要开销

语句

MySQL 慢查询日志分析工具

7-3 慢查询日志实例

7-4 实时获取性能问题SQL

如何实时获取有性能问题的SQL

7-5 SQL的解析预处理及生成执行计划

查询速度为什么会慢

MySQL服务器处理查询请求的整个过程

会造成MySQL生成错误的执行计划的原因

MySQL优化器可优化的SQL类型

7-6 如何确定查询处理各个阶段所消耗的时间

使用profile

使用performance_schema


UPDATE `setup_instruments` SET enabled='YES',TIMED='YES' WHERE NAME LIKE 'stage%';

UPDATE `setup_consumers` SET enabled='YES' WHRER NAME LIKE 'events%';

7-7 特定SQL的查询优化

如何进行大表的数据修改

大表的数据修改最好要分批处理
1000万行记录的表中删除/更新100万行记录
一次只删除/更新5000行记录
暂停几秒

大表的更新和删除

如何修改大表的结构

先修改从服务器,然后手动切换主从,在修改主服务器,再切回来,这种方法需要手动,有风险


主服务器建新表,将老表加触发器数据同步到新表,然后老表加排它锁,然后新表重新命名,删除老表

如何优化not in和<>查询


SELECT cutsomer_id,first_name,last_name,email FROM customer WHERE customer_id NOT IN(SELECT customer_id FROM payment)


SELECT a.customer_id,a.first_name,a.last_name,a.email FROM customer a LEFT JOIN payment b ON a.customer_id=b.customer_id WHERE b.customer_id IS NULL;

使用汇总表优化查询


SELECT COUNT(*) FROM product_comment WHERE product_id=999

汇总表就是提前以要统计的数据进行汇总并记录到表中以备后续的查询使用


CREATE TABLE product_comment_cnt(product_id INT,cnt INT);

SELECT SUM(cnt) FROM(SELECT cnt FROM product_comment_cnt WHERE product_id=999 UNION ALL SELECT COUNT(*) FROM product_comment WHERE product_id=999 AND timestr>DATE(NOW())) a

第八章 数据库的分库分表

8-1 数据库分库分表的几种方式

  1. 把一个实例中的多个数据库拆分到不同的实例

  2. 把一个库中的表分离到不同的数据库中

  3. 对一个库中的相关表进行水平拆分到不同实例的数据库中

  4. 垂直拆分

8-2 数据库分片前的准备

对一个库中的相关表进行水平拆分到不同实例的数据库中(不得不的时候才分片)

未完待续

本文是学习笔记,如有错误,劳请各位大佬指正

上一篇 下一篇

猜你喜欢

热点阅读