MySQL那些事

基础知识

查询语句的执行过程

大体来说,MySQL 可以分为 Server 层和存储引擎层两部分。Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

连接器

连接器负责跟客户端建立连接、获取权限、维持和管理连接。

查询缓存

连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。但是大多数情况下建议不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。

分析器

进行词法分析和语法分析。输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串"T"识别成"表名 T",把字符串"ID"识别成"列 ID"。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。

优化器

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

执行器会根据表的引擎定义,去使用这个引擎提供的接口进行查询。

更新语句的执行过程

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。

binlog

这两种日志有以下三点不同。

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

两阶段提交

update 语句时的内部流程:

  • 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  • 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  • 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  • 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  • 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

事务隔离

SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

  • 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化,顾名思义是对于同一行记录,"写"会加"写锁","读"会加"读锁"。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

MVCC

xie.infoq.cn/article/f89...

索引下推

MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中"名字第一个字是张,而且年龄是 10 岁的所有男孩"。那么,SQL 语句是这么写的:

sql 复制代码
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;

无索引下推执行流程:

索引下推执行流程:

InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。

全局锁和表级锁

全局锁

全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用场景是,做全库逻辑备份。

表级锁

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

表锁的语法是 lock tables ... read/write。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。

另一类表级的锁是 MDL(metadata lock)。MDL 不需要显式使用,在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

行锁

行锁并不是在事务开始时就一次性全部加上的,而是在实际需要访问特定行数据时才会加上。这种机制有助于提高并发度,减少不必要的锁定开销。具体来说:

  • 何时加锁:当一个事务试图修改(UPDATE, DELETE)或根据某些条件查询(SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE)某一行记录时,InnoDB会对涉及到的行加锁。这意味着锁是在执行具体的SQL操作时按需获取的,而不是在事务启动时就确定好的。

  • 锁类型

    • 共享锁(S锁) :通常由SELECT ... LOCK IN SHARE MODE语句产生。允许多个事务同时持有对同一数据项的共享锁,但如果有任何事务持有了某个数据项的共享锁,则其他事务不能获得该数据项的排他锁。
    • 排他锁(X锁) :通常由INSERT, UPDATE, DELETE以及SELECT ... FOR UPDATE语句产生。一旦一个事务获得了排他锁,其他的事务无论是请求共享锁还是排他锁都将被阻塞,直到拥有该锁的事务提交或回滚。
  • 加锁粒度:InnoDB支持行级锁定,这意味着它可以锁定单个表中的特定行,而不是整个表。这与一些只支持表级锁定的数据库系统相比,可以显著提高并发处理能力。

死锁和死锁检测

当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。

当出现死锁以后,有两种策略:

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

实践

唯一索引和普通索引

这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。尽量选择普通索引。普通索引可以更大程度的利用change buffer,减少IO。

怎么给字符串加索引

  • 直接创建完整索引,这样可能比较占用空间;
  • 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
  • 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;

MySQL抖动

利用 WAL 技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能。但是,由此也带来了内存脏页的问题。脏页会被后台线程自动 flush,也会由于数据页淘汰而触发 flush,而刷脏页的过程由于会占用资源,可能会让你的更新和查询语句的响应时间长一些。

order by 工作原理

全字段排序

  • 初始化 sort_buffer,确定放入需要返回的字段;
  • 从索引找到第一个满足条件的主键 id;
  • 到主键 id 索引取出整行,取出字段的值,存入 sort_buffer 中;
  • 从索引取下一个记录的主键 id;
  • 重复步骤 3、4 直到索引的值不满足查询条件为止;
  • 对 sort_buffer 中的数据按照排序字段做快速排序;
  • 按照排序结果取limit行返回给客户端。

rowid排序

就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。所以如果单行很大,这个方法效率不够好。max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。新的算法放入 sort_buffer 的字段,只有要排序的列和主键 id。

但这时,排序的结果就因为少了少了部分需要返回的字段的值,不能直接返回了,整个执行流程就变成如下所示的样子:

  • 初始化 sort_buffer,确定放入两个字段,即 排序字段 和 id;
  • 从索引字段找到第一个满足条件的主键 id;
  • 到主键 id 索引取出整行,取 排序字段、id 这两个字段,存入 sort_buffer 中;
  • 从索引取下一个记录的主键 id;
  • 重复步骤 3、4 直到不满足条件为止;
  • 对 sort_buffer 中的数据按照字段 排序字段 进行排序;
  • 遍历排序结果,取前limit行,并按照 id 的值回到原表中取出需要返回的字段返回给客户端。

查一行为什么也慢

查询长时间不返回

  • 等MDL锁,解决方式是通过查询 sys.schema_table_lock_waits 这张表,我们就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。

flush

用于关闭所有打开的表,并强制将所有修改过的表数据刷新到磁盘。

arduino 复制代码
flush tables t with read lock;

flush tables with read lock;

这两个 flush 语句,如果指定表 t 的话,代表的是只关闭表 t;如果没有指定具体的表名,则表示关闭 MySQL 里所有打开的表。flush语句通常需要等待当前正在执行的所有语句完成后才会继续执行,如果前面的语句耗时长,会导致flush等待,卡住其他正常执行查询的语句。

等行锁

由于访问 id=1 这个记录时要加读锁,如果这时候已经有一个事务在这行记录上持有一个写锁,我们的 select 语句就会被堵住。

幻读

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

  • 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在"当前读"下才会出现。
  • 幻读仅专指"新插入的行"。

如何解决幻读

为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。

执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。跟间隙锁存在冲突关系的,是"往这个间隙中插入一个记录"这个操作。间隙锁之间都不存在冲突关系。

MySQL如何实现数据一致性

前提是先理解redo log 和binlog的两阶段提交协议。然后机器重启存在以下三种情况: 1、redolog在prepare阶段持久化到磁盘(可能失败) 2、紧接着binlog持久化(可能失败) 3、最后redolog commit(可能失败) 情况一:在1处mysql异常重启,redo log没有fsync,内存丢失,直接回滚,不影响数据一致性; 情况二:redolog fsync成功,但是binlog写入错误,此时mysql异常重启,现在有redolog的磁盘数据没有binlog的数据,此时检测redolog处于prepare阶段,但是没有binlog,回滚(虽然刚刚redolog fsync了但是不影响数据一致性,因为redolog的操作并没有写入mysql,也永远不会写入mysql); 情况三:binlog完整但未commit,此时检测redolog处于prepare阶段,且binlog完整但未提交,默认添加commit标记,进而提交,写入mysql,满足数据一致性; 情况四:binlog完整且提交,写入musql,满足一致性;

MySQL主备原理

可以看到:主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog。备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。

一个事务日志同步的完整过程是这样的:

  • 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
  • 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
  • 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
  • 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
  • sql_thread 读取中转日志,解析出日志里的命令,并执行。
相关推荐
开心就好20251 小时前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默1 小时前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦1 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl2 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6862 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情3 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player3 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明3 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展
武超杰3 小时前
Spring Boot入门教程
java·spring boot·后端
IT 行者3 小时前
Spring Boot 集成 JavaMail 163邮箱配置详解
java·spring boot·后端