MySQL -- 全表扫描
Server层
1 | -- db1.t有200GB |
查询数据
- InnoDB的数据是保存在主键索引上,全表扫描实际上是直接扫描表t的主键索引
- 获取一行,写到
net_buffer
中,默认为16K,控制参数为net_buffer_length
- 重复获取行,直到写满
net_buffer
,然后调用网络接口发出去 - 如果发送成功,就清空
net_buffer
,然后继续取下一行并写入net_buffer
- 如果发送函数返回
EAGAIN
或者WSAEWOULDBLOCK
,表示本地网络栈socket send buffer
写满- 此时,进入等待,直到网络栈重新可写,再继续发送
- 一个查询在发送数据的过程中,占用MySQL内部的内存最大为
net_buffer_length
,因此不会达到200G socket send buffer
也不可能达到200G,如果socket send buffer
被写满,就会暂停读取数据
1 | -- 16384 Bytes = 16 KB |
Sending to client
- MySQL是边读边发的,如果客户端接收慢,会导致MySQL服务端由于结果发不出去,事务的执行时间变长
- 下图为MySQL客户端不读取
socket receive buffer
中的内容的场景- State为
Sending to client
,表示服务端的网络栈写满了
- State为
mysql --quick
,会使用mysql_use_result
方法,该方法会读取一行处理一行- 假设每读出一行数据后要处理的逻辑很慢,就会导致客户端要过很久才会去取下一行数据
- 这时也会出现State为
Sending to client
的情况 - 对于正常的线上业务,如果单个查询返回的结果不多,推荐使用
mysql_store_result
接口 - 适当地调大
net_buffer_length
可能是个更优的解决方案
Sending data
State切换
- MySQL的查询语句在进入执行阶段后,首先把State设置为
Sending data
- 然后,发送执行结果的列相关的信息(meta data)给客户端
- 再继续执行语句的流程,执行完成后,把State设置为空字符串
- 因此State为
Sending data
不等同于正在发送数据
样例
1 | CREATE TABLE `t` ( |
session A | session B |
---|---|
BEGIN; | |
SELECT * FROM t WHERE id=1 FOR UPDATE; | |
SELECT * FROM t LOCK IN SHARE MODE; (Blocked) |
1 | mysql> SHOW PROCESSLIST; |
InnoDB层
- 内存的数据页是在
Buffer Pool
中管理的 - 作用:加速更新(WAL机制)+加速查询
内存命中率
SHOW ENGINE INNODB STATUS
中的Buffer pool hit rate 990 / 1000
,表示命中率为99%Buffer Pool
的大小由参数innodb_buffer_pool_size
控制,一般设置为物理内存的60%~80%
Buffer Pool
一般都会小于磁盘的数据量,InnoDB将采用LRU
算法来淘汰数据页
1 | -- 134217728 Bytes = 128 MB |
基本LRU
- InnoDB采用的LRU算法,是基于链表实现的
- State1,链表头部是P1,表示P1是最近刚刚被访问过的数据页
- State2,有一个读请求访问P3,P3被移动到链表的最前面
- State3,要访问的数据页不在链表中,所以需要在
Buffer Pool
中新申请一个数据页Px,加到链表头部- 但由于
Buffer Pool
已满,不能再申请新的数据页 - 于是会清空链表末尾Pm这个数据页的内存,存入Px的内容,并且放到链表头部
- 但由于
冷数据全表扫描
- 扫描一个200G的表,该表为历史数据表,平时没有什么业务访问它
- 按照基本LRU算法,就会把当前Buffer Pool里面的数据全部淘汰,存入扫描过程中访问到的数据页
- 此时,对外提供业务服务的库来说,Buffer Pool的命中率会急剧下降,磁盘压力增加,SQL语句响应变慢
- 因此InnoDB采用了改进的LRU算法
改进LRU
- 在InnoDB的实现上,按照
5:3
的比例把整个LRU链表分成young
区和old
区 LRU_old
指向old区的第一个位置,即靠近链表头部的5/8
是young
区,靠近链表尾部的3/8
是old
区- State1,要访问数据页P3,由于P3在young区,与基本的LRU算法一样,将其移动到链表头部,变为State2
- 然后要访问一个不在当前链表的数据页,此时依然要淘汰数据页Pm,但新插入的数据页Px放在
LRU_old
- 处于old区的数据页,每次被访问的时候都需要做以下判断
- 如果这个数据页在LRU链表中存在的时间超过了1S,就把它移动到链表头部,否则,位置不变
- 存在时间的值由参数
innodb_old_blocks_time
控制
- 该策略是为了处理类似全表扫描的操作而定制的
- 扫描过程中,需要新插入的数据页,都被放到
old
区 - 一个数据页会有多条记录,因此一个数据页会被访问多次
- 但由于是_顺序扫描_
- 数据页的第一次被访问和最后一次被访问的时间间隔不会超过1S,因此还是会留在
old
区
- 继续扫描,之前的数据页再也不会被访问到,因此也不会被移到
young
区,最终很快被淘汰
- 扫描过程中,需要新插入的数据页,都被放到
- 该策略最大的收益是在扫描大表的过程中,虽然用到了Buffer Pool,但对young区完全没有影响
- 保证了Buffer Pool响应正常业务的查询命中率
1 | -- 1000ms = 1s |
INNODB STATUS
1 | mysql> SHOW ENGINE INNODB STATUS\G; |
参考资料
《MySQL实战45讲》
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.