这里先开始MySQL篇章吧,只写一些重点了,时间比较仓促,细节后面补充。
MySQL是典型的关系型数据库,通过将世界里的事物、关系抽象成关系模型并用二维表格存储。其优点包括:
由于其数据存储在硬盘当中,其读写性能相对较差,尤其面对高并发时性能往往较差。
MySQL常见的存储引擎包括:innodb、myisam、memory、merge等。
innodb:行级锁,
Innodb引擎提供了对数据库ACID事务的⽀持,并且实现了SQL标准的四种隔离级别,该引擎还提供了⾏级锁和外键约束。由于锁的粒度更⼩,写操作不会锁定全表,所以在并发较⾼时,使⽤Innodb引擎会提升效率。但是使⽤⾏级锁也不是绝对的,如果在执⾏⼀个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。
myisam:全表锁,
是它没有提供对数据库事务的⽀持,也不⽀持⾏级锁和外键,因此当INSERT(插⼊)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低⼀些,并发性能差,但其占用空间相对较小。
memory:全表锁
,存储在内存,占用空间是和存储数据成正比,系统重启后数据就消失了,默认使用Hash索引,检索效率非常高。
merge:myisam表的组合。
innodb 和 myisam的区别,参考链接。
innodb | myisam | |
---|---|---|
事务 | 支持 | 不支持 |
锁 | 行级锁 | 全表锁 |
缓存 | 缓存索引、数据 | 只缓存索引 |
主键 | 必须有,用于实现聚簇索引 | 可以没有 |
索引 | B+树,主键是聚簇索引 | B+树,主键是非聚簇索引 |
select count(*) from table | 扫描全表 | 用一个变量保存了表的行数 |
hash索引 | 支持 | 不支持 |
存储顺序 | 按主键大小顺序插入 | 按记录插入顺序保存 |
外键 | 支持 | 不支持 |
全文索引 | 5.7 支持 | 支持 |
关注点 | 事务 | 性能 |
索引是辅助索引(普通索引);索引不是唯一的。
**原因**
:MySQL的数据页默认是16K,而文件系统的数据页是4K,IO操作是按页为单位就行读写的。这就可能出现数据库对一个16k的数据页修改后,操作系统开始进行写磁盘,但是在这个过程中数据库宕机导致没有完全将16K数据页写到磁盘上。数据库重启后,校验数据页,发现有数据页不完整,就起不来了(redo是基于完整数据页进行的恢复),原文链接**解决方案**
:为了解决这个问题,MySQL引入了double write这个特性。double write针对的是脏数据,提高innodb的可靠性,用来解决部分写失败(partial page write)。为了数据的持久性,脏数据需要刷新到磁盘上,而double write就产生在将脏数据刷盘的过程中。刷盘是一份脏数据写到共享表空间ibdata中,一份写到真正的数据文件永久的保存。写了两次脏数据,就叫double wriete。**流程**
:
,如果观察到某些索引被频繁访问,索引成为热数据
,并为其建立哈希索引。自适应哈希索引通过缓冲池的 B+ 树构造而来,因此建立的速度很快。而且不需要将整个表都建哈希索引,InnoDB 会自动根据访问的频率和模式来为某些页建立哈希索引。自适应哈希索引功能由innodb_adaptive_hash_index
变量启用,或在服务器启动时由--skip-innodb-adaptive-hash-index
禁用。**线性预读(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。数据库索引是数据库管理系统的一种排序的数据结构
,索引通常由B+树
(也有散列索引、位图索引)实现,而且索引是一种文件,占用物理空间。
优点:
缺点:
类型 | 作用 |
---|---|
Primary Key 主键索引(聚簇索引) | 一个表只能有一个主键(字段),列中的值不允许重复,不允许为NULL |
Compound Index 组合索引 | 组合索引是在多个字段上创建的索引,需要遵守最左前缀原则,即在查询条件中,只有使用了组合索引中的第一个字段,索引才会使用 |
Unique 唯一索引 | 列允许为NULL,不允许重复。一个表允许多个列创建唯一索引。若是组合索引,则(字段组合的) 列值的组合必须唯一 |
Key 普通索引 | 基本索引类型,没有唯一性的限制,允许值为NULL |
FullText 全文索引 | 主要用来查找文本中的关键字,而不是直接与索引中的值相比较(InnoDB中不支持使用全文索引) |
Spatial 空间索引 | 空间索引的数据结构是R树,R树实际上就是多维的B树 |
且每张表只会存在一个
。通常是主键,若没有主键,则优先选择非空的唯一索引,若唯一索引也没有,则会创建一个隐藏的自增的 row_id 作为聚簇索引。聚集索引(clustered index)就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,聚集索引的存储并不是物理上连续的,而是逻辑上连续的。最左前缀集合
。// 组合索引
alter table 表名 add index 索引名(字段名1,字段名2,字段3)
建立这样的索引,其实相当于建立了三个索引: 原因:组合索引“最左前缀”
- 字段1,字段2,字段3
- 字段1,字段2
- 字段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.**模糊查询中,后模糊匹配才能让索引有效
like %keyword 索引失效
,使用全表扫描。但可以通过翻转函数(reverse)+like前模糊查询+建立翻转函数索引=走翻转函数索引
,不走全表扫描。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');
索引并非越多越好
,一个表中如果有大量的索引,不仅占用磁盘空间,而且会影响INSERT、DELETE、UPDATE等语句的性能,因为在表中的数据更改的同时,索引也会进行调整和更新。避免对经常更新的表进行过多的索引
,并且索引中的列尽可能少。而对经常用于查询的字段应该创建索引,但要避免添加不必要的字段。数据量小的表最好不要使用索引
,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。不同值较多的列上建立索引
,在不同值很少的列上不要建立索引
。比如在学生表的“性别”字段上只有“男”与“女”两个不同值,因此就无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度。唯一性是某种数据本身的特征时,指定唯一索引
。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。频繁进行排序或分组(即进行group by或order by、count操作)的列上建立索引
,如果待排序的列有多个,可以在这些列上建立组合索引。最适合索引的列是出现在WHERE子句中的列,或连接子句中指定的列
,而不是出现在SELECT关键字后的选择列表中的列。 SELECT t.Name FROM mytable t LEFT JOIN mytable m ON t.Name=m.username WHERE m.age=20 AND m.city='郑州'
使用短索引
。如果对字符串列进行索引,应该指定一个前缀长度,只要有可能就应该这样做。
利用最左前缀
。在创建一个n列的索引时,实际是创建了MySQL可利用的n个索引。多列索引可起几个索引的作用,因为可利用索引中最左边的列集来匹配行。这样的列集称为最左前缀。
定义有外键的数据列一定要建立索引。
对于定义为text、image和bit的数据类型的列不要建立索引。
PS:若查询的字段都建立了索引,那么引擎会直接在索引表中查询而不会访问原始数据,这叫索引覆盖。否则只要有一个字段没有建立索引就会做全表扫描。
MySQL中使用较多的索引有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现为:B+树索引。哈希索引底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快。
- n棵子tree的节点包含n个关键字,用来保存数据的索引。
- 非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
- B+ 树中,数据对象的插入和删除仅在叶节点上进行。
- 叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶子节点。
哈希索引基于哈希表实现,只有精确匹配索引所有列查询时才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希索引将哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。索引自身只需存储对应的哈希值,所以索引结构十分紧凑,这让哈希索引的速度非常快。只能用于查找
限制:
索引的原理就是把无序的数据变成有序的查询。
删除索引的过程:
在InnoDB中,对于主键索引,只需要跑一遍主键索引的查询就能获取叶子节点的数据。对于普通索引,叶子节点存储的是 key + 主键值,所以还需要跑一遍主键索引的查询才能找到数据行,这就是回表查询,先定位主键值,再定位数据行。
但不是所有的普通索引一定会出现回表查询,若查询SQL所要求的字段全部命中索引,那就不用进行回表查询。比如有一个user表,主键为id,name是个普通索引,执行SQL:select id,name from user where name='aitao'
时,通过name的索引就可以获取到id和name数据,所以无需回表查询数据行。
数值类型:
类型 | 大小(Byte) | 范围(有符号) | 范围(无符号) | 用途 |
---|---|---|---|---|
TINYINT | 1 Bytes | (-128,127) | (0,255) | 小整数值 |
SMALLINT | 2 Bytes | (-32 768,32 767) | (0,65 535) | 大整数值 |
MEDIUMINT | 3 Bytes | (-8 388 608,8 388 607) | (0,16 777 215) | 大整数值 |
INT或INTEGER | 4 Bytes | (-2 147 483 648,2 147 483 647) | (0,4 294 967 295) | 大整数值 |
BIGINT | 8 Bytes | (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) | (0,18 446 744 073 709 551 615) | 极大整数值 |
FLOAT | 4 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) | 单精度( 浮点数值) |
DOUBLE | 8 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) | 范围 | 格式 | 用途 |
---|---|---|---|---|
DATE | 3 | 1000-01-01/9999-12-31 | YYYY-MM-DD | 日期值 |
TIME | 3 | ‘-838:59:59’/‘838:59:59’ | HH:MM:SS | 时间值或持续时间 |
YEAR | 1 | 1901/2155 | YYYY | 年份值 |
DATETIME | 8 | ‘1000-01-01 00:00:00’ 到 ‘9999-12-31 23:59:59’ | YYYY-MM-DD hh:mm:ss | 混合日期和时间值 |
TIMESTAMP | 4 | ‘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:07 | YYYY-MM-DD hh:mm:ss | 混合日期和时间值,时间戳 |
字符串类型:
类型 | 大小(Byte) | 用途 |
---|---|---|
CHAR | 0-255 bytes | 定长字符串 |
VARCHAR | 0-65535 bytes | 变长字符串 |
TINYBLOB | 0-255 bytes | 不超过 255 个字符的二进制字符串 |
TINYTEXT | 0-255 bytes | 短文本字符串 |
BLOB | 0-65 535 bytes | 二进制形式的长文本数据 |
TEXT | 0-65 535 bytes | 长文本数据 |
MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 |
MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 |
LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 |
LONGTEXT | 0-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字节。
SQL注入就是通过把SQL命令插入到Http请求中,达到欺骗服务器执行恶意的SQL命令。
应对方法:
- delete和truncate只删除表的数据不删除表的结构;
- delete语句是DML(数据库操作语言)操作,事务提交后才会生效;若有相应的触发器(trigger),执行时会被触发;
- truncate和drop是DDL(数据库定义语言)操作,操作立即生效,不能回滚,不触发触发器(trigger)
- SQL 执行速度: drop > truncate > delete
- truncate删除表中的所有数据,表结构还在;drop删除表,表结构不在
索引失效的情况:
参考阿秀笔记对于数据库的查询,尽量使用缓存:
1、减少使用join,join过多在对SQL进行慢优化的时候很难发现怎么优化;替代方案是把数据库加载到缓存中,在缓存中合并数据
2、减少数据库的冗余
3、使用in (exist)代替join
4、使用es宽表,背后机制缓存机制的使用
select_type | 描述 |
---|---|
SIMPLE | 不包含任何子查询或union等查询 |
PRIMARY | |
SUBQUERY | 在select或where子句中包含的查询 |
DERIVED | from子句中包含的查询 |
UNION | 出现在union后的查询 |
UNION RESULT | 从union中获取结果集 |
type | 描述 |
---|---|
const | 通过主键或唯一键查询,并且结果只有1行(也就是用等号查询) |
eq_ref | 通常出现于两表关联查询时,使用主键或非空唯一键关联,并且查询条件不是主键或唯一键的等号查询 |
ref | 通过普通索引查询,并且使用的等号查询 |
range | 索引的范围查找(>=、<、in 等) |
index | 全索引扫描。 |
all | 全表扫描 |
extra | 描述 |
---|---|
Using index | 使用覆盖索引 |
Using where | 使用了where子句来过滤结果集 |
Using filesort | 使用文件排序。使用非索引列排序时,非常消耗性能 |
Using temporary | 使用临时表 |
参考:
Mysql面经
explain有哪些字段,分别有什么含义
如何做慢SQL优化
分析原因:是查询条件没有命中索引?还是加载了不需要的字段?还是数据量太大?
优化步骤:
垂直拆分
在数据库里将表按照不同的业务属性,拆分到不同库中,就是垂直拆分;比如会员数据库、订单数据库、支付数据库、消息数据库等,垂直拆分在大型电商项目中使用比较常见。
优点:拆分后业务清晰,拆分规则明确,系统之间整合或扩展更加容易。
缺点:部分业务表无法join,跨数据库查询比较繁琐(必须通过接口形式通讯(http+json))、会产生分布式事务的问题,提高了系统的复杂度。
水平拆分:
-把同一张表中的数据拆分到不同的数据库中进行存储、或者把一张表拆分成 n 多张小表。相对于垂直拆分,水平拆分不是将表的数据做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中,主要有分表(分布到多个表),分库(分布到多个数据库)两种模式。该方式提高了系统的稳定性跟负载能力,但是跨库join性能较差。
1、应用服务器与数据库服务器建立连接
2、数据库进行拿到请求SQL
3、解析并生成执行计划,执行
4、读取数据到内存并进行逻辑处理
5、将结果发送给客户端
6、关闭连接,释放资源
1、客户端发送一条查询给服务器;
2、服务器先查询缓存,若命中了缓存则立即返回结果,否则进入下一阶段;
3、服务器进行SQL解析、预处理,再由优化器生成对象的执行计划;
4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询;
5、最后将结果返回到客户端。
第一范式:所有的字段都是不可在分隔的(列不可再分)
第二范式:满足1NF的前提下,表必须有一个主键列,并且所有的非主键列都必须完全依赖于主键列
第三范式:满足2NF的前提下,消除了传递依赖,也就是说所有的非主键列都直接依赖主键列,不依赖其他非主键。
视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的 sql 操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。
视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。
创建视图:create view xxx as xxxx
对于某些视图比如未使用联结子查询分组聚集函数Distinct Union(去重)等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。
事务是一组SQL语句要么执行都成功,要么执行都失败。
原子性:一个事务不可分割。要么都执行,要么都回滚。
一致性:一个事务提交前和提交后的数据 状态 必须保持一致,比如存的钱余额必须大于0。
隔离性:多个事务之间是相互隔离的,相互独立的,事务A不能干扰事务B。
持久性:事务提交后,数据会持久化存储在数据库中。
脏读:事务A读到了事务B未提交的数据。
不可重复读:事务A多次读数据,事务B修改数据,事务A读到了事务B修改的数据,导致两次读到的数据不一致。
幻读:事务A读取数据,事务B插入数据,事务A读取到表中原本没有的数据。
未提交读(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 之前(按索引排序)的间隙中插入新的记录。
MySQL RR 级别(可重复读)是通过MVCC多版本并发控制实现的。
MVCC是多版本并发控制,它通过管理数据行的多个版本来实现数据库的并发控制。通过比较版本号来决定数据是否显示,读取数据时不需要加锁也能保证事务的隔离效果。在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
MVCC多版本并发控制,在很多情况下避免加锁,大都实现了非阻塞的读操作,写操作也只锁定必要的行。InnoDB在每行记录后面保存两个隐藏列,分别是创建版本号和删除版本号。每开始一个新的事务系统版本号都会递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。在RR级别下,MVCC的工作方式:
select:必须同时满足以下两个条件的数据行记录才会显示(数据库中会存在多个该行的记录)。
只查版本号早于当前版本的数据行,用于保证在select操作之前所有的操作已经执行
;
行的删除版本要么未定义,要么大于当前事务版本号,删除版本号大于当前版本意味着有一个并发事务将该行删除了
。
insert:为插入的每一行记录保存当前系统版本号作为创建版本号。
delete:为删除的每一行记录保存当前系统版本号作为删除版本号。
update:插入一条新记录,(分两步,删除+插入)保存当前系统版本号作为创建版本号,同时保存当前系统版本号作为原来的数据行的删除版本号。
事实上,上述的说法(创建版本、删除版本)只是简化版的理解
,真正的MVCC用于读已提交和可重复读级别的控制,主要通过undo log日志版本链和read view来实现。每条数据隐藏的两个字段也并不是创建时间版本号和过期(删除)时间版本号,而是roll_pointer和trx_id。
它的实现原理主要是版本链,undo日志 ,Read View来实现的,数据库中的每行数据之后还有几个隐藏字段,分别是trx_id
、db_roll_pointer
。
trx_id是当前操作该记录的事务ID,而roll_pointer是一个回滚指针,用于配合undo日志,指向上一个旧版本。每次对数据库记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表。
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
记录并维护系统当前活跃事务的ID(trx_id)的列表(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。
Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
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的理解
快照读:生成一个事务快照(ReadView),之后都从这个快照获取数据。普通 select 语句就是快照读。读取数据的可能不是最新版本,有可能是之前的历史版本。
当前读:读取数据的最新版本。会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。
- 对于快照读,因为MVCC是从ReadView读取,所以必然不会看到新插入的记录,所以是可以解决幻读问题的。
- 对于当前读,则MVCC是无法解决的。需要使用间隙锁(Gap Lock)或Next-Key Lock来解决。
- 注意: MVCC只能解决快照读下的幻读问题。
- 注意:在RR级别下,快照读是通过MVCC和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
乐观锁 认为一个用户读数据的时候,其他用户不会去写自己所读的数据。乐观锁一般会使用版本号机制或CAS算法实现。乐观锁适合多读少写的场景。
悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。悲观锁比较适合多写少读的场景。
共享锁 又称读锁,简称S锁:当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避兔出现重复读的问题。
排他锁 又称写锁,简称X锁:当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。
1、表锁
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问
特点:粒度大,加锁简单,容易冲突,不会出现死锁
2、行锁
行锁是指上锁的时候锁住的是表的某一行或多条记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高,会出现死锁
行锁的实现需要注意:
3、记录锁
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录,实现精准条件命中,并且命中的条件字段是唯一索引。加了记录锁之后数据可以避免数据在査询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
4、页锁
页级锁是Mysql中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速慢;所以取了折衷的页级,一次锁定相邻的一组记录。特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
5、间隙锁
属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成个区间,遵循左开右闭原则
6、临建锁
也属于行锁的一种(Next-key Lock),并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把査询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。结合记录锁和间隙锁的特性,临键锁避免了在范围査询时岀现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入
如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。
意向共享锁 当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。
意向排他锁 当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。
时间戳就是在数据库表中单独加1列作为时间戳列。每次读取数据时,把该字段也读出来,当写数据时,把该字段加1,提交前跟数据库的字段比较1次,如果比数据库的值大,就允许保存,否则就不允许保存。
并发控制一般有三种方法,分别是乐观锁、悲观锁、时间戳
MySQL中存在着以下几种日志:重写日志(redo log)、回滚日志(undo log)、归档日志(二进制日志、bin log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)
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%'
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 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
undo log主要用来回滚到某一个版本,是一种逻辑日志。undo log记录的是修改之前的数据,比如:当delete一条记录时,undolog中会记录一条对应的insert记录,从而保证能恢复到数据修改之前。在执行事务回滚的时候,就可以通过undo log中的记录内容并以此进行回滚。
undo log还可以提供多版本并发控制下的读取(MVCC)。
error log主要记录MySQL在启动、关闭或者运行过程中的错误信息,在MySQL的配置文件my.cnf
中,可以通过log-error=/var/log/mysqld.log
执行mysql错误日志的位置。
MySQL 命令获取到错误日志的位置
show variables like "%log_error%";
慢查询日志用来记录执行时间超过指定阈值的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分析,找出慢的原因。
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确认给主库,主库收到至少一个从库的确认就认为操作完成。
学习和参考的是这些链接,中间加了我自己个人的理解。
MySQL面经汇总
阿秀笔记
Mysql秋招
MySQL每日面经
解析MySQL六种日志