全表扫描时,客户端查询服务端数据库中大量数据,查询结果是如何返回给客户端的。

全表扫描对server层的影响

mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file

InnoDB的数据是保存在主键索引上的,所以全表扫描实际上是直接扫描表t的主键索引。这条查询语句由于没有其他的判断条件,所以查到的每一行都可以直接放到结果集里面,然后返回给客户端。那么,这个“结果集”存在哪里呢?

实际上,服务端并不需要保存一个完整的结果集。取数据和发数据的流程是这样的:

即数据并不是一次性查出来然后一次性发给客户端,这里需要分批次来发送。

  1. 获取一行,写到net_buffer中。这块内存的大小是由参数net_buffer_length定义的,默认是16k。

  2. 重复获取行,直到net_buffer写满,调用网络接口发出去。

  3. 如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer。

  4. 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。

这个过程对应的流程图如下所示。

图1 查询结果发送流程

从这个流程中,你可以看到:

  1. 一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G;

  2. socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果socket send buffer被写满,就会暂停读数据的流程。

也就是说,MySQL是“边读边发的”,这个概念很重要。这就意味着,如果客户端接收得慢,会导致MySQL服务端由于结果发不出去,这个事务的执行时间变长。

比如下面这个状态,让客户端不去读socket receive buffer中的内容,然后在服务端show processlist看到的结果。

图2 服务端发送阻塞

如果你看到State的值一直处于“Sending to client”,就表示服务器端的网络栈写满了。而如果要快速减少处于这个状态的线程的话,将net_buffer_length参数设置为一个更大的值是一个可选方案。

与“Sending to client”长相很类似的一个状态是“Sending data”,这是一个经常被误会的问题。

实际上一个查询语句的状态变化:

  • MySQL查询语句进入执行阶段后,首先把状态设置成“Sending data”;
  • 然后,发送执行结果的列相关的信息(meta data) 给客户端;
  • 再继续执行语句的流程;
  • 执行完成后,把状态设置成空字符串。

也就是说,“Sending data”并不一定是指“正在发送数据”,而可能是处于执行器过程中的任意阶段。比如,你可以构造一个锁等待的场景,就能看到Sending data状态。

图3 读全表被锁

图 4 Sending data状态

可以看到,session B明显是在等锁,状态显示为Sending data。也就是说,仅当一个线程处于“等待客户端接收结果”的状态,才会显示"Sending to client";而如果显示成“Sending data”,它的意思只是“正在执行”。

查询的结果是分段发给客户端的,因此扫描全表,查询返回大量的数据,并不会把内存打爆。

全表扫描对InnoDB的影响

内存的数据页是在Buffer Pool (BP)中管理的,在WAL里Buffer Pool 起到了加速更新的作用。而实际上,Buffer Pool 还有一个更重要的作用,就是加速查询。由于有WAL机制,当事务提交的时候,磁盘上的数据页是旧的,那如果这时候马上有一个查询要来读这个数据页,是不是要马上把redo log应用到数据页呢?不需要。因为这时候内存数据页的结果是最新的,直接读内存页就可以了。

而Buffer Pool对查询的加速效果,依赖于一个重要的指标,即:内存命中率

你可以在show engine innodb status结果中,查看一个系统当前的BP命中率。一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在99%以上。

执行show engine innodb status ,可以看到“Buffer pool hit rate”字样,显示的就是当前的命中率。比如图5这个命中率,就是99.0%。

图5 show engine innodb status显示内存命中率

如果所有查询需要的数据页都能够直接从内存得到,那是最好的,对应的命中率就是100%。但,这在实际生产上是很难做到的。

InnoDB Buffer Pool的大小是由参数 innodb_buffer_pool_size确定的,一般建议设置成可用物理内存的60%~80%。

在大约十年前,单机的数据量是上百个G,而物理内存是几个G;现在虽然很多服务器都能有128G甚至更高的内存,但是单机的数据量却达到了T级别。

所以,innodb_buffer_pool_size小于磁盘的数据量是很常见的。如果一个 Buffer Pool满了,而又要从磁盘读入一个数据页,那肯定是要淘汰一个旧数据页的。

InnoDB内存管理用的是最近最少使用 (Least Recently Used, LRU)算法,这个算法的核心就是淘汰最久未使用的数据。

下图是一个LRU算法的基本模型。

图6 基本LRU算法

InnoDB管理Buffer Pool的LRU算法,是用链表来实现的。

  1. 在图6的状态1里,链表头部是P1,表示P1是最近刚刚被访问过的数据页;假设内存里只能放下这么多数据页;

  2. 这时候有一个读请求访问P3,因此变成状态2,P3被移到最前面;

  3. 状态3表示,这次访问的数据页是不存在于链表中的,所以需要在Buffer Pool中新申请一个数据页Px,加到链表头部。但是由于内存已经满了,不能申请新的内存。于是,会清空链表末尾Pm这个数据页的内存,存入Px的内容,然后放到链表头部。

  4. 从效果上看,就是最久没有被访问的数据页Pm,被淘汰了。

假设按照这个算法,我们要扫描一个200G的表,而这个表是一个历史数据表,平时没有业务访问它。

那么,按照这个算法扫描的话,就会把当前的Buffer Pool里的数据全部淘汰掉,存入扫描过程中访问到的数据页的内容。也就是说Buffer Pool里面主要放的是这个历史数据表的数据。对于一个正在做业务服务的库,Buffer Pool的内存命中率急剧下降,磁盘压力增加,SQL语句响应变慢。

所以,InnoDB不能直接使用这个LRU算法。实际上,InnoDB对LRU算法做了改进。

图 7 改进的LRU算法

在InnoDB实现上,按照5:3的比例把整个LRU链表分成了young区域和old区域。图中LRU_old指向的就是old区域的第一个位置,是整个链表的5/8处。也就是说,靠近链表头部的5/8是young区域,靠近链表尾部的3/8是old区域。

改进后的LRU算法执行流程变成了下面这样。

  1. 图7中状态1,要访问数据页P3,由于P3在young区域,因此和优化前的LRU算法一样,将其移到链表头部,变成状态2。

  2. 之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉数据页Pm,但是新插入的数据页Px,是放在LRU_old处。

  3. 处于old区域的数据页,每次被访问的时候都要做下面这个判断:

    • 若这个数据页在LRU链表中存在的时间超过了1秒,就把它移动到链表头部;
    • 如果这个数据页在LRU链表中存在的时间短于1秒,位置保持不变。1秒这个时间,是由参数innodb_old_blocks_time控制的。其默认值是1000,单位毫秒。

这个策略,就是为了处理类似全表扫描的操作量身定制的。还是以刚刚的扫描200G的历史数据表为例,我们看看改进后的LRU算法的操作逻辑:

  1. 扫描过程中,需要新插入的数据页,都被放到old区域;

  2. 一个数据页里面有多条记录,这个数据页会被多次访问到,但由于是顺序扫描,这个数据页第一次被访问和最后一次被访问的时间间隔不会超过1秒,因此还是会被保留在old区域;

  3. 再继续扫描后续的数据,之前的这个数据页之后也不会再被访问到,于是始终没有机会移到链表头部(也就是young区域),很快就会被淘汰出去。

可以看到,这个策略最大的收益,就是在扫描这个大表的过程中,虽然也用到了Buffer Pool,但是对young区域完全没有影响,从而保证了Buffer Pool响应正常业务的查询命中率。

参考资料:

MySQL实战45讲--林晓斌

MySQL实战45讲学习笔记----查询结果返回过程分析相关推荐

  1. MySQL实战45讲学习笔记

    文章目录 MySQL实战45讲-学习笔记 01 基础架构:一条SQL查询语句是如何执行的? mysql逻辑架构 连接器 查询缓存 分析器 优化器 执行器 02 日志系统:一条SQL更新语句如何执行 r ...

  2. 丁奇的MySQL实战45讲 学习笔记[链接]

    收录一下, 方便自己查阅 <MySQL实战45讲>1~15讲 -丁奇,学习笔记 <MySQL实战45讲>16~30讲 -丁奇,学习笔记 <MySQL实战45讲>31 ...

  3. mysql 实战 45讲 学习笔记 基础知识 原理剖析

    MySQL 实战45讲 持续更新中~ 00讲 开篇 我们知道如何写出逻辑正确的SQL语句来实现业务目标,却不确定这个语句是不是最优的 我们听说了一些使用数据库的最佳实践,但是更想了解为什么这么做 我们 ...

  4. 极客时间MySQL实战45讲学习笔记

    零:基础 第一讲:基础架构:一条SQL查询语句是如何执行的? MySQL的基本架构示意图 1.MySQL基础架构 大体来说,MySQL可以分为Server层和存储引擎层两部分. Server层包括连接 ...

  5. MySQL实战45讲学习笔记:MySQL架构(第一讲)

    一.MySQL逻架构图 二.连接器工作原理刨析 1.连接器工作原理图 2.原理图说明 1.连接命令 mysql -h$ip -P$port -u$user -p 2.查询链接状态 3.长连接端连接 1 ...

  6. MySQL实战45讲学习笔记:第七讲

    一.两阶段锁 1.持有哪些锁,以及在什么时候释放 我先给你举个例子.在下面的操作序列中,事务 B 的 update 语句执行时会是什么现象呢? 假设字段 id 是表 t 的主键. 这个问题的结论取决于 ...

  7. mysql执行动态说起来_MySQL实战45讲学习笔记:第十四讲

    一.引子 在开发系统的时候,你可能经常需要计算一个表的行数,比如一个交易系统的所有变更记录总数.这时候你可能会想,一条 select count(*) from t 语句不就解决了吗? 但是,你会发现 ...

  8. mysql实战45讲(15-22)

    mysql实战45讲学习笔记 15 日志与索引的关系 15.1 日志 1,分析一下在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象. 如果在图中时刻 A 的地方,也就是写入 redo log ...

  9. mysql实战45讲(23-26)

    mysql实战45讲学习笔记 23 mysql保证数据不丢失 23.1,binlog的写入 逻辑:事务执行过程中,先把日志写到binlog cache中,事务提交的时候,再把cache写到binlog ...

最新文章

  1. ECC-based 算法(ECDSA/ECDH) 新潮算法的原理
  2. windows下安装mysql压缩包版[转]
  3. 关于处理小数点位数的几个oracle函数
  4. Linux系统中的load average
  5. 某公司为本科以上学历的人重新分配工作,分配原则如下。 (1)如果年龄不满18岁,学历是本科,男性要求报考研究生,女性则担任行政工作; (2)如果年龄满18岁不满5o 岁,学历本科,不分男女,任中层领导
  6. Linux 基础知识系列第一篇
  7. [机器学习]京东机器学习类图书畅销原因分析-决策树或随机森林
  8. PackageManager.getPackageSizeInfo||UserHandle.myUserId()
  9. 【人脸表情识别】基于matlab GUI LBP+SVM脸部动态特征人脸表情识别【含Matlab源码 1369期】
  10. 基于大数据可视化技术的毕业生就业分析服务项目 (软件创新设计期末报告)
  11. html用post怎么加密,post提交数据如何加密
  12. 中原银行AI面试记录
  13. java面试真题 烽火通信_java和数据库面试题-烽火通信
  14. C#网络编程之基础语法 网络流(NetworkStream) 文本流(Stream) 文件流(Filestream )
  15. JIRA-使用教程_界面_创建、方案配置
  16. 打印机显示域服务器,操作打印机提示“active directory域服务当前不可用”怎么办?...
  17. H5页面播放M4a音频文件
  18. 可以买到 Linux 电脑的 10 个地方
  19. 如何监控和保护Linux下进程安全
  20. SEO人员,如何拨乱反正?

热门文章

  1. mysql定义日期类型格式_Mysql 日期时间类型详解
  2. java_获取某同一天在某段时间内的周次
  3. 9月英语月刊--thinking
  4. JDBC访问数据库的步骤
  5. 用JS在html页面实现打印功能
  6. 树莓派学习手记——制作一个空调遥控器(红外接收、发射的实现) 1
  7. 【JVM】10道不得不会的JVM面试题
  8. 语音讲解机器人更是一种营销工具
  9. 如何理解count=count++,count的值不变
  10. linux添加删除软连接方式