读mysql45讲-全表扫描

全表扫描对server层的影响

假设需要对一个200G的表做一个全表扫描,并不是一次性直接把200G的数据发给客户端,那么干的话肯定会内存溢出的。
取数据和发数据的流程大概是这样的:

  1. 获取一行,存到net_buffer中,这块内存的大小是参数net_buffer_length配置的,默认是16k左右
  2. 重复获取行数据,一直到net_buffer被用完,然后调用网络接口发送出去
  3. 发送成功之后,就将net_buffer清空,重复取数据发数据的步骤
  4. 如果发送函数返回EAGIN或者是WSAEWOULDBLOCK,表示本地网络栈(socket send buffer)写满了;等待直到可以重新写数据。

读mysql45讲-全表扫描_第1张图片

也就是说,MYSQL是边读边发的,如果客户端读取数据的速度很慢,也会影响到MYSQL发送数据的效率。之前有说过在连接数据库的时候加上-quick参数,就是使用mysql_use_result,读取一行处理一行,这种情况下,如果对一行的数据处理逻辑比较复杂,就会导致客户端读数据就会很慢。

所以在返回数据不是很大的情况下,可以用mysql_store_reuslt,也就是将查询结果缓存到本地。

全表扫描对InnoDB的影响

在WAL机制那块,分析了InnoDB内存的作用,就是在内存中保存的数据,配合redo log,在redo log记录了操作日志,当查询数据在内存中,并且redo log也有操作记录的话,就将日志内容应用到内存的数据从而可以得到最新的数据,避免了直接对磁盘的多次IO操作。

内存的数据页是在Buffer Pool中管理的,在WAL里Buffer Pool 起到了加速更新的作用。而 实际上,Buffer Pool 还有一个更重要的作用,就是加速查询。

当事务提交的时候,磁盘上的数据页是旧的,那如果这时候马上有一个查询要来读这个数据页,是不是要马上把redo log应用到数据页呢?
答案是不需要。因为这时候内存数据页的结果是最新的,直接读内存页就可以了,所以加速了查询。

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

可以通过执行show engine innodb status命令返回的结果中查看,可以看到“Buffer pool hit rate”字样,显示的就是当前的命中 率

读mysql45讲-全表扫描_第2张图片

InnoDB Buffer Pool的大小是由参数InnoDB_buffer_pool_size配置的,一般设置成可用物理内存的60%~80%。
innodb_buffer_pool_size是小于磁盘大小的,所以当一个buffer pool内存占满了,又需要重新从磁盘中读取一个新的数据页,就需要淘汰一个旧的数据页。
InnoDB内存管理使用的是最近最少使用 (Least RecentlyUsed, LRU)算法,这个算法的核心就是淘汰最久未使用的数据。

读mysql45讲-全表扫描_第3张图片

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

  1. 在上图的状态1里,链表头部是P1,表示P1是最近刚刚被访问过的数据页;假设内存里只能 放下这么多数据页;
  2. 这时候有一个读请求访问P3,因此变成状态2,P3被移到最前面;
  3. 状态3表示,这次访问的数据页是不存在于链表中的,所以需要在Buffer Pool中新申请一个 数据页Px,加到链表头部。但是由于内存已经满了,不能申请新的内存。于是,会清空链表 末尾Pm这个数据页的内存,存入Px的内容,然后放到链表头部。
  4. 从效果上看,就是最久没有被访问的数据页Pm,被淘汰了。

但是如果这个时候来一个全表扫描的查询请求,并且查询的是一个历史表,就会导致内存的数据页都替换成了历史表相关的数据,但是对历史表的查询并不是高频的,就会导致内存命中率急速下降,磁盘压力增加,sql返回结果缓慢。

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

读mysql45讲-全表扫描_第4张图片

在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。
    1. 之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉数据页Pm,但是新 插入的数据页Px,是放在LRU_old处。
  2. 处于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,mysql,数据库)