MySQL的相关问题


前言

这里先开始MySQL篇章吧,只写一些重点了,时间比较仓促,细节后面补充。


一、MySQL简介

MySQL是典型的关系型数据库,通过将世界里的事物、关系抽象成关系模型并用二维表格存储。其优点包括:

  1. 容易理解,数据库表的每个字段都代表了真实事物的一个属性
  2. 使用方便,可以方便的使用SQL语句进行增删改查
  3. 易于维护,减少了数据的冗余和数据不一致的概率
  4. 支持复杂查询

由于其数据存储在硬盘当中,其读写性能相对较差,尤其面对高并发时性能往往较差。

二、MySQL的存储引擎

MySQL常见的存储引擎包括:innodb、myisam、memory、merge等。
innodb行级锁,Innodb引擎提供了对数据库ACID事务的⽀持,并且实现了SQL标准的四种隔离级别,该引擎还提供了⾏级锁和外键约束。由于锁的粒度更⼩,写操作不会锁定全表,所以在并发较⾼时,使⽤Innodb引擎会提升效率。但是使⽤⾏级锁也不是绝对的,如果在执⾏⼀个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。

myisam全表锁,是它没有提供对数据库事务的⽀持,也不⽀持⾏级锁和外键,因此当INSERT(插⼊)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低⼀些,并发性能差,但其占用空间相对较小。

memory全表锁,存储在内存,占用空间是和存储数据成正比,系统重启后数据就消失了,默认使用Hash索引,检索效率非常高。

merge:myisam表的组合。

innodb 和 myisam的区别,参考链接。

innodbmyisam
事务支持不支持
行级锁全表锁
缓存缓存索引、数据只缓存索引
主键必须有,用于实现聚簇索引可以没有
索引B+树,主键是聚簇索引B+树,主键是非聚簇索引
select count(*) from table扫描全表用一个变量保存了表的行数
hash索引支持不支持
存储顺序按主键大小顺序插入按记录插入顺序保存
外键支持不支持
全文索引5.7 支持支持
关注点事务性能

innoDB的四大特性

  • 插入缓存(insert buffer)
    对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池(Buffer pool)中,若在,则直接插入;若不在,则先放入到一个 Insert Buffer 对象中,然后再以一定的频率和情况进行 Insert Buffer 和辅助索引叶子节点的 merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。插入缓冲的使用需要满足以下两个条件:索引是辅助索引(普通索引);索引不是唯一的。
  • 二次写(double write)
    **原因**:MySQL的数据页默认是16K,而文件系统的数据页是4K,IO操作是按页为单位就行读写的。这就可能出现数据库对一个16k的数据页修改后,操作系统开始进行写磁盘,但是在这个过程中数据库宕机导致没有完全将16K数据页写到磁盘上。数据库重启后,校验数据页,发现有数据页不完整,就起不来了(redo是基于完整数据页进行的恢复),原文链接
    **解决方案**:为了解决这个问题,MySQL引入了double write这个特性。double write针对的是脏数据,提高innodb的可靠性,用来解决部分写失败(partial page write)。为了数据的持久性,脏数据需要刷新到磁盘上,而double write就产生在将脏数据刷盘的过程中。刷盘是一份脏数据写到共享表空间ibdata中,一份写到真正的数据文件永久的保存。写了两次脏数据,就叫double wriete。
    **流程**
    1. 先将脏数据复制到内存中的 doublewrite buffer;
    2. 之后通过 doublewrite buffer 再分2次,每次1MB写入到共享表空间的磁盘上(顺序写,性能很高);
    3. 完成第二步之后,马上调用 fsync 函数,将doublewrite buffer中的脏页数据写入实际的各个表空间文件(离散写)。
  • 自适应哈希索引(adaptive hash index)
    InnoDB 会监控对表上索引的查找,如果观察到某些索引被频繁访问,索引成为热数据,并为其建立哈希索引。自适应哈希索引通过缓冲池的 B+ 树构造而来,因此建立的速度很快。而且不需要将整个表都建哈希索引,InnoDB 会自动根据访问的频率和模式来为某些页建立哈希索引。自适应哈希索引功能由innodb_adaptive_hash_index变量启用,或在服务器启动时由--skip-innodb-adaptive-hash-index禁用。
  • 预读(read ahead)
    **线性预读(Linear read-ahead):** extent(区间)中有多少页(page)是顺序读取的,如果超过了阈值,那么就开始一个异步 io 读取下一个extent 到缓存池中。线性预读方式有一个很重要的变量 innodb_read_ahead_threshold,可以控制 Innodb 执行预读操作的触发阈值。innodb_read_ahead_threshold 可以设置为0-64(一个 extend 上限就是64页)的任何值,默认值为56,值越高,访问模式检查越严格。
    **随机预读(Random read-ahead):** 当同一个 extent 中的一些 page 在 buffer pool 中发现时,Innodb 会将该 extent 中的剩余 page 一并读到 buffer pool中。由于随机预读方式给编码带来了不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃。要启用此功能,需要将配置变量 innodb_random_read_ahead 设置为ON。

二、索引

1、索引的优缺点

数据库索引是数据库管理系统的一种排序的数据结构,索引通常由B+树(也有散列索引、位图索引)实现,而且索引是一种文件,占用物理空间。
优点:

  1. 索引可以加快检索的速度
  2. 创建唯⼀性索引,保证数据库表中每⼀⾏数据的唯⼀性
  3. 在使⽤分组和排序⼦句进⾏数据检索时,可以显著减少查询中分组和排序的时间
  4. 加速表和表之间的连接

缺点:

  1. 索引需要占⽤数据表以外的物理存储空间
  2. 创建索引和维护索引要花费⼀定的时间
  3. 当对表进⾏更新操作时,索引需要被创建,这样降低了数据的维护速度

2、索引的类型

类型作用
Primary Key 主键索引(聚簇索引)一个表只能有一个主键(字段),列中的值不允许重复,不允许为NULL
Compound Index 组合索引组合索引是在多个字段上创建的索引,需要遵守最左前缀原则,即在查询条件中,只有使用了组合索引中的第一个字段,索引才会使用
Unique 唯一索引列允许为NULL,不允许重复。一个表允许多个列创建唯一索引。若是组合索引,则(字段组合的)列值的组合必须唯一
Key 普通索引基本索引类型,没有唯一性的限制,允许值为NULL
FullText 全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较(InnoDB中不支持使用全文索引)
Spatial 空间索引空间索引的数据结构是R树,R树实际上就是多维的B树
  • 聚簇索引: 将索引与数据行放在了一起,找到索引也就找到了数据。无需进行回表查询操作,效率高,InnoDB必然会有聚簇索引,且每张表只会存在一个。通常是主键,若没有主键,则优先选择非空的唯一索引,若唯一索引也没有,则会创建一个隐藏的自增的 row_id 作为聚簇索引。聚集索引(clustered index)就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,聚集索引的存储并不是物理上连续的,而是逻辑上连续的。
  • 非聚簇索引: 将索引与数据行分开,找到索引后需要通过对应的地址找到的数据行。
  • 组合索引: 组合索引指在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用。使用组合索引时遵循最左前缀集合
// 组合索引
alter table 表名 add index 索引名(字段名1,字段名2,字段3)

建立这样的索引,其实相当于建立了三个索引: 原因:组合索引“最左前缀”

  1. 字段1,字段2,字段3
  2. 字段1,字段2
  3. 字段1

有效调用:(注意:and会自动排序,只需包含就可)

1. select * from 表名 where 字段2="xxxx" and 字段1="xxx"
2. select * from 表名 where 字段1="xxx"

无效调用:(即没有遵循最左原则)

1. select * from 表名 where 字段3="xxxx" and 字段2="xxx"
2. select * from 表名 where 字段2="xxxx"

无效调用:(即 存储引擎不能使用索引中范围条件(>、<、<=、>=、like、between in、like 值%)右边的列。)
**P.S.**模糊查询中,后模糊匹配才能让索引有效

  1. like %keyword 索引失效,使用全表扫描。但可以通过翻转函数(reverse)+like前模糊查询+建立翻转函数索引=走翻转函数索引,不走全表扫描。
  2. like keyword% 索引有效。
  3. like %keyword% 索引失效,也无法使用反向索引。
因为LIKE是范围查询,所以起作用的索引只有前两列
select * from 表名 where 字段1="xxxx" and 字段2="x%" and 字段3="xxxx"

组合索引前缀综述总结,包括order by在内,左边的必须有,同时不能有范围

order by使用索引最左前缀
	- order by a
	- order by a,b
	- order by a,b,c
	- order by a desc, b desc, c desc

如果where使用索引的最左前缀定义为常量,则order by能使用索引
	- where a=const order by b,c
	- where a=const and b=const order by c
	# 这个where虽然有b>,但是后面用的是order by b,c所以可以使用,如果用的是order by c,就不可以了
	- where a=const and b > const order by b,c 
	
不能使用索引进行排序
	- order by a , b desc ,c desc --排序不一致,a升序
	- where d=const order by b,c --a丢失
	- where a=const order by c --b丢失
	- where a=const order by b,d --d不是索引的一部分

	- where a in(...) order by b,c --a属于范围查询
  • 唯一索引:
    它与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
# 创建索引
CREATE UNIQUE INDEX [indexName] ON [table_name](字段(length))
# 修改索引
ALTER table [table_name] ADD UNIQUE [indexName](字段(length))
  • 普通索引
    这是最基本的索引,它没有任何限制。
# 创建索引
CREATE INDEX [indexName] ON [table_name] (column_name)
# 修改索引
ALTER table [tableName] ADD INDEX [indexName] (columnName)
# 删除索引
DROP INDEX [indexName] ON [tableName];
  • 全文索引: like + % 在文本比较少时是合适的,但是对于大量的文本数据检索,是不可想象的全文索引在大量的数据面前,能比 like + % 快 N 倍,速度不是一个数量级,但是全文索引可能存在精度问题。
#创建全文索引
create fulltext index [indexName] on [tableName](字段1,字段2);
alter table [tableName] add fulltext index [indexName](字段1,字段2);
#删除
drop index [indexName]on [tableName];
alter table [tableName] drop index [indexName];

使用全文索引: 他有自己的语法,使用match和against,match() 函数中指定的列必须和全文索引中指定的列完全相同,否则就会报错,无法使用全文索引,这是因为全文索引不会记录关键字来自哪一列如果想要对某一列使用全文索引,请单独为该列创建全文索引使用全文索引时,测试表里至少要有 4 条以上的记录

select * from [tableName] where match(content,tag) against('xxx xxx');

3、索引建立的原则

  1. 索引并非越多越好,一个表中如果有大量的索引,不仅占用磁盘空间,而且会影响INSERT、DELETE、UPDATE等语句的性能,因为在表中的数据更改的同时,索引也会进行调整和更新。
  2. 避免对经常更新的表进行过多的索引,并且索引中的列尽可能少。而对经常用于查询的字段应该创建索引,但要避免添加不必要的字段。
  3. 数据量小的表最好不要使用索引,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
  4. 不同值较多的列上建立索引在不同值很少的列上不要建立索引。比如在学生表的“性别”字段上只有“男”与“女”两个不同值,因此就无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度。
  5. 唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。
  6. 频繁进行排序或分组(即进行group by或order by、count操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引。
  7. 最适合索引的列是出现在WHERE子句中的列,或连接子句中指定的列,而不是出现在SELECT关键字后的选择列表中的列。
 SELECT t.Name FROM mytable t LEFT JOIN mytable m ON t.Name=m.username WHERE m.age=20 AND m.city='郑州'
  1. 使用短索引。如果对字符串列进行索引,应该指定一个前缀长度,只要有可能就应该这样做。

  2. 利用最左前缀。在创建一个n列的索引时,实际是创建了MySQL可利用的n个索引。多列索引可起几个索引的作用,因为可利用索引中最左边的列集来匹配行。这样的列集称为最左前缀。

  3. 定义有外键的数据列一定要建立索引。

  4. 对于定义为text、image和bit的数据类型的列不要建立索引。

PS:若查询的字段都建立了索引,那么引擎会直接在索引表中查询而不会访问原始数据,这叫索引覆盖。否则只要有一个字段没有建立索引就会做全表扫描。

4、索引的数据结构

MySQL中使用较多的索引有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现为:B+树索引。哈希索引底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快。

B+树索引

  • B+树的性质
  1. n棵子tree的节点包含n个关键字,用来保存数据的索引。
  2. 非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
  3. B+ 树中,数据对象的插入和删除仅在叶节点上进行。
  4. 叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  5. B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶子节点。
  • B树和B+树的区别
  1. 在B树中,可以将键和值存放在内部节点和叶子节点;在B+树中,内部节点都是键,没有值,叶子节点同时存放了键和值;
  2. B树的叶子节点相互独立;B+树的叶子节点是相互连接的,形似一个链表;
  • B+树更适合应用于数据库索引
  1. B+树更适应磁盘特性,相比B树减少了I/O读写的次数。B+树的非叶子结点只存key不存数据,因此单个页可以存储更多key,一次性读入内存需要查找的key也就更多,磁盘的I/O读取次数就相对较少。
  2. B+树的查询效率比B树更稳定,因为数据只存在叶子结点上,所以查找效率为O(logN);
  3. B树非叶子结点存了数据,所有只能通过中序遍历按序遍历。B+树叶子结点间用链表链表,所以遍历所有数据只需遍历一遍叶子结点,相对于B树效率更高;

hash索引

哈希索引基于哈希表实现,只有精确匹配索引所有列查询时才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希索引将哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。索引自身只需存储对应的哈希值,所以索引结构十分紧凑,这让哈希索引的速度非常快。只能用于查找
限制:

  1. 数据不是按照索引值顺序存储的,无法排序
  2. 不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的;
  3. 只支持等值比较查询,不支持范围查询

5、索引的原理

索引的原理就是把无序的数据变成有序的查询。

  1. 将创建了索引的列的内容进行排序;
  2. 对排序结果生成倒排表;
  3. 在倒排表内容上拼上数据地址链;
  4. 在查询时先拿到倒排表内容,再取出数据地址链,从而拿到具体数据;

6、innoDB的索引

  • 数据文件本身就是索引文件
  • 数据文件本身是按B+树组织的一个索引结构文件
  • 聚集索引(主键索引)的叶子节点包含了完整的数据记录
  • 数据库的表必须有主键且推荐使用整型的自增主键
  • 普通索引结构的叶子节点存储的是主键值

删除索引的过程:

  • 先删除索引
  • 再删除一些无用数据

7、回表查询

在InnoDB中,对于主键索引,只需要跑一遍主键索引的查询就能获取叶子节点的数据。对于普通索引,叶子节点存储的是 key + 主键值,所以还需要跑一遍主键索引的查询才能找到数据行,这就是回表查询,先定位主键值,再定位数据行。

但不是所有的普通索引一定会出现回表查询,若查询SQL所要求的字段全部命中索引,那就不用进行回表查询。比如有一个user表,主键为id,name是个普通索引,执行SQL:select id,name from user where name='aitao'时,通过name的索引就可以获取到id和name数据,所以无需回表查询数据行。

三、SQL

1、MySQL的数据类型

数值类型

类型大小(Byte)范围(有符号)范围(无符号)用途
TINYINT1 Bytes(-128,127)(0,255)小整数值
SMALLINT2 Bytes(-32 768,32 767)(0,65 535)大整数值
MEDIUMINT3 Bytes(-8 388 608,8 388 607)(0,16 777 215)大整数值
INT或INTEGER4 Bytes(-2 147 483 648,2 147 483 647)(0,4 294 967 295)大整数值
BIGINT8 Bytes(-9,223,372,036,854,775,808,9 223 372 036 854 775 807)(0,18 446 744 073 709 551 615)极大整数值
FLOAT4 Bytes(-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38)0,(1.175 494 351 E-38,3.402 823 466 E+38)单精度( 浮点数值)
DOUBLE8 Bytes(-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308)双精度(浮点数值)
DECIMAL对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2依赖于M和D的值依赖于M和D的值小数值

日期和世界类型

类型大小(Byte)范围格式用途
DATE31000-01-01/9999-12-31YYYY-MM-DD日期值
TIME3‘-838:59:59’/‘838:59:59’HH:MM:SS时间值或持续时间
YEAR11901/2155YYYY年份值
DATETIME8‘1000-01-01 00:00:00’ 到 ‘9999-12-31 23:59:59’YYYY-MM-DD hh:mm:ss混合日期和时间值
TIMESTAMP4‘1970-01-01 00:00:01’ UTC 到 ‘2038-01-19 03:14:07’ UTC,结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07YYYY-MM-DD hh:mm:ss混合日期和时间值,时间戳

字符串类型:

类型大小(Byte)用途
CHAR0-255 bytes定长字符串
VARCHAR0-65535 bytes变长字符串
TINYBLOB0-255 bytes不超过 255 个字符的二进制字符串
TINYTEXT0-255 bytes短文本字符串
BLOB0-65 535 bytes二进制形式的长文本数据
TEXT0-65 535 bytes长文本数据
MEDIUMBLOB0-16 777 215 bytes二进制形式的中等长度文本数据
MEDIUMTEXT0-16 777 215 bytes中等长度文本数据
LONGBLOB0-4 294 967 295 bytes二进制形式的极大文本数据
LONGTEXT0-4 294 967 295 bytes极大文本数据

注意:

  • varchar和char的区别

      char的特点:
      	1、char是定长字符串,长度固定;
      	2、若保存的字符串长度小于char的固定长度,则会用空格填充;
      	3、char的访问速度快,但是耗费空间;(空间换时间)
      	4、char最多能存放255个字符,和编码无关;
      varchar的特点:
      	1、varchar是可变长字符串,长度是可变的;
      	2、varchar保存的字符串多长,就按多长来存储;
      	3、varchar访问速度慢,但是节约空间;(时间换空间)
      	4、varchar最多能存放65535个字符;
      char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。varchar(50)和(200)存储hello所占空间一样,但后者在排序时会消耗更多内存,因为order by col采用fixed_length计算col长度(memory引擎也一样)
    
  • int(20)
    20的含义是指显示字符的长度。20表示最大显示宽度为20,但仍占4字节存储,存储范围不变;不影响内部存储,只是影响带 zerofill 定义的 int 时,前面补多少个 0,易于报表展示。

  • datetime和timestamp的区别
    datetime能保存大范围的值,从1001~9999年,精度为秒。把日期和时间封装到了一个整数中,与时区无关,使用8字节存储空间。
    timestamp只使用4字节的存储空间,范围比datetime小,只能表示1970~2038年,并且依赖于时区。

  • float和double的区别
    FLOAT类型数据可以存储至多8位十进制数,并在内存中占4字节。
    DOUBLE类型数据可以存储至多18位十进制数,并在内存中占8字节。

2、关联查询

  1. 内连接(inner join): 只返回两个表中连接字段相等的行。
  2. 左连接(left join): 返回包括左表中的所有记录和右表中连接字段相等的记录。
  3. 右连接(right join):返回包括右表中的所有记录和左表中连接字段相等的记录。
  4. 全外连接:连接的表中不匹配的数据全部会显示出来。
  5. 交叉连接: 笛卡尔效应,显示的结果是链接表数的乘积。
  6. union:对多个结果集进行合并时,对记录会去重,并按字段的默认规则排序。
  7. union all:对多个结果集进行合并时,对记录不会去重和排序。

3、SQL注入

SQL注入就是通过把SQL命令插入到Http请求中,达到欺骗服务器执行恶意的SQL命令。
应对方法:

  • 使用正则表达式过滤传入的参数
  • 使用预编译手段,绑定参数是最好的防SQL注入的方法。
    解释:预编译可以只包含SQL的操作(例如增删改查),需要传入的数据可以先不传入,这样一个SQL语句要执行的动作就实现确定了,再传参数只需要把要操作的表名、字段传进来就可以,因为事先要执行的操作已经确定,不会发生SQL注入的情况。
  • 参数绑定

4、删除操作

  • drop、delete与truncate的区别
  1. delete和truncate只删除表的数据不删除表的结构;
  2. delete语句是DML(数据库操作语言)操作,事务提交后才会生效;若有相应的触发器(trigger),执行时会被触发;
  3. truncate和drop是DDL(数据库定义语言)操作,操作立即生效,不能回滚,不触发触发器(trigger)
  4. SQL 执行速度: drop > truncate > delete
  5. truncate删除表中的所有数据,表结构还在;drop删除表,表结构不在
  • count(字段)、count(主键)、count(1)、count(*)的区别
    count是一个聚合函数,对于返回的结果集它会一行行去判断,只要不为NULL,就累加1。最后返回累计值。
    count(可空字段) < count(主键 id) < count(1) ≈ count(*)

5、SQL优化

1、SQL语句的书写规范

  1. 查询语句中不要使用 select *
  2. 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
  3. 减少使用IN或者NOT IN ,使用exists,not exists或者关联查询语句替代
  4. or 的查询尽量用 union或者union all 代替(在确认没有重复数据或者不用剔除重复数据时,union all会更好)
    索引失效的情况:参考阿秀笔记
  5. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,索引不会包含有NULL值的列
  6. 应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
  7. 不要在列上使用函数,这将导致索引失效而进行全表扫描。
  8. 不要在列上进行运算,这也将导致索引失效而进行全表扫描。
  9. 多个单列索引并不是最佳选择,可以创建组合索引
  10. 尽量少用范围查询
  11. 当查询条件左右两侧类型不匹配的时候会发生隐式转换,隐式转换带来的影响就是可能导致索引失效而进行全表扫描
  12. like 语句的索引失效问题 (like %xx%)

对于数据库的查询,尽量使用缓存:
1、减少使用join,join过多在对SQL进行慢优化的时候很难发现怎么优化;替代方案是把数据库加载到缓存中,在缓存中合并数据
2、减少数据库的冗余
3、使用in (exist)代替join
4、使用es宽表,背后机制缓存机制的使用

2、explain字段

  • id:标识符,越大越先执行
  • select_type:查询表类型
select_type描述
SIMPLE不包含任何子查询或union等查询
PRIMARY
SUBQUERY在select或where子句中包含的查询
DERIVEDfrom子句中包含的查询
UNION出现在union后的查询
UNION RESULT从union中获取结果集
  • table:输出结果集的表
  • partitions:匹配的分区
  • type:表的连接类型
    按类型排序,从好到坏,常见的有:const > eq_ref > ref > range > index > all
type描述
const通过主键或唯一键查询,并且结果只有1行(也就是用等号查询)
eq_ref通常出现于两表关联查询时,使用主键或非空唯一键关联,并且查询条件不是主键或唯一键的等号查询
ref通过普通索引查询,并且使用的等号查询
range索引的范围查找(>=、<、in 等)
index全索引扫描。
all全表扫描
  • possible_keys:查询时可能使用的索引
  • key:实际使用的索引
  • key_len:使用的索引字段的长度
  • ref:列与索引的比较
  • rows:预计要检查的行数
  • filtered:按表条件过滤的行百分比
  • extra:附加信息
extra描述
Using index使用覆盖索引
Using where使用了where子句来过滤结果集
Using filesort使用文件排序。使用非索引列排序时,非常消耗性能
Using temporary使用临时表

参考:
Mysql面经
explain有哪些字段,分别有什么含义

如何做慢SQL优化
分析原因:是查询条件没有命中索引?还是加载了不需要的字段?还是数据量太大?
优化步骤

  1. 先用 explain 分析SQL的执行计划,查看使用索引的情况。
  2. 分析SQL语句或重写,检查是否存在一些导致索引失效的用法,是否加载了额外的数据,是否加载了许多结果中并不需要的字段。
  3. 若对语句的优化无法进行,可以考虑表中的数据量是否太大,若是,可以垂直拆分或水平拆分。

3、分表分库

  • 垂直拆分
    在数据库里将表按照不同的业务属性,拆分到不同库中,就是垂直拆分;比如会员数据库、订单数据库、支付数据库、消息数据库等,垂直拆分在大型电商项目中使用比较常见。
    优点:拆分后业务清晰,拆分规则明确,系统之间整合或扩展更加容易。
    缺点:部分业务表无法join,跨数据库查询比较繁琐(必须通过接口形式通讯(http+json))、会产生分布式事务的问题,提高了系统的复杂度。

  • 水平拆分:
    -把同一张表中的数据拆分到不同的数据库中进行存储、或者把一张表拆分成 n 多张小表。相对于垂直拆分,水平拆分不是将表的数据做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中,主要有分表(分布到多个表),分库(分布到多个数据库)两种模式。该方式提高了系统的稳定性跟负载能力,但是跨库join性能较差。

6、SQL生命周期

1、应用服务器与数据库服务器建立连接
2、数据库进行拿到请求SQL
3、解析并生成执行计划,执行
4、读取数据到内存并进行逻辑处理
5、将结果发送给客户端
6、关闭连接,释放资源

7、查询执行流程

1、客户端发送一条查询给服务器;
2、服务器先查询缓存,若命中了缓存则立即返回结果,否则进入下一阶段;
3、服务器进行SQL解析、预处理,再由优化器生成对象的执行计划;
4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询;
5、最后将结果返回到客户端。
在这里插入图片描述

四、数据库事务

1、三大范式

第一范式:所有的字段都是不可在分隔的(列不可再分)

第二范式:满足1NF的前提下,表必须有一个主键列,并且所有的非主键列都必须完全依赖于主键列

第三范式:满足2NF的前提下,消除了传递依赖,也就是说所有的非主键列都直接依赖主键列,不依赖其他非主键。

2、视图

视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的 sql 操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。

创建视图:create view xxx as xxxx

对于某些视图比如未使用联结子查询分组聚集函数Distinct Union(去重)等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。

3、事务

事务是一组SQL语句要么执行都成功,要么执行都失败。

4、事务的特性

原子性:一个事务不可分割。要么都执行,要么都回滚。

一致性:一个事务提交前和提交后的数据 状态 必须保持一致,比如存的钱余额必须大于0。

隔离性:多个事务之间是相互隔离的,相互独立的,事务A不能干扰事务B。

持久性:事务提交后,数据会持久化存储在数据库中。

5、事务并发带来的问题

脏读:事务A读到了事务B未提交的数据。

不可重复读:事务A多次读数据,事务B修改数据,事务A读到了事务B修改的数据,导致两次读到的数据不一致。

幻读:事务A读取数据,事务B插入数据,事务A读取到表中原本没有的数据。

6、数据库的隔离级别

未提交读(Read Uncommitted):数据在事务中发生了修改,即使没有提交,其他事务也是可见的,可能会导致脏读、不可重复读、幻读。比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100

已提交读(Read Commmitted):对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的。Oracle数据库默认隔离级别。比如对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了

可重复读(Repeated Read):同一事务对同一字段的多次读取结果都是一致的,除非数据是被本身事务所修改。该隔离级别还存在幻读。InnoDB默认级别。SQL标准中规定的RR级别并不能消除幻读,但MySQL的RR级别可以,靠的Next-Key Lock 锁。

串行化(Serializable):每次读都需要获得表级共享锁,读写相互都会阻塞。

PS:Next-Key Locks 是记录锁(record lock)与间隙锁(gap lock)的结合。记录锁是行级别的锁(row-level locks),当InnoDB 对索引进行搜索或扫描时,
会在索引对应的记录上设置共享或排他的记录锁,同时还会锁定此记录之前的”间隙“(gap lock)。

如果一个会话获取了索引对应记录 R 上的共享或排他锁,则另一个会话不能在R 之前(按索引排序)的间隙中插入新的记录。

7、MVCC

1、MVCC的定义

MySQL RR 级别(可重复读)是通过MVCC多版本并发控制实现的。

MVCC是多版本并发控制,它通过管理数据行的多个版本来实现数据库的并发控制。通过比较版本号来决定数据是否显示,读取数据时不需要加锁也能保证事务的隔离效果。在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

MVCC多版本并发控制,在很多情况下避免加锁,大都实现了非阻塞的读操作,写操作也只锁定必要的行。InnoDB在每行记录后面保存两个隐藏列,分别是创建版本号和删除版本号。每开始一个新的事务系统版本号都会递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。在RR级别下,MVCC的工作方式:

  • select:必须同时满足以下两个条件的数据行记录才会显示(数据库中会存在多个该行的记录)。
    只查版本号早于当前版本的数据行,用于保证在select操作之前所有的操作已经执行
    行的删除版本要么未定义,要么大于当前事务版本号,删除版本号大于当前版本意味着有一个并发事务将该行删除了
    在这里插入图片描述

  • insert:为插入的每一行记录保存当前系统版本号作为创建版本号。

  • delete:为删除的每一行记录保存当前系统版本号作为删除版本号。

  • update:插入一条新记录,(分两步,删除+插入)保存当前系统版本号作为创建版本号,同时保存当前系统版本号作为原来的数据行的删除版本号。

2、MVCC的工作原理

事实上,上述的说法(创建版本、删除版本)只是简化版的理解,真正的MVCC用于读已提交和可重复读级别的控制,主要通过undo log日志版本链和read view来实现。每条数据隐藏的两个字段也并不是创建时间版本号和过期(删除)时间版本号,而是roll_pointer和trx_id。

它的实现原理主要是版本链,undo日志 ,Read View来实现的,数据库中的每行数据之后还有几个隐藏字段,分别是trx_iddb_roll_pointer

  • trx_id,6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
  • roll_pointer(版本链关键),7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)

trx_id是当前操作该记录的事务ID,而roll_pointer是一个回滚指针,用于配合undo日志,指向上一个旧版本。每次对数据库记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表。
在这里插入图片描述

3、Read View

事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。

记录并维护系统当前活跃事务的ID(trx_id)的列表(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。

Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View几个属性

  • trx_ids: 当前系统活跃(未提交)事务版本号集合。
  • low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
  • up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
  • creator_trx_id: 创建当前read view的事务版本号;

Read View可见性判断条件

(1)trx_id < up_limit_id || trx_id == creator_trx_id(显示)

如果数据事务ID(trx_id)小于read view中的最小活跃事务ID(up_limit_id),则可以肯定该数据是在当前事务启之前就已经存在了的,所以对于当前事务可以显示。

或者数据的事务ID(trx_id)等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

(2)trx_id >= low_limit_id(不显示)

如果数据事务ID(trx_id)大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据对于当前事务不显示。此时通过roll_pointer取上一版本重新对比,以此类推。

(3)如果trx_id < low_limit_id则进入下一个判断,判断trx_id是否在活跃事务(trx_ids)中

不存在:则说明read view产生的时候事务已经commit了,这种情况数据对于当前事务则可以显示。
已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,当前事务也是看不见的。

MVCC简介
MVCC详解
MySQL秋招面经
MySQL面经
关于MVCC的理解

4、快照读和当前读

快照读:生成一个事务快照(ReadView),之后都从这个快照获取数据。普通 select 语句就是快照读。读取数据的可能不是最新版本,有可能是之前的历史版本。

  • 不加锁的select操作(注:事务级别不是串行化)

当前读:读取数据的最新版本。会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。

  • select ······ lock in share mode (共享锁)
  • select ······ for update (排他锁)
  • update (排他锁)
  • insert (排他锁)
  • delete (排他锁)
  • 串行化事务隔离级别
  • 对于快照读,因为MVCC是从ReadView读取,所以必然不会看到新插入的记录,所以是可以解决幻读问题的。
  • 对于当前读,则MVCC是无法解决的。需要使用间隙锁(Gap Lock)或Next-Key Lock来解决。
  • 注意: MVCC只能解决快照读下的幻读问题。
  • 注意:在RR级别下,快照读是通过MVCC和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。

五、锁

1、乐观锁与悲观锁

乐观锁 认为一个用户读数据的时候,其他用户不会去写自己所读的数据。乐观锁一般会使用版本号机制或CAS算法实现。乐观锁适合多读少写的场景。

悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。悲观锁比较适合多写少读的场景。

2、共享锁和排他锁

共享锁 又称读锁,简称S锁:当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避兔出现重复读的问题。

排他锁 又称写锁,简称X锁:当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。

3、其他锁

1、表锁
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问
特点:粒度大,加锁简单,容易冲突,不会出现死锁

2、行锁
行锁是指上锁的时候锁住的是表的某一行或多条记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高,会出现死锁

行锁的实现需要注意:

  • 行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
  • 如果是共享锁,两个事务可以锁同一个索引,排它锁则不能。
  • insert,delete,update在事务中都会自动默认加上排它锁。

3、记录锁
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录,实现精准条件命中,并且命中的条件字段是唯一索引。加了记录锁之后数据可以避免数据在査询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。

4、页锁
页级锁是Mysql中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速慢;所以取了折衷的页级,一次锁定相邻的一组记录。特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

5、间隙锁
属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成个区间,遵循左开右闭原则

6、临建锁
也属于行锁的一种(Next-key Lock),并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把査询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。结合记录锁和间隙锁的特性,临键锁避免了在范围査询时岀现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入

4、意向锁

如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。

意向共享锁 当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。
意向排他锁 当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。

5、时间戳

时间戳就是在数据库表中单独加1列作为时间戳列。每次读取数据时,把该字段也读出来,当写数据时,把该字段加1,提交前跟数据库的字段比较1次,如果比数据库的值大,就允许保存,否则就不允许保存。

6、并发策略

并发控制一般有三种方法,分别是乐观锁、悲观锁、时间戳

六、日志

MySQL中存在着以下几种日志:重写日志(redo log)、回滚日志(undo log)、归档日志(二进制日志、bin log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)

1、bin long 归档日志

MySQL的bin log日志是用来记录MySQL中增删改时的记录日志。简单来讲,就是当你的一条sql操作对数据库中的内容进行了更新,就会增加一条bin log日志。查询操作不会记录到bin log中。bin log最大的用处就是进行主从复制,以及数据库的恢复属于 MySQL Server 层的日志。

查看bin log 日志
show VARIABLES like '%log_bin%'

开启bin log
log-bin=mysql-bin
server-id=1
binlog_format=ROW

其中log-bin指定日志文件的名称,默认会放到数据库目录下,可通过以下命令查看
show VARIABLES like '%datadir%'

2、redo log 重写日志

redo log是一种基于磁盘的数据结构,用来在MySQL宕机情况下将不完整的事务执行数据纠正,redo日志记录事务执行后的状态。

当事务开始后,redo log就开始产生,并且随着事务的执行不断写入redo log file中。redo log file中记录了xxx页做了xx修改的信息,我们都知道数据库的更新操作会在内存中先执行,最后刷入磁盘。redo log就是为了恢复更新了内存但是由于宕机等原因没有刷入磁盘中的那部分数据。

InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是1GB ,那么这块 “ 粉板 ” 总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
在这里插入图片描述
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos 和 checkpoint 之间的是 “ 粉板 ” 上还空着的部分,可以用来记录新的操作。如果 write pos追上 checkpoint ,表示 “ 粉板 ” 满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint 推进一下。

有了 redo log , InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe 。redo log 是 InnoDB 引擎特有的。

bin log 和 redo log 两种日志有以下三点不同:
1、redo log 是 InnoDB 引擎特有的; binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
2、redo log 是物理日志,记录的是 “ 在某个数据页上做了什么修改 ” ; binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如 “ 给ID=2 这一行的 c 字段加 1 ”,可以理解为SQL语句 。
3、redo log 是循环写的,空间固定会用完; binlog 是可以追加写入的。 “ 追加写 ” 是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

3、undo log 回滚日志

undo log主要用来回滚到某一个版本,是一种逻辑日志。undo log记录的是修改之前的数据,比如:当delete一条记录时,undolog中会记录一条对应的insert记录,从而保证能恢复到数据修改之前。在执行事务回滚的时候,就可以通过undo log中的记录内容并以此进行回滚。

undo log还可以提供多版本并发控制下的读取(MVCC)。

4、error log 错误日志

error log主要记录MySQL在启动、关闭或者运行过程中的错误信息,在MySQL的配置文件my.cnf中,可以通过log-error=/var/log/mysqld.log 执行mysql错误日志的位置。

MySQL 命令获取到错误日志的位置
show variables like "%log_error%";

5、slow query log 慢查询日志

慢查询日志用来记录执行时间超过指定阈值的SQL语句,慢查询日志往往用于优化生产环境的SQL语句。

查看慢查询日志是否开启以及日志的位置:
show variables like "%slow_query%";

慢查询日志的常用配置参数如下:
1、slow_query_log=1 #是否开启慢查询日志,0关闭,1开启
2、slow_query_log_file=/usr/local/mysql/mysql-8.0.20/data/slow-log.log #慢查询日志地址(5.6及以上版本)
3、long_query_time=1 #慢查询日志阈值,指超过阈值时间的SQL会被记录
4、log_queries_not_using_indexes #表示未走索引的SQL也会被记录

分析慢查询日志一般会用专门的日志分析工具。找出慢SQL后可以通过explain关键字进行SQL分析,找出慢的原因。

6、general log 一般查询日志

general log 记录了客户端连接信息以及执行的SQL语句信息

查看是否开启以及日志的位置:
show variables like '%general_log%';

general log 可通过配置文件启动,配置参数如下:
1、general_log = on
2、general_log_file = /usr/local/mysql/mysql-8.0.20/data/hecs-78422.log

普通查询日志会记录增删改查的信息,一般是关闭的。

七、主从同步

MySQL主从同步中主要有三个线程:Master一条线程:bin log dump thread、以及Slave两条线程:I/O thread、sql thread。

主节点的bin log 日志文件,是数据库服务启动时用于保存所有修改数据库结构或内容的文件,主从复制的基础是主库记录所有的变更存储到 bin log 文件中,且bin log发生变动,主节点的bin log dump thread就会读取其内容并发送到从节点,然后从节点I/O线程接收到bin log的内容后,将其写入到relay log文件中,最后从节点的sql线程读取relay log 文件的内容并对数据更新进行重放,保证主从数据库的一致性。

注意:主节点使用bin log 日志文件+ pososition 偏移量来定位主从同步的位置,从节点会保存其已经接收到的便宜量,如果从节点发生宕机,则会自动从 pososition 偏移量的位置发起同步。(增量同步)

mysql默认的复制方式是异步的,主库把日志文件发送给从库后不关心从库是否已经处理,在这种情况下,假设主库挂了,从库处理失败,此时从库升为主库后,日志就丢失了。由此产生两个概念:

  • 全同步复制
    主库写入bin log后强制同步日志到从库,所有从库都执行完后才返回给主库确认信息,该方式性能会受到严重的影响。

  • 半同步复制
    不同步不需要等待所有从库都执行完后才返回给主库确认信息,而是从库执行完后返回ACK确认给主库,主库收到至少一个从库的确认就认为操作完成。

八、场景题

1、数据库高并发是我们经常会遇到的,你有什么好的解决方案吗?

  1. 在web服务框架中加入缓存。在服务器与数据库层之间加入缓存层,将高频访问的数据存入缓存中,减少数据库的读取负担。
  2. 增加数据库索引,进而提高查询速度。(不过索引太多会导致速度变慢,并且数据库的写入会导致索引的更新,也会导致速度变慢)
  3. 主从读写分离,让主服务器负责写,从服务器负责读。
  4. 将数据库进行拆分,使得数据库的表尽可能小,提高查询的速度。
  5. 使用分布式架构,分散计算压力。

2、假如你所在的公司选择MySQL数据库作数据存储,一天五万条以上的增量,预计运维三年,你有哪些优化手段?

  1. 设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。
  2. 选择合适的表字段数据类型和存储引擎,适当的添加索引。
  3. MySQL库主从读写分离。
  4. 找规律分表,减少单表中的数据量提高查询速度。
  5. 添加缓存机制,比如Memcached,Apc等。
  6. 不经常改动的页面,生成静态页面。
  7. 书写高效率的SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE

总结

学习和参考的是这些链接,中间加了我自己个人的理解。
MySQL面经汇总
阿秀笔记
Mysql秋招
MySQL每日面经
解析MySQL六种日志

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值