mysql - 实战 (1)
1 基础架构: 一条SQL查询语句如何执行
1.1 MySQL逻辑架构图
- MySQL 可以分为 Server 层和存储引擎层两部分
1.1.1 Service层
- Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等
1.1.1.1 连接器
- 连接器负责跟客户端建立连接、获取权限、维持和管理连接
mysql -h$ip -P$port -u$user -p
- 连接建立以后权限就确定下来了,如果修改权限下次重新连接生效
- 连接之后如没有后续操作这个连接将会处于空闲状态
查看命令:
show processlist;
空闲状态显示为sleep
- 客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时
长连接和短连接:
- 长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接
- 短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个
长连接的问题:
有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了
解决方案:
- 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连
- 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态
1.1.1.2 查询缓存
- MySQL 8.0 版本直接将查询缓存的整块功能删掉了
原理:
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高
查询缓存的问题:
- 表更新,表上的查询缓存全部清空
- 8.0 之前可以将参数 query_cache_type 设置成 DEMAND,再使用SQL_CACHE显示指定来查询缓存
mysql> select SQL_CACHE * from T where ID=10;
- 缓存需要语句完全相等,包括参数
适用场景:
- 业务就是有一张静态表,很长时间才会更新一次
- 比如,一个系统配置表,那这张表上的查询才适合使用查询缓存
1.1.1.3 分析器
- 解析语句,生成解析树
- 检查语句中的关键词,表,字段是否存在
- 词法分析
你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么
MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”
- 语法分析
根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法
1.1.1.4 优化器
- 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序
- 原则是:尽可能扫描少的数据库行纪录
1.1.1.5 执行器
- 执行时,查询用户对该表是否有查询的权限,没有则报错
- 为什么对权限的检查不在优化器之前做? - SQL语句要操作的表不只是SQL字面上那些。比如如果有个触发器,得在执行器阶段(过程中)才能确定。优化器阶段前是无能为力的
- 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口
执行器执行流程(没有索引):
调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
执行器执行流程(有索引):
第一次调用的是“取满足条件的第一行”这个接口
之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。
- 慢查询日志中 rows_examined 的字段,表示这个语句执行过程中扫描了多少行
- 这个值就是在执行器每次调用引擎获取数据行的时候累加的
1.1.2 存储引擎层
- 存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。
- create table建表时如不指定引擎类型则默认InnoDB或使用engine=memory来指定使用内存引擎创建表
- 不同的存储引擎共用一个Server层,即连接器到执行器部分
1.1.3 常见问题
- 连接器是从权限表里边查询用户权限并保存在一个变量里边以供查询缓存,分析器,执行器在检查权限的时候使用
- sql执行过程中可能会有触发器这种在运行时才能确定的过程,分析器工作结束后的precheck是不能对这种运行时涉及到的表进行权限校验的,所以需要在执行器阶段进行权限检查。另外正是因为有precheck这个步骤,才会在报错时报的是用户无权,而不是 k字段不存在(为了不向用户暴露表结构)。
- 词法分析阶段是从information schema里面获得表的结构信息的
- 可以使用连接池的方式,将短连接变为长连接
- mysql_reset_connection是mysql为各个编程语言提供的api,不是sql语句
- wait_timeout是非交互式连接的空闲超时,interactive_timeout是交互式连接的空闲超时。执行时间不计入空闲时间。这两个超时设置得是否一样要看情况
2 日志系统:一条SQL更新语句是如何执行的
2.1 日志模块
更新流程除了查询流程还有两个日志模块:
- redo log(重做日志)
- binlog(归档日志)
2.1.1 重做日志 redo log
目的:
如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高
WAL技术 (Write-Ahead Logging)
- 先写日志,再写磁盘
- 先写日志也是先写磁盘,只是写日志是顺序写盘,速度很快
- 有点类似于存下来,但是存的位置可以不限,但是如果限定位置,也就是记账的过程,就涉及到查找了,而磁盘的寻道时间开销是非常高的
redo log是固定大小,从头开始写,写到末尾就又回到开头循环写
-
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头
-
checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件
-
红线之间的部分是空闲的空间,可以用来记录新的操作
-
如果 write pos 追上 checkpoint,不能再执行新的操作,需要先擦掉一些记录并推进checkpoint
这种设计可能会带来的问题:
- redo log满了再停下来擦除可能会造成请求阻塞
- 光写log文件不写进库里可能会导致数据实时性的问题
设计解决方案:
- 后台线程定期会刷脏页
- 清理LRU链表时会顺带刷脏页
- redoLog写满会强制刷
- 数据库关闭时会将所有脏页刷回磁盘
- 脏页数量过多(默认占缓冲池75%)时,会强制刷
Crash-safe - InnoDB依靠redo log可以保证即使数据库发生异常重启,之前提交的记录都不会丢失
innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘 - 保证 MySQL 异常重启之后数据不丢失
2.1.2 归档日志 binlog
- 属于Server层的日志
为什么有两种日志:
因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘 - 保证 MySQL 异常重启之后 binlog 不丢失
2.2 两种日志的区别
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”
REDO的写盘时间会直接影响系统吞吐,显而易见,REDO的数据量要尽量少。其次,系统崩溃总是发生在始料未及的时候,当重启重放REDO时,系统并不知道哪些REDO对应的Page已经落盘,因此REDO的重放必须可重入,即REDO操作要保证幂等。最后,为了便于通过并发重放的方式加快重启恢复速度,REDO应该是基于Page的,即一个REDO只涉及一个Page的修改。 熟悉的读者会发现,数据量小是Logical Logging的优点,而幂等以及基于Page正是Physical Logging的优点,因此InnoDB采取了一种称为Physiological Logging的方式,来兼得二者的优势。所谓Physiological Logging,就是以Page为单位,但在Page内以逻辑的方式记录。举个例子,MLOG_REC_UPDATE_IN_PLACE类型的REDO中记录了对Page中一个Record的修改,方法如下: (Page ID,Record Offset,(Filed 1, Value 1) ... (Filed i, Value i) ... ) 其中,PageID指定要操作的Page页,Record Offset记录了Record在Page内的偏移位置,后面的Field数组,记录了需要修改的Field以及修改后的Value
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志
2.3 Update语句执行流程
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成
2.4 两阶段提交
2.4.1 怎样让数据库恢复到半个月内任意一秒的状态?
整库备份+binlog:
- 首先,找到最近的一次全量备份,从这个备份恢复到临时库
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到误删表之前的那个时刻
- 这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去
2.4.2 为什么需要两阶段提交?
为了让两份日志之间的逻辑一致
先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同
先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同
3 问题总结
3.1 基础架构
- MySQL的框架有几个组件, 各是什么作用?
- Server层和存储引擎层各是什么作用?
- you have an error in your SQL syntax 这个保存是在词法分析里还是在语法分析里报错?
- 对于表的操作权限验证在哪里进行?
- 执行器的执行查询语句的流程是什么样的?
3.2 日志系统
- redo log的概念是什么? 为什么会存在.
- 什么是WAL(write-ahead log)机制, 好处是什么.
- redo log 为什么可以保证crash safe机制.
- binlog的概念是什么, 起到什么作用, 可以做crash safe吗?
- binlog和redolog的不同点有哪些?
- 物理一致性和逻辑一致性各应该怎么理解?
- 执行器和innoDB在执行update语句时候的流程是什么样的?
- 如果数据库误操作, 如何执行数据恢复?
- 什么是两阶段提交, 为什么需要两阶段提交, 两阶段提交怎么保证数据库中两份日志间的逻辑一致性(什么叫逻辑一致性)?
- 如果不是两阶段提交, 先写redo log和先写bin log两种情况各会遇到什么问题?