一文搞懂MySQL的基本常识和工作原理

MySQL总结

一、存储引擎之间的区别

  • InnoDB

    1. 支持事务
    2. 并发高,支持行锁,间隙锁
    3. 支持全文索引、B+树索引
    4. 空间利用率高
  • MyISAM:

    1. 不支持事务
    2. 并发低,表锁
    3. 只支持全文索引、B+树索引
    4. 空间使用率低

二、MySQL表设计

2.1 MySQL 三范式:

  • 每个字段是原子的,不可再分
  • 每个表都应该有一个业务主键,业务非主键依赖于业务主键
  • 非业务主键之外的键不能互相依赖

2.2 建议

  • 尽量控制数据行数在2000w行数以下,保证每次查询都能命中索引,并及时做好冷备
  • 选择适当的数据类型,使用最小的数据类型,字段越大,建立索引时需要的空间也就越大
  • 避免使用TEXT、BLOB数据类型
  • 创建业务主键,如果业务主键无法定义则定义ID为主键
  • 尽可能把所有列定义为NUT NULL,然后特殊情况通过default来进行占用
  • 每张表上的索引数量不超过5个,索引也是占用磁盘空间的
  • 定期备份和恢复测试

三、MySQL 索引

索引分类:

  • 聚簇索引:非叶子节点不存储数据,只有叶子节点存储数据,可以更好的支持范围查询

    • B+树索引
  • 非聚簇索引:非叶子节点也存储数据

    • 全文索引
    • 唯一索引
    • 联合索引

3.1 B+树工作原理

通常我们都知道,操作系统在读取内存中的一个数据块时采用了局部性原理,通常会将要读取的块的附近的4KB的内容加载到磁盘中(连续内存中,如果第一个字节的内存要被应用程序使用到,那么会将其连续的4KB内存加载到内存中),而不是一次一次的加载,如果这样的话效率太低。而MySQL则通过针对每16KB大小来进行作为页来进行B+树的一个节点,所以一个节点大小为16384字节大小。我们在理解了MySQL和Page和操作系统的Page之间的关系后,我们通过主键索引来具体了解一下MySQL是如何利用B+树索引的。

这里我们直接给出B+树索引结构图: Root默认为16KB

MySQL中我们知道,对于主键其占用8个字节,指针占用8个字节,所以一个Page(16K)可以索引 16384/(8+6) 个记录,其中,8为bigint作为主键时的大小,6为MySQL在InnoDB中的指针的大小。具体我们看下图:图片是从网上找的

image-20240311214956693

我们来具体计算一下100w行、2000w行的表分别需要执行几次IO才能获取到对应的数据。

Root Page 保存在内存中,用来做原始的引用,因为本身也就16K大小,也不是很占用空间等,16384/(8+6)= 1170,也就是说通过Root Page就可以索引到1170条记录,同理,如果一张表内的数据量不超过1170,那么就不需要进行IO。

当数据超过1170之后会怎么办呢?如果数据超过1170,那么InnoDB则会在Root Page下面建立二级索引,每一个指针都指向一个16KB大小的Page,在该大小的Page内又同样能保存1170条索引记录。那么以及索引就会有 1170*1170 = 1368900条记录,此时叶子节点就会有 1368900 个叶子节点,也就能够索引到这么多的数据量,当数据量刚好为100w时,此时会通过内存中的 RootPage来定位到二级索引的Page中,然后通过指针加载到内存中,此时会发生一次IO ,当加载到内存中会进行二分查找,精确定位到要查找ID的那个索引,然后根据select对应的字段判断是否需要回表 ,如果需要回表则会根据叶子节点索引对应的行指针去精确查询对应的记录,此时又回进行一次IO,所以100w行表的数据最多会进行2次IO就能够找到所有的行数据。

如果表数据2000w呢?如果表数据时2000w,那么当数据量大于 1368900 记录时,会建立三级索引,而三级索引大小为:1368900 * 1170 = 1601613000 记录,所以2000w行的表数据最多执行3次IO就可以获取到对应的数据,前提是命中索引,如果没有命中索引则需要全表扫描。

3.2 索引基础概念

3.2.1 索引覆盖、回表

在介绍B+树工作原理时,我们提到了一个 回表 的概念,当我们进行 select 时对应的列如果不存在索引中,那么当我们在索引结构(B+树)中遍历到叶子节点时,叶子节点只包含索引和该索引对应的行的指针,如果要查询的列不是索引,那么InnoDB会根据索引叶子节点对应的行指针去DB中查找到对应的行,也就是回表操作,将找到对应的行中要查找的列返回给客户端。

而索引覆盖就是在索引查找到叶子节点时要查找的列刚刚好是索引,那么本次查询就不需要去回表查询当前行的记录就叫做索引覆盖。

3.2.1 索引最左前缀匹配

当我们创建联合索引时,比如我们创建了ABC组合的一个索引,如果我们要查找ABC三个字段,那么就会用到该索引,如果我们要查找C,则不会用到该索引,其命中原理就是:如果联合索引的最左边的索引列没有命中那么该索引则不会命中。

可以这么理解,ABC组合索引,但是BC是A的二级索引,C是AB的三级索引类似,如果要让B索引生效,那么A索引必须生效,否则ABC索引就不生效。

3.4 索引分析

我们在日常工作开发中,经常会遇到索引失效的问题,但是我们仅仅只看SQL也看不出来问题,如果我们看SQL就能看出来问题,那么我们当初也就不会把SQL写错了,哈哈哈。

我们来具体分析一下 EXPLAIN 具体用法:

sql 复制代码
# 未使用索引
mysql> explain  select * from blogs_article;
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table         | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | blogs_article | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   80 |   100.00 | NULL  |
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-------+

# 使用索引
mysql> explain select id,title from blogs_article where id = 123;
+----+-------------+---------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table         | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+---------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | blogs_article | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+---------------+------------+-------+---------------+---------+---------+-------+------+----------+-------+

我们来具体分下一下各个字段的含义:

  • select_type:本次要查询的类型

  • table:本次查询的是那个表

  • partitions :是否分区?

  • type :表的访问类型

    • ALL:全表扫描
    • Index 使用索引
    • Range 使用索引进行范围查询
    • Ref 使用非唯一索引扫描
    • eq_ref 使用唯一索引进行等值匹配
    • PRIMARY 使用主键索引
  • possible_keys:可能使用到的索引

  • key:实际使用的索引

  • key_len:使用的索引长度(联合索引)

  • ref:索引的那一列被用了

  • rows:本次需要扫描的行数

  • extra:其他信息,Using Where、文件排序、临时表等。

3.3 索引失效

  • 当要用索引查询的列中使用了内置函数进行计算则会导致索引失效。原理是InnoDB中是通过列本来的值来构建索引列的,如果利用计算或者函数改变之后,那么这个列就变成了其他的值,可以认为变成了一个新的列,那么自然就不会利用索引了。
  • Or语句前后没有同时使用索引的话索引则会失效
  • Like中最前面使用了%则会导致索引失效
  • 联合索引中不满足最左前缀匹配则失效
  • 数据类型出现隐式转换,会使索引失效,从而全表扫描
  • 针对命令is null 或者 is not null 使用的字段没有设置 not null 时则会进行全表扫描
  • 在索引上使用not ,<>!= 时永远也不会用到索引
  • 当全表扫描比索引速度快时会索引失效

四、MySQL锁

我们主要分析行锁、间隙锁、NextKey Lock锁,意向锁有一点点不是很明白。

我们创建如下数据库: 其中id为唯一索引,b为辅助索引

sql 复制代码
create table t
(
    id int(11) not null auto_increment,
    b  int(11),
    key (b),
    primary key (id)
);

image-20240312222755990

4.1 Record Lock 行锁

InnoDB中,当我们遍历到某个索引时,并且要对该行索引进行update,此时会对该行加锁,如果当前索引是唯一索引,那么就会对当前行加索引。

现在我们模拟在事务A中让事务A对ID为1的行加行锁。然后通过事务B查看是否阻塞。

ini 复制代码
# 事务A
begin ;
select * from t where id = 1 for update ;
commit ;

# 事务B
begin ;
select * from t where id = 1 for update ;
commit ;

我们先执行事务A,然后不进行事务提交,然后执行事务B,我们可以看到其被Lock阻塞了。此时InnoDB加的是行锁。

image-20240312223012893

4.2 Gap Lock 间隙锁

InnoDB中,还有一个叫做Gap的间隙锁的概念,其锁住的并不是某一个具体索引行,而是某一个区间锁。例如:

sql 复制代码
begin ;
select * from t where id > 3 and id < 6 for update ;
commit ;

当我们执行上面的SQL,并且不提交时,此时InnoDB上锁的情况为:Gap Lock 间隙锁

image-20240312224922230

此时,当我们在另一个事物中执行下面的SQL时,我们就会发现查询ID为1的数据时不会被阻塞,然而查询ID为5的数据则会被阻塞住。造成这种原因的则只有时上面的事务A对区间 (3,6)加了间隙锁。

sql 复制代码
begin ;
select * from t where id = 1 for update ;
select * from t where id = 5 for update ;

image-20240312225721365

4.3 NextKey Lock

为了模拟NextKey Lock锁解决的问题,我们需要把id=4的数据删除掉。

NextKey Lock 是Record Lock 和 Gap Lock 两种锁的结合。当遍历到的索引是唯一索引时则会降级到 Record Lock。当遍历到的是辅助索引时,则会使用Gap Lock 以及 Record Lock 两种锁。

我们来看下面的这个例子:注意,这里我们是针对辅助索引b来上锁

sql 复制代码
begin;
select * from t where b = 5 for update;

当我们执行完上面的SQL之后,其会根据索引b=5来加Gap锁,同时其会前后都加Gap锁,并且针对b=5这一行数据的唯一索引id=5加Record Lock。具体加锁之后如下:

image-20240312232331438

之后我们在事务B中执行下面的SQL,我们就会发现确实被锁住了:在执行 select * from t where id = 5 for update; 时,我们会发现其会被阻塞住,同时我们在区间 (3,5)(5,7)之间执行 X 锁的操作也是不被允许的。也是会阻塞的。

sql 复制代码
begin ;
# 事务A已经锁住了id=5的行
select * from t where id = 5 for update;
# 保证b=4的这一行数据时不存在的。
insert into t(b) values (4);

五、InnoDB 事务

InnoDB中通过MVVC机制实现了多个事务中的一致性非锁定读。

5.1 事务的ACID特性

  • Atomicity:原子性。事务中的所有SQL要么都执行成功,要么都执行失败。

  • Consistency:一致性,事务能够保证DB从一个正确的状态转移到另一个正确的状态

  • Isolation:隔离性,一个事物的执行不能够影响到另一个并发执行的事务,解决手段如下:

    • 锁机制:解决一个写操作影响另一个写操作
    • MVCC:解决一个写操作影响另一个读操作
  • Durability:持久性,事务一旦提交,它对数据库的改变是永久性的

5.2 事务的隔离级别

  • 读未提交:事务A可以读取到事务B未提交的数据

    会产生脏读,假设事务B最后因为某个SQL执行失败导致回滚,此时事物A读取到的数据就是脏数据

  • 读已提交:事务A只能读取到事务B已经提交了的数据

    不可重复读取,假设事务A读取了两次事务B的内容,读取的时机为,第一次时机是事务B修改了某行记录,之后事务A读取,最后事务B因为某个SQL回滚,导致事务A再次读取到的数据不一致

  • 可重复读:事务A执行多次SQL保证的结果都是一致性的

    会产生幻读:事务B先开启事务,然后事务A开启,此时在事务B内删除掉ID=10的数据,并且提交,此时事物A仍然能读取到ID=10的数据,这个是根据undo log 日志来找到ID=10的记录在事务A开启前的最新的版本的记录,所以会造成幻读

  • 可串行化:脏读、可重复读、幻

5.2 MVCC 机制

用来解决一个写操作时影响另一个读操作时的隔离性 参考:juejin.cn/post/701616...

MVCC多版本并发控制机制,其中的多版本指的是某一条记录在undo log中存在多个版本,而该机制最主要保证的是事务A开启之后,事务A内的所有SQL操作其可看到的快照数据都是其事务A开启前最后一刻内的数据快照,同时会产生幻读的可能性。

相关推荐
捂月38 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn1 小时前
依赖注入注解
java·后端·spring
打鱼又晒网1 小时前
【MySQL】数据库精细化讲解:内置函数知识穿透与深度学习解析
数据库·mysql
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪1 小时前
Django:从入门到精通
后端·python·django
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet271 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom2 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
tatasix2 小时前
MySQL UPDATE语句执行链路解析
数据库·mysql
天海华兮2 小时前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql