 全表扫描与内存占用
全表扫描与内存占用
  # 全表扫描与内存占用
注意
内存只有100G,要对磁盘里一张200G的表进行全表扫描,内存会不会爆?
结论:MySQL的策略是“边读边发”。表数据会分段发给客户端,服务端buffer被阻塞就暂停发送。
- 首先InnoDB会搜集数据并将数据页分段加入buffer pool中。
- server层每次读取一行记录,并放到MySQL内存net_buffer中,当net_buffer写满后再调用网络接口将部分结果集发送出去。
- 发送函数会将net_buffer的内容写到socket send buffer本地网络栈,然后发送到客户端。如果发送函数返回EAGAIN或WASEWOULDBLOCK,则说明socket send buffer写满了,进入等待,然后重新发送。

# 1.server层

客户端请求数据时,在服务端查看执行查询线程id可能存在两种状态:
# 1.1sending to client
表示本地网络栈当前已经写满了,线程处于等待客户端接收结果的状态。比如客户端使用-quick参数就会出现这种情况,此时会调用mysql_use_result方法请求数据。客户端取出一条数据后可能会进行较长时间的处理步骤,从而导致server端的buffer被“阻塞”不能立刻将所有数据发送出去。
✨因此一般情况下,如果返回的整个结果集并不是很大,不会占用太多内存,那么可以使用mysql_store_result接口,直接将查询结果保存在本地内存再处理。
如果依旧发现多个线程处于sending to client状态,那么优化方法如下:
- 评估是否可以减小返回的结果集。
- 将net_buffer设置的更大一些。一般来说net_buffer会比socket send buffer大很多。而对于执行器来说,只要当前结果都读到net_buffer则相当于”已经写出去“了,所以优先确保server层尽快把数据从InnoDB读出来,再利用网络栈慢慢发送。
# 1.2sending data
表示当前查询线程处于“收集+发送数据”的状态,具体来说当前线程可能处在以下任意一个状态:
- 当前SQL语句在InnoDB层需要进行锁等待。
- InnoDB层需要收集数据,包括查找索引,回表。
- server层将数据发送给客户端。
因此如果服务端的线程出现大量的sending data状态,那么需要进一步仔细排查索引、回表等问题。
# 2.InnoDB层与buffer pool
InnoDB内存数据页是在buffer pool进行管理,因此要理解全表查询时的执行步骤和优化,关键就看在buffer pool里发生了什么。
buffer pool作为InnoDB的缓冲池,它的核心机制如下:
# 2.1加速更新
执行更新语句时,如果数据页在buffer pool(不在就从磁盘读出来),那么只需要在buffer pool的数据页上进行更新,不需要将脏页刷到磁盘。如果遇到崩溃丢失内存更新数据,直接配合redo log进行恢复即可。
因此buffer pool+redo log共同保证了加速更新。(反过来说如果没有redolog,那么每次更新就要刷盘,否则崩溃内存就会丢失更新数据)
# 2.2加速查询
buffer pool中的数据页肯定保证是最新的,查询请求到来时,如果buffer pool有那就直接读,不需要读磁盘。但如果buffer pool没有命中,就还是需要读磁盘。因此加速查询的效果取决于buffer pool的命中率。InnoDB通过采用不同的淘汰页算法,从而提高buffer pool的内存页命中率。
# LRU
InnoDB管理的LRU算法基于链表实现。越靠近头结点位置的节点越新,靠近尾节点的节点越旧。
- 访问某个节点时如果命中,则将该节点取出来放到头结点位置。
- 如果内存不命中,则从磁盘将数据页读到buffer pool,作为新的头结点,同时将尾节点删除。
💣存在的问题:如果当前业务1正在利用buffer pool的内存命中给用户快速返回数据,这时候有另外一个事务需要进行全表扫描访问历史数据,并且表数据很大200G,那么就会依次从磁盘将新的数据页读到内存,并淘汰掉当前业务1的数据页。
这么一来①业务1的本来可以命中的数据页都需要重新读磁盘,增加了磁盘压力
②历史数据一般来说读了一次后,就不会再读了,因此会霸占整个buffer pool的资源,直到新的数据页把它们刷掉。
# 优化版本LRU
InnoDB将整个LRU链表按照5:3分成了young区域和old区域。添加了一个LRU_old指针,其中head到pre_LRU_old之间为young区域,而LRU_old到tail之间为old区域。

- 如果内存命中,分为以下两种情况:
- 命中young区域,则将数据页提出来作为新的head节点
- 如果命中old区域,则根据内存数据页存活时间与innodb_old_blocks_time(默认1s)比较的结果决定:
- 如果数据页在LRU链表存在的时间大于1s,则将他提到head位置
- 如果数据页在LRU链表存在的时间小于1s,则原地不动。
 
 
- 如果内存没有命中,从磁盘读新的数据页,并放在LRU_old指针位置。同时删除tail数据页。
因此虽然大表也会用到buffer pool,但因为它每个数据页读得快,只会扫描一次,所以只会老老实实呆在old区域。对整个young区域没有影响,从而保证正常业务的buffer pool命中率。
显然如果innodb_old_blocks_time越小,则表示对所有的表数据一视同仁,内存淘汰算法退化为上面的普通LRU。而如果innodb_old_blocks_time越大,则“LRU算法性”越弱,退化为普通内存。
总结:old区域放冷数据历史数据,短时间内会被优先清除。而young则存放真正最近访问的数据,短时间内不会被清除。
# 3.客户端阻塞会给服务端造成什么影响
客户端请求数据时如果因为阻塞导致一直不接受服务端数据,那么当前这个事务就是”长事务“。
- 当前事务因为占据锁资源,因此导致其它语句被锁住。
- 如果频繁执行更新事务,undolog不断变大由于服务端阻塞无法被回收,磁盘爆满。
