MySQL · 引擎特性 · InnoDB Buffer Pool

一序

     上一篇在介绍innodb的双写特性时,提到了Buffer Pool,知识点很多本文专门整理。《MYSQL运维内参的》的第11章第一节介绍了这部分。

      用户对数据库的最基本要求就是能高效的读取和存储数据,但是读写数据都涉及到与低速的设备交互,为了弥补两者之间的速度差异,所有数据库都有缓存池,用来管理相应的数据页,提高数据库的效率,当然也因为引入了这一中间层,数据库对内存的管理变得相对比较复杂。本文主要分析MySQL Buffer Pool的相关技术以及实现原理.Innodb的缓冲池主要是用来存储访问过的数据页的,它就是一块连续的内存,通过一定的算法使这块内存得到有效的管理。它是数据库系统中拥有最大块内存的系统。Innodb存储引擎中数据的访问是按照页(默认为16K)的方式从数据库文件中读取到缓冲区中,然后在内存中用同样大小的内存空间来做一个映射,为了提高数据访问效率,数据库系统预先就分配很多这样的空间,用来与文件中的数据进行交换。访问时按照最近最少使用(LRU)算法来实现缓冲区页面的管理,经常访问的在最前面,最不经常的在最后面,如果缓冲区中没有空闲的页面来做文件数据的映射时,就从缓冲池中找到最后面的且不使用的将它淘汰然后拿来映射新数据文件页面,同时将它移到LRU链表中最前面。这样就能保证经常访问的页面在没有刷盘的情况下始终在缓冲池中,从而保证了数据库的访问效率。

    缓冲池大小是可以配置的,可以通过配置文件中的参数innodb_buffer_pool_size的大小来决定,默认大小为128M,在mysql 5.7.5之前的版本,一旦MYSQL已经启动,则是不能再做修改的。之后的版本可以动态修改。需要注意的是如果Buffer Pool的大小超过1GB,则应该调整innodb_buffer_pool_size=N,把它分为若干个instance的做法,提升MYSQL的处理请求的并发能力,因为Buffer Pool是通过链表来管理页面的,同时为了保护页面,需要在存取的时候对链表进行加锁,在多线程的情况下,并发读写Buffer Pool的页面需要锁的竞争与等待。

    以下基础知识部分来自taobao.mysql.我个人觉得学习起这些尤其是爱看书的时候,很多知识点是紧密关联的,作者很熟悉了但是我不清楚,缺乏背景知识介绍(可以理解,书上限于篇幅不能面面俱到,照顾到每一个人),所以我补充一下,方便理解。

二 基础知识

2.1 相关概念

   Buffer Pool Instance:   

     大小等于innodb_buffer_pool_size/innodb_buffer_pool_instances,每个instance都有自己的锁,信号量,物理块(Buffer chunks)以及逻辑链表(下面的各种List),即各个instance之间没有竞争关系,可以并发读取与写入。所有instance的物理块(Buffer chunks)在数据库启动的时候被分配,直到数据库关闭内存才予以释放。当innodb_buffer_pool_size小于1GB时候,innodb_buffer_pool_instances被重置为1,主要是防止有太多小的instance从而导致性能问题。每个Buffer Pool Instance有一个page hash链表,通过它,使用space_id和page_no就能快速找到已经被读入内存的数据页,而不用线性遍历LRU List去查找。注意这个hash表不是InnoDB的自适应哈希,自适应哈希是为了减少Btree的扫描,而page hash是为了避免扫描LRU List。

  数据页:
      InnoDB中,数据管理的最小单位为页,默认是16KB,页中除了存储用户数据,还可以存储控制信息的数据。InnoDB IO子系统的读写最小单位也是页。如果对表进行了压缩,则对应的数据页称为压缩页,如果需要从压缩页中读取数据,则压缩页需要先解压,形成解压页,解压页为16KB。压缩页的大小是在建表的时候指定,目前支持16K,8K,4K,2K,1K。即使压缩页大小设为16K,在blob/varchar/text的类型中也有一定好处。假设指定的压缩页大小为4K,如果有个数据页无法被压缩到4K以下,则需要做B-tree分裂操作,这是一个比较耗时的操作。正常情况下,Buffer Pool中会把压缩和解压页都缓存起来,当Free List不够时,按照系统当前的实际负载来决定淘汰策略。如果系统瓶颈在IO上,则只驱逐解压页,压缩页依然在Buffer Pool中,否则解压页和压缩页都被驱逐。

Buffer Chunks:
       包括两部分:数据页和数据页对应的控制体,控制体中有指针指向数据页。Buffer Chunks是最低层的物理块,在启动阶段从操作系统申请,直到数据库关闭才释放。通过遍历chunks可以访问几乎所有的数据页,有两种状态的数据页除外:没有被解压的压缩页(BUF_BLOCK_ZIP_PAGE)以及被修改过且解压页已经被驱逐的压缩页(BUF_BLOCK_ZIP_DIRTY)。此外数据页里面不一定都存的是用户数据,开始是控制信息,比如行锁,自适应哈希等。

逻辑链表:
      链表节点是数据页的控制体(控制体中有指针指向真正的数据页),链表中的所有节点都有同一的属性,引入其的目的是方便管理。下面其中链表都是逻辑链表。

Free List:
      其上的节点都是未被使用的节点,如果需要从数据库中分配新的数据页,直接从上获取即可。InnoDB需要保证Free List有足够的节点,提供给用户线程用,否则需要从FLU List或者LRU List淘汰一定的节点。InnoDB初始化后,Buffer Chunks中的所有数据页都被加入到Free List,表示所有节点都可用。

LRU List:
       这个是InnoDB中最重要的链表,上一篇里面的双写介绍到脏页驱逐,刷盘策略提到的LRU就与此相关。所有新读取进来的数据页都被放在上面。链表按照最近最少使用算法排序,最近最少使用的节点被放在链表末尾,如果Free List里面没有节点了,就会从中淘汰末尾的节点。LRU List还包含没有被解压的压缩页,这些压缩页刚从磁盘读取出来,还没来的及被解压。LRU List被分为两部分,默认前5/8为young list,存储经常被使用的热点page,后3/8为old list。新读入的page默认被加在old list头,只有满足一定条件后,才被移到young list上,主要是为了预读的数据页和全表扫描污染buffer pool。

FLU List:
这个链表中的所有节点都是脏页,也就是说这些数据页都被修改过,但是还没来得及被刷新到磁盘上。在FLU List上的页面一定在LRU List上,但是反之则不成立。一个数据页可能会在不同的时刻被修改多次,在数据页上记录了最老(也就是第一次)的一次修改的lsn,即oldest_modification。不同数据页有不同的oldest_modification,FLU List中的节点按照oldest_modification排序,链表尾是最小的,也就是最早被修改的数据页,当需要从FLU List中淘汰页面时候,从链表尾部开始淘汰。加入FLU List,需要使用flush_list_mutex保护,所以能保证FLU List中节点的顺序。

Quick List:
这个链表是阿里云RDS MySQL 5.6加入的,使用带Hint的SQL查询语句,可以把所有这个查询的用到的数据页加入到Quick List中,一旦这个语句结束,就把这个数据页淘汰,主要作用是避免LRU List被全表扫描污染。

Unzip LRU List:
这个链表中存储的数据页都是解压页,也就是说,这个数据页是从一个压缩页通过解压而来的。

Zip Clean List:
这个链表只在Debug模式下有,主要是存储没有被解压的压缩页。这些压缩页刚刚从磁盘读取出来,还没来的及被解压,一旦被解压后,就从此链表中删除,然后加入到Unzip LRU List中。

Zip Free:
压缩页有不同的大小,比如8K,4K,InnoDB使用了类似内存管理的伙伴系统来管理压缩页。Zip Free可以理解为由5个链表构成的一个二维数组,每个链表分别存储了对应大小的内存碎片,例如8K的链表里存储的都是8K的碎片,如果新读入一个8K的页面,首先从这个链表中查找,如果有则直接返回,如果没有则从16K的链表中分裂出两个8K的块,一个被使用,另外一个放入8K链表中。

上面这么多list,常用的就是LRU,FLU,FREE。

2.2核心数据结构


InnoDB Buffer Pool有三种核心的数据结构:buf_pool_t,buf_block_t,buf_page_t。

but_pool_t:
存储Buffer Pool Instance级别的控制信息,例如整个Buffer Pool Instance的mutex,instance_no, page_hash,old_list_pointer等。还存储了各种逻辑链表的链表根节点。Zip Free这个二维数组也在其中。
源码在innobase/include/buf0buf.h

buf_block_t:

这个就是数据页的控制体,用来描述数据页部分的信息(大部分信息在buf_page_t中)。buf_block_t中第一字段就是buf_page_t,这个不是随意放的,是必须放在第一字段,因为只有这样buf_block_t和buf_page_t两种类型的指针可以相互转换。第二个字段是frame字段,指向真正存数据的数据页。buf_block_t还存储了Unzip LRU List链表的根节点。另外一个比较重要的字段就是block级别的mutex。源码不贴了,也在innobase/include/buf0buf.h。

buf_page_t:
这个可以理解为另外一个数据页的控制体,大部分的数据页信息存在其中,例如space_id, page_no, page state, newest_modification,oldest_modification,access_time以及压缩页的所有信息等。压缩页的信息包括压缩页的大小,压缩页的数据指针(真正的压缩页数据是存储在由伙伴系统分配的数据页上)。这里需要注意一点,如果某个压缩页被解压了,解压页的数据指针是存储在buf_block_t的frame字段里。源码也在innobase/include/buf0buf.h ,一开始搜了一圈没找到。buf_page_t { 中间有空格。

这里介绍一下buf_page_t中的state字段,这个字段主要用来表示当前页的状态。一共有八种状态,不懂先跳过了。
BUF_BLOCK_POOL_WATCH:
这种类型的page是提供给purge线程用的。这里的核心思想就是通过控制体在内存中的地址来确定数据页是否还在被使用。
BUF_BLOCK_ZIP_PAGE:(短暂状态)
当压缩页从磁盘读取出来的时候,先通过malloc分配一个临时的buf_page_t,然后从伙伴系统中分配出压缩页存储的空间,把磁盘中读取的压缩数据存入,然后把这个临时的buf_page_t标记为BUF_BLOCK_ZIP_PAGE状态(buf_page_init_for_read),只有当这个压缩页被解压了,state字段才会被修改为BUF_BLOCK_FILE_PAGE,并加入LRU List和Unzip LRU List(buf_page_get_gen)。

BUF_BLOCK_ZIP_DIRTY:(短暂状态)
如果一个压缩页对应的解压页被驱逐了,但是需要保留这个压缩页且压缩页是脏页,则被标记为BUF_BLOCK_ZIP_DIRTY(buf_LRU_free_page),如果该压缩页又被解压了,则状态会变为BUF_BLOCK_FILE_PAGE

BUF_BLOCK_NOT_USED:(长期状态)
当链表处于Free List中,状态就为此状态。是一个能长期存在的状态。

BUF_BLOCK_READY_FOR_USE:(短暂状态)
当从Free List中,获取一个空闲的数据页时,状态会从BUF_BLOCK_NOT_USED变为BUF_BLOCK_READY_FOR_USE(buf_LRU_get_free_block),也是一个比较短暂的状态。处于这个状态的数据页不处于任何逻辑链表中。

BUF_BLOCK_FILE_PAGE:(长期状态)
正常被使用的数据页都是这种状态。LRU List中,大部分数据页都是这种状态。压缩页被解压后,状态也会变成BUF_BLOCK_FILE_PAGE。

BUF_BLOCK_MEMORY:
Buffer Pool中的数据页不仅可以存储用户数据,也可以存储一些系统信息,例如InnoDB行锁,自适应哈希索引以及压缩页的数据等,这些数据页被标记为BUF_BLOCK_MEMORY。处于这个状态的数据页不处于任何逻辑链表中

BUF_BLOCK_REMOVE_HASH:(短暂状态)
当加入Free List之前,需要先把page hash移除。因此这种状态就表示此页面page hash已经被移除,但是还没被加入到Free List中,是一个比较短暂的状态。

         总体来说,大部分数据页都处于BUF_BLOCK_NOT_USED(全部在Free List中)和BUF_BLOCK_FILE_PAGE(大部分处于LRU List中,LRU List中还包含除被purge线程标记的BUF_BLOCK_ZIP_PAGE状态的数据页)状态,少部分处于BUF_BLOCK_MEMORY状态,极少处于其他状态。前三种状态的数据页都不在Buffer Chunks上,对应的控制体都是临时分配的,InnoDB把他们列为invalid state(buf_block_state_valid)。 如果理解了这八种状态以及其之间的转换关系,那么阅读Buffer pool的代码细节就会更加游刃有余。

        接下来,简单介绍一下buf_page_t中buf_fix_count和io_fix两个变量,这两个变量主要用来做并发控制,减少mutex加锁的范围。当从buffer pool读取一个数据页时候,会其加读锁,然后递增buf_page_t::buf_fix_count,同时设置buf_page_t::io_fix为BUF_IO_READ,然后即可以释放读锁。后续如果其他线程在驱逐数据页(或者刷脏)的时候,需要先检查一下这两个变量,如果buf_page_t::buf_fix_count不为零且buf_page_t::io_fix不为BUF_IO_NONE,则不允许驱逐(buf_page_can_relocate)。这里的技巧主要是为了减少数据页控制体上mutex的争抢,而对数据页的内容,读取的时候依然要加读锁,修改时加写锁。

小结:

    说了这么多概念,结合一开始介绍了缓存池的作用来理解。bp目的是为了减少IO操作,加速读写。(一个是加速读,一个是加速写)。加速读呢? 就是当需要访问一个数据页面的时候,如果这个页面已经在缓存池中,那么就不再需要访问磁盘,直接从缓冲池中就能获取这个页面的内容。加速写呢?就是当需要修改一个页面的时候,先将这个页面在缓冲池中进行修改,记下相关的重做日志,这个页面的修改就算已经完成了。至于这个被修改的页面什么时候真正刷新到磁盘,这个是Buffer Pool后台刷新线程来完成的.

     结合上面的知识点链表来看:而为了更好的管理缓存页面,引入了上面的链表.Free链表管理空闲的缓存。需要考虑客观条件的限制,因为机器的内存大小是有限的,所以当Free链表中已经没有多余的空闲,需要淘汰掉部分最近很少使用的缓存页,也就是LRU链表。而为了加速写,如果每发生一次修改就立即同步到磁盘上对应的页上,频繁的往磁盘中写数据会严重的影响程序的性能。所以页面更新是在缓存池中先进行的,每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步。为了区分Buffer Pool中哪些页是脏页需要刷新到磁盘上,使用FLU链表管理。这里的脏页修改指的此页被加载进Buffer Pool后第一次被修改,只有第一次被修改时才需要加入FLUSH链表(代码中是根据Page头部的oldest_modification == 0来判断是否是第一次修改),如果这个页被再次修改就不会再放到FLUSH链表了,因为已经存在。需要注意的是,脏页数据实际还在LRU链表中,而FLUSH链表中的脏页记录只是通过指针指向LRU链表中的脏页。并且在FLUSH链表中的脏页是根据oldest_lsn(这个值表示这个页第一次被更改时的lsn号,对应值oldest_modification,每个页头部记录)进行排序刷新到磁盘的,值越小表示要最先被刷新,避免数据不一致。

  三 实现原理

       MySQL在启动时,如果存储引擎为innodb,则innodb会初始化其所有的子系统,其中就包括了页面缓冲区子系统,通过函数buf_pool_init来实现。Buffer缓冲池可以有多个实例,可以通过配置文件中的参数innodb_buffer_pool_instances来设置,默认值为1,实现多实例的缓冲池主要是为了提高在数据页放问时的并发度。每个实例的空间大小都是相同的,也就是说系统会将整个配置的缓冲区大小按实例个数平分,然后每个实例各自进行初始化操作。

   

看下源码。

/********************************************************************//**
Creates the buffer pool.
@return DB_SUCCESS if success, DB_ERROR if not enough memory or error */
dberr_t
buf_pool_init(
/*==========*/
	ulint	total_size,	/*!< in: size of the total pool in bytes */
	ulint	n_instances)	/*!< in: number of instances */
{
	ulint		i;
    /*计算没个实例的大小:总大小/实例数*/
	const ulint	size	= total_size / n_instances;

	ut_ad(n_instances > 0);
	ut_ad(n_instances <= MAX_BUFFER_POOLS);
	ut_ad(n_instances == srv_buf_pool_instances);

	NUMA_MEMPOLICY_INTERLEAVE_IN_SCOPE;

	buf_pool_resizing = false;
	buf_pool_withdrawing = false;
	buf_withdraw_clock = 0;
    /*buf_pool_ptr 管理整个buffer pool空间,数组元素就是bp实例空间首地址*/
	buf_pool_ptr = (buf_pool_t*) ut_zalloc_nokey(
		n_instances * sizeof *buf_pool_ptr);

	buf_chunk_map_reg = UT_NEW_NOKEY(buf_pool_chunk_map_t());

	for (i = 0; i < n_instances; i++) {
        /*根据下标计算得到每一个实例的bp对象*/
		buf_pool_t*	ptr	= &buf_pool_ptr[i];
          /*初始化每一个bp实例*/
		if (buf_pool_init_instance(ptr, size, i) != DB_SUCCESS) {

			/* Free all the instances created so far. */
			buf_pool_free(i);

			return(DB_ERROR);
		}
	}

	buf_chunk_map_ref = buf_chunk_map_reg;
    /*统计最终的bp空间大小*/
	buf_pool_set_sizes();
	buf_LRU_old_ratio_update(100 * 3/ 8, FALSE);
    /*初始化bp自适应搜索系统*/
	btr_search_sys_create(buf_pool_get_curr_size() / sizeof(void*) / 64);

	return(DB_SUCCESS);
}

  从代码中可以看出,每一个实例的在buffer pool都是独立的。使用中查找方式为buf_pool_get,通过取模来映射到一个具体的实例。

/***********************mysql5.7版本****************************/
/** Returns the buffer pool instance given a page id.
@param[in]	page_id	page id
@return buffer pool */
UNIV_INLINE
buf_pool_t*
buf_pool_get(
	const page_id_t&	page_id)
{
        /* 2log of BUF_READ_AHEAD_AREA (64) */
        ulint		ignored_page_no = page_id.page_no() >> 6;

        page_id_t	id(page_id.space(), ignored_page_no);

        ulint		i = id.fold() % srv_buf_pool_instances;

        return(&buf_pool_ptr[i]);
}
/***********************以下是书上举例子的mysql5.6版本****************************//**
Returns the buffer pool instance given space and offset of page
@return buffer pool */
UNIV_INLINE
buf_pool_t*
buf_pool_get(
/*==========*/
	ulint	space,	/*!< in: space id */
	ulint	offset)	/*!< in: offset of the page within space */
{
	ulint	fold;
	ulint	index;
	ulint	ignored_offset;

	ignored_offset = offset >> 6; /* 2log of BUF_READ_AHEAD_AREA (64)*/
	fold = buf_page_address_fold(space, ignored_offset);
	index = fold % srv_buf_pool_instances;
	return(&buf_pool_ptr[index]);
}

  再来看看buf_pool_init_instance过程

/********************************************************************//**
Initialize a buffer pool instance.
@return DB_SUCCESS if all goes well. */
ulint
buf_pool_init_instance(
/*===================*/
	buf_pool_t*	buf_pool,	/*!< in: buffer pool instance */
	ulint		buf_pool_size,	/*!< in: size in bytes */
	ulint		instance_no)	/*!< in: id of the instance */
{
	ulint		i;
	ulint		chunk_size;
	buf_chunk_t*	chunk;

	ut_ad(buf_pool_size % srv_buf_pool_chunk_unit == 0);

	/* 1. Initialize general fields
	------------------------------- */
	mutex_create(LATCH_ID_BUF_POOL, &buf_pool->mutex);

	mutex_create(LATCH_ID_BUF_POOL_ZIP, &buf_pool->zip_mutex);

	new(&buf_pool->allocator)
		ut_allocator<unsigned char>(mem_key_buf_buf_pool);

	buf_pool_mutex_enter(buf_pool);

	if (buf_pool_size > 0) {
		buf_pool->n_chunks
			= buf_pool_size / srv_buf_pool_chunk_unit;
		chunk_size = srv_buf_pool_chunk_unit;

		buf_pool->chunks =
			reinterpret_cast<buf_chunk_t*>(ut_zalloc_nokey(
				buf_pool->n_chunks * sizeof(*chunk)));
		buf_pool->chunks_old = NULL;

		UT_LIST_INIT(buf_pool->LRU, &buf_page_t::LRU);
		UT_LIST_INIT(buf_pool->free, &buf_page_t::list);
		UT_LIST_INIT(buf_pool->withdraw, &buf_page_t::list);
		buf_pool->withdraw_target = 0;
		UT_LIST_INIT(buf_pool->flush_list, &buf_page_t::list);
		UT_LIST_INIT(buf_pool->unzip_LRU, &buf_block_t::unzip_LRU);

#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUG
		UT_LIST_INIT(buf_pool->zip_clean, &buf_page_t::list);
#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */

		for (i = 0; i < UT_ARR_SIZE(buf_pool->zip_free); ++i) {
			UT_LIST_INIT(
				buf_pool->zip_free[i], &buf_buddy_free_t::list);
		}

		buf_pool->curr_size = 0;
		chunk = buf_pool->chunks;

		do {
			if (!buf_chunk_init(buf_pool, chunk, chunk_size)) {
				while (--chunk >= buf_pool->chunks) {
					buf_block_t*	block = chunk->blocks;

					for (i = chunk->size; i--; block++) {
						mutex_free(&block->mutex);
						rw_lock_free(&block->lock);

						ut_d(rw_lock_free(
							&block->debug_latch));
					}

					buf_pool->allocator.deallocate_large(
						chunk->mem, &chunk->mem_pfx);
				}
				ut_free(buf_pool->chunks);
				buf_pool_mutex_exit(buf_pool);

				return(DB_ERROR);
			}

			buf_pool->curr_size += chunk->size;
		} while (++chunk < buf_pool->chunks + buf_pool->n_chunks);

		buf_pool->instance_no = instance_no;
		buf_pool->read_ahead_area =
			ut_min(BUF_READ_AHEAD_PAGES,
			       ut_2_power_up(buf_pool->curr_size /
					     BUF_READ_AHEAD_PORTION));
		buf_pool->curr_pool_size = buf_pool->curr_size * UNIV_PAGE_SIZE;

		buf_pool->old_size = buf_pool->curr_size;
		buf_pool->n_chunks_new = buf_pool->n_chunks;

		/* Number of locks protecting page_hash must be a
		power of two */
		srv_n_page_hash_locks = static_cast<ulong>(
			 ut_2_power_up(srv_n_page_hash_locks));
		ut_a(srv_n_page_hash_locks != 0);
		ut_a(srv_n_page_hash_locks <= MAX_PAGE_HASH_LOCKS);

		buf_pool->page_hash = ib_create(
			2 * buf_pool->curr_size,
			LATCH_ID_HASH_TABLE_RW_LOCK,
			srv_n_page_hash_locks, MEM_HEAP_FOR_PAGE_HASH);

		buf_pool->page_hash_old = NULL;

		buf_pool->zip_hash = hash_create(2 * buf_pool->curr_size);

		buf_pool->last_printout_time = ut_time();
	}
	/* 2. Initialize flushing fields
	-------------------------------- */

	mutex_create(LATCH_ID_FLUSH_LIST, &buf_pool->flush_list_mutex);

	for (i = BUF_FLUSH_LRU; i < BUF_FLUSH_N_TYPES; i++) {
		buf_pool->no_flush[i] = os_event_create(0);
	}

	buf_pool->watch = (buf_page_t*) ut_zalloc_nokey(
		sizeof(*buf_pool->watch) * BUF_POOL_WATCH_SIZE);
	for (i = 0; i < BUF_POOL_WATCH_SIZE; i++) {
		buf_pool->watch[i].buf_pool_index = buf_pool->instance_no;
	}

	/* All fields are initialized by ut_zalloc_nokey(). */

	buf_pool->try_LRU_scan = TRUE;

	/* Initialize the hazard pointer for flush_list batches */
	new(&buf_pool->flush_hp)
		FlushHp(buf_pool, &buf_pool->flush_list_mutex);

	/* Initialize the hazard pointer for LRU batches */
	new(&buf_pool->lru_hp) LRUHp(buf_pool, &buf_pool->mutex);

	/* Initialize the iterator for LRU scan search */
	new(&buf_pool->lru_scan_itr) LRUItr(buf_pool, &buf_pool->mutex);

	/* Initialize the iterator for single page scan search */
	new(&buf_pool->single_scan_itr) LRUItr(buf_pool, &buf_pool->mutex);

	buf_pool_mutex_exit(buf_pool);

	return(DB_SUCCESS);
}

核心方法就是buf_chunk_init,书上贴的代码是mysql 5.6版本的,

os_mem_alloc_large使用了mmp方式分配内存,截图上也给了注释,用mmap分配的内存都是虚存,使用的时候才真正分配。从mysql 5.7开始,使用了bufpool的属性allocator来分配内存。 buf_pool->allocator.allocate_large(mem_size,&chunk->mem_pfx);

这块不明白,网上的资料不多,更多还是基于5.6版本的说明,等大神解释。

/********************************************************************//**
Allocates a chunk of buffer frames.
@return chunk, or NULL on failure */
static
buf_chunk_t*
buf_chunk_init(
/*===========*/
	buf_pool_t*	buf_pool,	/*!< in: buffer pool instance */
	buf_chunk_t*	chunk,		/*!< out: chunk of buffers */
	ulint		mem_size)	/*!< in: requested size in bytes */
{
	buf_block_t*	block;
	byte*		frame;
	ulint		i;

	/* Round down to a multiple of page size,
	although it already should be. */
	mem_size = ut_2pow_round(mem_size, UNIV_PAGE_SIZE);
	/* Reserve space for the block descriptors. */
	mem_size += ut_2pow_round((mem_size / UNIV_PAGE_SIZE) * (sizeof *block)
				  + (UNIV_PAGE_SIZE - 1), UNIV_PAGE_SIZE);

	DBUG_EXECUTE_IF("ib_buf_chunk_init_fails", return(NULL););
    /*使用bp 属性allocator分配内存*/
	chunk->mem = buf_pool->allocator.allocate_large(mem_size,
							&chunk->mem_pfx);

	if (UNIV_UNLIKELY(chunk->mem == NULL)) {

		return(NULL);
	}

#ifdef HAVE_LIBNUMA
	if (srv_numa_interleave) {
		int	st = mbind(chunk->mem, chunk->mem_size(),
				   MPOL_INTERLEAVE,
				   numa_all_nodes_ptr->maskp,
				   numa_all_nodes_ptr->size,
				   MPOL_MF_MOVE);
		if (st != 0) {
			ib::warn() << "Failed to set NUMA memory policy of"
				" buffer pool page frames to MPOL_INTERLEAVE"
				" (error: " << strerror(errno) << ").";
		}
	}
#endif /* HAVE_LIBNUMA */


	/* Allocate the block descriptors from
	the start of the memory block. */
	chunk->blocks = (buf_block_t*) chunk->mem;

	/* Align a pointer to the first frame.  Note that when
	os_large_page_size is smaller than UNIV_PAGE_SIZE,
	we may allocate one fewer block than requested.  When
	it is bigger, we may allocate more blocks than requested. */
    /*frame 是物理页面,做对齐*/
	frame = (byte*) ut_align(chunk->mem, UNIV_PAGE_SIZE);
    /*chunk 计算,bp 中页面个数*/
	chunk->size = chunk->mem_pfx.m_size / UNIV_PAGE_SIZE
		- (frame != chunk->mem);

	/* Subtract the space needed for block descriptors. */
    /*给frame找合适的位置,chunk空间中前面放bp页面控制信息,后面放bp页面,这里计算出bp实例可以存放多少个页面,使得空间浪费最小 */
	{
		ulint	size = chunk->size;
        /*frame 从前往后,chunk从后往前,是为了确认frame是bp实例的第一个bp物理页面*/
		while (frame < (byte*) (chunk->blocks + size)) {
            
			frame += UNIV_PAGE_SIZE;
            /*chunk->blocks 类型是buf_block_t,根据size大小调整实际的大小*/
			size--;
		}
        /*size为bp实例中最终页面数*/
		chunk->size = size;
	}

	/* Init block structs and assign frames for them. Then we
	assign the frames to the first blocks (we already mapped the
	memory above). */
    /*从chunk开始位置存储block信息(页面控制信息)*/
	block = chunk->blocks;

	for (i = chunk->size; i--; ) {

		buf_block_init(buf_pool, block, frame);
		UNIV_MEM_INVALID(block->frame, UNIV_PAGE_SIZE);

		/* Add the block to the free list */
		UT_LIST_ADD_LAST(buf_pool->free, &block->page);

		ut_d(block->page.in_free_list = TRUE);
		ut_ad(buf_pool_from_block(block) == buf_pool);

		block++;
		frame += UNIV_PAGE_SIZE;
	}

	buf_pool_register_chunk(chunk);

#ifdef PFS_GROUP_BUFFER_SYNC
	pfs_register_buffer_block(chunk);
#endif /* PFS_GROUP_BUFFER_SYNC */
	return(chunk);
}

#ifdef UNIV_DEBUG

       从上面代码可以看出,一个缓冲池实例的内存分布是一块连续的内存空间,这块内存空间中存储了两部分内容,前面是这些数据缓存页面的控制头结构信息(buf_block_t结构),每一个页面对应一个控制头信息,所以控制头信息连续存储在一起,所以控制信息存储完成之后才是真正的缓冲页面,所以一个缓冲池实例实际所用的空间是比配置中指定的要大,因为还需要存储控制头信息的空间,所以参数:innodb_buffer_pool_bytes_data总是比innodb_buffer_pool_size.具体的分配过程,Buffer Pool从整个实例池的从后往前分配,每次分配一个页面,而控制结构是从前往后分配,每次分配一个buf_block_t结构大小,直到相遇为止这样就把一个实例初始化好了,通常中间会有空余,因为空余空间放不下一个控制结构+一个page了。下面表示的是一个缓冲池实例的内存分布情况:

      对于缓冲池中的所有页面,都有一个控制头信息与它对应,从上图可以看出,每一个ctl都控表示了一个属于自己的page的使用情况。初始化实例时当然还需要对每一个控制头信息进行初始化,也就是每一个buf_block_t结构,初始化一个页面控制信息是通过buf_block_init函数实现的,buf_block_t结构中包含了很多信息,主要包括:其对应的页面地址frame;页信息结构buf_page_t,这个结构用来描述一个页面的信息,包括所属表空间的ID号、页面号、被修改所使用的LSN(newest_modification及oldest_modification)、使用状态(现在共有9种状态)等;用来保护这个页面的互斥量mutex;访问页面时对这个页面上的锁lock(read/write)等。在初始化完每一个页面之后,需要将每一个页面加入到上面提到的空闲页链表中,因为这些页面现在的状态都是未使用(BUF_BLOCK_NOT_USED)。到现在为止,缓冲池的一个实例就算初始化完成          

四  相关源码

4.1Buf_page_get 

       这个函数极其重要,是其他模块获取数据页的外部接口函数。如果请求的数据页已经在Buffer Pool中了,修改相应信息后,就直接返回对应数据页指针,如果Buffer Pool中没有相关数据页,则从磁盘中读取。Buf_page_get是一个宏定义,源码在/innobase/include/buf0buf.h

/**************************************************************//**
NOTE! The following macros should be used instead of buf_page_get_gen,
to improve debugging. Only values RW_S_LATCH and RW_X_LATCH are allowed
in LA! */
#define buf_page_get(ID, SIZE, LA, MTR)	\
	buf_page_get_gen(ID, SIZE, LA, NULL, BUF_GET, __FILE__, __LINE__, MTR)

     真正的函数为buf_page_get_gen,参数主要为space_id, page_no, lock_type, mode以及mtr。这里主要介绍一个mode这个参数,其表示读取的方式,目前支持六种,前三种用的比较多。
 BUF_GET:
默认获取数据页的方式,如果数据页不在Buffer Pool中,则从磁盘读取,如果已经在Buffer Pool中,需要判断是否要把他加入到young list中以及判断是否需要进行线性预读。如果是读取则加读锁,修改则加写锁。
BUF_GET_IF_IN_POOL:
只在Buffer Pool中查找这个数据页,如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则直接返回空。加锁方式与BUF_GET类似。
BUF_PEEK_IF_IN_POOL:
与BUF_GET_IF_IN_POOL类似,只是即使条件满足也不把它加入到young list中也不进行线性预读。加锁方式与BUF_GET类似。
BUF_GET_NO_LATCH:
不管对数据页是读取还是修改,都不加锁。其他方面与BUF_GET类似。
BUF_GET_IF_IN_POOL_OR_WATCH:
只在Buffer Pool中查找这个数据页,如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则设置watch。加锁方式与BUF_GET类似。这个是要是给purge线程用。
BUF_GET_POSSIBLY_FREED:
这个mode与BUF_GET类似,只是允许相应的数据页在函数执行过程中被释放,主要用在估算Btree两个slot之前的数据行数。 接下来,我们简要分析一下这个函数的主要逻辑。

/** This is the general function used to get access to a database page.
@param[in]	page_id		page id
@param[in]	rw_latch	RW_S_LATCH, RW_X_LATCH, RW_NO_LATCH
@param[in]	guess		guessed block or NULL
@param[in]	mode		BUF_GET, BUF_GET_IF_IN_POOL,
BUF_PEEK_IF_IN_POOL, BUF_GET_NO_LATCH, or BUF_GET_IF_IN_POOL_OR_WATCH
@param[in]	file		file name
@param[in]	line		line where called
@param[in]	mtr		mini-transaction
@param[in]	dirty_with_no_latch
				mark page as dirty even if page
				is being pinned without any latch
@return pointer to the block or NULL */
buf_block_t*
buf_page_get_gen(
	const page_id_t&	page_id,
	const page_size_t&	page_size,
	ulint			rw_latch,
	buf_block_t*		guess,
	ulint			mode,
	const char*		file,
	ulint			line,
	mtr_t*			mtr,
	bool			dirty_with_no_latch)
{
	buf_block_t*	block;
	unsigned	access_time;
	rw_lock_t*	hash_lock;
	buf_block_t*	fix_block;
	ulint		retries = 0;
	buf_pool_t*	buf_pool = buf_pool_get(page_id);

	ut_ad(mtr->is_active());
	ut_ad((rw_latch == RW_S_LATCH)
	      || (rw_latch == RW_X_LATCH)
	      || (rw_latch == RW_SX_LATCH)
	      || (rw_latch == RW_NO_LATCH));
#ifdef UNIV_DEBUG
	switch (mode) {
	case BUF_GET_NO_LATCH:
		ut_ad(rw_latch == RW_NO_LATCH);
		break;
	case BUF_GET:
	case BUF_GET_IF_IN_POOL:
	case BUF_PEEK_IF_IN_POOL:
	case BUF_GET_IF_IN_POOL_OR_WATCH:
	case BUF_GET_POSSIBLY_FREED:
		break;
	default:
		ut_error;
	}

	bool			found;
	const page_size_t&	space_page_size
		= fil_space_get_page_size(page_id.space(), &found);

	ut_ad(found);

	ut_ad(page_size.equals_to(space_page_size));
#endif /* UNIV_DEBUG */

	ut_ad(!ibuf_inside(mtr)
	      || ibuf_page_low(page_id, page_size, FALSE, file, line, NULL));

	buf_pool->stat.n_page_gets++;
	hash_lock = buf_page_hash_lock_get(buf_pool, page_id);
loop:
	block = guess;

	rw_lock_s_lock(hash_lock);

	/* If not own buf_pool_mutex, page_hash can be changed. */
	hash_lock = buf_page_hash_lock_s_confirm(hash_lock, buf_pool, page_id);

	if (block != NULL) {

		/* If the guess is a compressed page descriptor that
		has been allocated by buf_page_alloc_descriptor(),
		it may have been freed by buf_relocate(). */

		if (!buf_block_is_uncompressed(buf_pool, block)
		    || !page_id.equals_to(block->page.id)
		    || buf_block_get_state(block) != BUF_BLOCK_FILE_PAGE) {

			/* Our guess was bogus or things have changed
			since. */
			block = guess = NULL;
		} else {
			ut_ad(!block->page.in_zip_hash);
		}
	}

	if (block == NULL) {
		block = (buf_block_t*) buf_page_hash_get_low(buf_pool, page_id);
	}

	if (!block || buf_pool_watch_is_sentinel(buf_pool, &block->page)) {
		rw_lock_s_unlock(hash_lock);
		block = NULL;
	}

	if (block == NULL) {
		/* Page not in buf_pool: needs to be read from file */

		if (mode == BUF_GET_IF_IN_POOL_OR_WATCH) {
			rw_lock_x_lock(hash_lock);

			/* If not own buf_pool_mutex,
			page_hash can be changed. */
			hash_lock = buf_page_hash_lock_x_confirm(
				hash_lock, buf_pool, page_id);

			block = (buf_block_t*) buf_pool_watch_set(
				page_id, &hash_lock);

			if (block) {
				/* We can release hash_lock after we
				increment the fix count to make
				sure that no state change takes place. */
				fix_block = block;

				if (fsp_is_system_temporary(page_id.space())) {
					/* For temporary tablespace,
					the mutex is being used for
					synchronization between user
					thread and flush thread,
					instead of block->lock. See
					buf_flush_page() for the flush
					thread counterpart. */

					BPageMutex*	fix_mutex
						= buf_page_get_mutex(
							&fix_block->page);
					mutex_enter(fix_mutex);
					buf_block_fix(fix_block);
					mutex_exit(fix_mutex);
				} else {
					buf_block_fix(fix_block);
				}

				/* Now safe to release page_hash mutex */
				rw_lock_x_unlock(hash_lock);
				goto got_block;
			}

			rw_lock_x_unlock(hash_lock);
		}

		if (mode == BUF_GET_IF_IN_POOL
		    || mode == BUF_PEEK_IF_IN_POOL
		    || mode == BUF_GET_IF_IN_POOL_OR_WATCH) {

			ut_ad(!rw_lock_own(hash_lock, RW_LOCK_X));
			ut_ad(!rw_lock_own(hash_lock, RW_LOCK_S));

			return(NULL);
		}

		if (buf_read_page(page_id, page_size)) {
			buf_read_ahead_random(page_id, page_size,
					      ibuf_inside(mtr));

			retries = 0;
		} else if (retries < BUF_PAGE_READ_MAX_RETRIES) {
			++retries;
			DBUG_EXECUTE_IF(
				"innodb_page_corruption_retries",
				retries = BUF_PAGE_READ_MAX_RETRIES;
			);
		} else {
			ib::fatal() << "Unable to read page " << page_id
				<< " into the buffer pool after "
				<< BUF_PAGE_READ_MAX_RETRIES << " attempts."
				" The most probable cause of this error may"
				" be that the table has been corrupted. Or,"
				" the table was compressed with with an"
				" algorithm that is not supported by this"
				" instance. If it is not a decompress failure,"
				" you can try to fix this problem by using"
				" innodb_force_recovery."
				" Please see " REFMAN " for more"
				" details. Aborting...";
		}

#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUG
		ut_a(fsp_skip_sanity_check(page_id.space())
		     || ++buf_dbg_counter % 5771
		     || buf_validate());
#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */
		goto loop;
	} else {
		fix_block = block;
	}

	if (fsp_is_system_temporary(page_id.space())) {
		/* For temporary tablespace, the mutex is being used
		for synchronization between user thread and flush
		thread, instead of block->lock. See buf_flush_page()
		for the flush thread counterpart. */
		BPageMutex*	fix_mutex = buf_page_get_mutex(
			&fix_block->page);
		mutex_enter(fix_mutex);
		buf_block_fix(fix_block);
		mutex_exit(fix_mutex);
	} else {
		buf_block_fix(fix_block);
	}

	/* Now safe to release page_hash mutex */
	rw_lock_s_unlock(hash_lock);

got_block:

	if (mode == BUF_GET_IF_IN_POOL || mode == BUF_PEEK_IF_IN_POOL) {

		buf_page_t*	fix_page = &fix_block->page;
		BPageMutex*	fix_mutex = buf_page_get_mutex(fix_page);
		mutex_enter(fix_mutex);
		const bool	must_read
			= (buf_page_get_io_fix(fix_page) == BUF_IO_READ);
		mutex_exit(fix_mutex);

		if (must_read) {
			/* The page is being read to buffer pool,
			but we cannot wait around for the read to
			complete. */
			buf_block_unfix(fix_block);

			return(NULL);
		}
	}

	switch (buf_block_get_state(fix_block)) {
		buf_page_t*	bpage;

	case BUF_BLOCK_FILE_PAGE:
		bpage = &block->page;
		if (fsp_is_system_temporary(page_id.space())
		    && buf_page_get_io_fix(bpage) != BUF_IO_NONE) {
			/* This suggest that page is being flushed.
			Avoid returning reference to this page.
			Instead wait for flush action to complete.
			For normal page this sync is done using SX
			lock but for intrinsic there is no latching. */
			buf_block_unfix(fix_block);
			os_thread_sleep(WAIT_FOR_WRITE);
			goto loop;
		}
		break;

	case BUF_BLOCK_ZIP_PAGE:
	case BUF_BLOCK_ZIP_DIRTY:
		if (mode == BUF_PEEK_IF_IN_POOL) {
			/* This mode is only used for dropping an
			adaptive hash index.  There cannot be an
			adaptive hash index for a compressed-only
			page, so do not bother decompressing the page. */
			buf_block_unfix(fix_block);

			return(NULL);
		}

		bpage = &block->page;

		/* Note: We have already buffer fixed this block. */
		if (bpage->buf_fix_count > 1
		    || buf_page_get_io_fix(bpage) != BUF_IO_NONE) {

			/* This condition often occurs when the buffer
			is not buffer-fixed, but I/O-fixed by
			buf_page_init_for_read(). */
			buf_block_unfix(fix_block);

			/* The block is buffer-fixed or I/O-fixed.
			Try again later. */
			os_thread_sleep(WAIT_FOR_READ);

			goto loop;
		}

		/* Buffer-fix the block so that it cannot be evicted
		or relocated while we are attempting to allocate an
		uncompressed page. */

		block = buf_LRU_get_free_block(buf_pool);

		buf_pool_mutex_enter(buf_pool);

		/* If not own buf_pool_mutex, page_hash can be changed. */
		hash_lock = buf_page_hash_lock_get(buf_pool, page_id);

		rw_lock_x_lock(hash_lock);

		/* Buffer-fixing prevents the page_hash from changing. */
		ut_ad(bpage == buf_page_hash_get_low(buf_pool, page_id));

		buf_block_unfix(fix_block);

		buf_page_mutex_enter(block);
		mutex_enter(&buf_pool->zip_mutex);

		fix_block = block;

		if (bpage->buf_fix_count > 0
		    || buf_page_get_io_fix(bpage) != BUF_IO_NONE) {

			mutex_exit(&buf_pool->zip_mutex);
			/* The block was buffer-fixed or I/O-fixed while
			buf_pool->mutex was not held by this thread.
			Free the block that was allocated and retry.
			This should be extremely unlikely, for example,
			if buf_page_get_zip() was invoked. */

			buf_LRU_block_free_non_file_page(block);
			buf_pool_mutex_exit(buf_pool);
			rw_lock_x_unlock(hash_lock);
			buf_page_mutex_exit(block);

			/* Try again */
			goto loop;
		}

		/* Move the compressed page from bpage to block,
		and uncompress it. */

		/* Note: this is the uncompressed block and it is not
		accessible by other threads yet because it is not in
		any list or hash table */
		buf_relocate(bpage, &block->page);

		buf_block_init_low(block);

		/* Set after buf_relocate(). */
		block->page.buf_fix_count = 1;

		block->lock_hash_val = lock_rec_hash(page_id.space(),
						     page_id.page_no());

		UNIV_MEM_DESC(&block->page.zip.data,
			      page_zip_get_size(&block->page.zip));

		if (buf_page_get_state(&block->page) == BUF_BLOCK_ZIP_PAGE) {
#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUG
			UT_LIST_REMOVE(buf_pool->zip_clean, &block->page);
#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */
			ut_ad(!block->page.in_flush_list);
		} else {
			/* Relocate buf_pool->flush_list. */
			buf_flush_relocate_on_flush_list(bpage, &block->page);
		}

		/* Buffer-fix, I/O-fix, and X-latch the block
		for the duration of the decompression.
		Also add the block to the unzip_LRU list. */
		block->page.state = BUF_BLOCK_FILE_PAGE;

		/* Insert at the front of unzip_LRU list */
		buf_unzip_LRU_add_block(block, FALSE);

		buf_block_set_io_fix(block, BUF_IO_READ);
		rw_lock_x_lock_inline(&block->lock, 0, file, line);

		UNIV_MEM_INVALID(bpage, sizeof *bpage);

		rw_lock_x_unlock(hash_lock);
		buf_pool->n_pend_unzip++;
		mutex_exit(&buf_pool->zip_mutex);
		buf_pool_mutex_exit(buf_pool);

		access_time = buf_page_is_accessed(&block->page);

		buf_page_mutex_exit(block);

		buf_page_free_descriptor(bpage);

		/* Decompress the page while not holding
		buf_pool->mutex or block->mutex. */

		/* Page checksum verification is already done when
		the page is read from disk. Hence page checksum
		verification is not necessary when decompressing the page. */
		{
			bool	success = buf_zip_decompress(block, FALSE);
			ut_a(success);
		}

		if (!recv_no_ibuf_operations) {
			if (access_time) {
#ifdef UNIV_IBUF_COUNT_DEBUG
				ut_a(ibuf_count_get(page_id) == 0);
#endif /* UNIV_IBUF_COUNT_DEBUG */
			} else {
				ibuf_merge_or_delete_for_page(
					block, page_id, &page_size, TRUE);
			}
		}

		buf_pool_mutex_enter(buf_pool);

		buf_page_mutex_enter(fix_block);

		buf_block_set_io_fix(fix_block, BUF_IO_NONE);

		buf_page_mutex_exit(fix_block);

		--buf_pool->n_pend_unzip;

		buf_pool_mutex_exit(buf_pool);

		rw_lock_x_unlock(&block->lock);

		break;

	case BUF_BLOCK_POOL_WATCH:
	case BUF_BLOCK_NOT_USED:
	case BUF_BLOCK_READY_FOR_USE:
	case BUF_BLOCK_MEMORY:
	case BUF_BLOCK_REMOVE_HASH:
		ut_error;
		break;
	}

	ut_ad(block == fix_block);
	ut_ad(fix_block->page.buf_fix_count > 0);

	ut_ad(!rw_lock_own(hash_lock, RW_LOCK_X));
	ut_ad(!rw_lock_own(hash_lock, RW_LOCK_S));

	ut_ad(buf_block_get_state(fix_block) == BUF_BLOCK_FILE_PAGE);

#if defined UNIV_DEBUG || defined UNIV_IBUF_DEBUG

	if ((mode == BUF_GET_IF_IN_POOL || mode == BUF_GET_IF_IN_POOL_OR_WATCH)
	    && (ibuf_debug || buf_debug_execute_is_force_flush())) {

		/* Try to evict the block from the buffer pool, to use the
		insert buffer (change buffer) as much as possible. */

		buf_pool_mutex_enter(buf_pool);

		buf_block_unfix(fix_block);

		/* Now we are only holding the buf_pool->mutex,
		not block->mutex or hash_lock. Blocks cannot be
		relocated or enter or exit the buf_pool while we
		are holding the buf_pool->mutex. */

		if (buf_LRU_free_page(&fix_block->page, true)) {

			buf_pool_mutex_exit(buf_pool);

			/* If not own buf_pool_mutex,
			page_hash can be changed. */
			hash_lock = buf_page_hash_lock_get(buf_pool, page_id);

			rw_lock_x_lock(hash_lock);

			/* If not own buf_pool_mutex,
			page_hash can be changed. */
			hash_lock = buf_page_hash_lock_x_confirm(
				hash_lock, buf_pool, page_id);

			if (mode == BUF_GET_IF_IN_POOL_OR_WATCH) {
				/* Set the watch, as it would have
				been set if the page were not in the
				buffer pool in the first place. */
				block = (buf_block_t*) buf_pool_watch_set(
					page_id, &hash_lock);
			} else {
				block = (buf_block_t*) buf_page_hash_get_low(
					buf_pool, page_id);
			}

			rw_lock_x_unlock(hash_lock);

			if (block != NULL) {
				/* Either the page has been read in or
				a watch was set on that in the window
				where we released the buf_pool::mutex
				and before we acquire the hash_lock
				above. Try again. */
				guess = block;

				goto loop;
			}

			ib::info() << "innodb_change_buffering_debug evict "
				<< page_id;

			return(NULL);
		}

		buf_page_mutex_enter(fix_block);

		if (buf_flush_page_try(buf_pool, fix_block)) {

			ib::info() << "innodb_change_buffering_debug flush "
				<< page_id;

			guess = fix_block;

			goto loop;
		}

		buf_page_mutex_exit(fix_block);

		buf_block_fix(fix_block);

		/* Failed to evict the page; change it directly */

		buf_pool_mutex_exit(buf_pool);
	}
#endif /* UNIV_DEBUG || UNIV_IBUF_DEBUG */

	ut_ad(fix_block->page.buf_fix_count > 0);

#ifdef UNIV_DEBUG
	/* We have already buffer fixed the page, and we are committed to
	returning this page to the caller. Register for debugging.
	Avoid debug latching if page/block belongs to system temporary
	tablespace (Not much needed for table with single threaded access.). */
	if (!fsp_is_system_temporary(page_id.space())) {
		ibool   ret;
		ret = rw_lock_s_lock_nowait(
			&fix_block->debug_latch, file, line);
		ut_a(ret);
	}
#endif /* UNIV_DEBUG */

	/* While tablespace is reinited the indexes are already freed but the
	blocks related to it still resides in buffer pool. Trying to remove
	such blocks from buffer pool would invoke removal of AHI entries
	associated with these blocks. Logic to remove AHI entry will try to
	load the block but block is already in free state. Handle the said case
	with mode = BUF_PEEK_IF_IN_POOL that is invoked from
	"btr_search_drop_page_hash_when_freed". */
	ut_ad(mode == BUF_GET_POSSIBLY_FREED
	      || mode == BUF_PEEK_IF_IN_POOL
	      || !fix_block->page.file_page_was_freed);

	/* Check if this is the first access to the page */
	access_time = buf_page_is_accessed(&fix_block->page);

	/* This is a heuristic and we don't care about ordering issues. */
	if (access_time == 0) {
		buf_page_mutex_enter(fix_block);

		buf_page_set_accessed(&fix_block->page);

		buf_page_mutex_exit(fix_block);
	}

	if (mode != BUF_PEEK_IF_IN_POOL) {
		buf_page_make_young_if_needed(&fix_block->page);
	}

#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUG
	ut_a(fsp_skip_sanity_check(page_id.space())
	     || ++buf_dbg_counter % 5771
	     || buf_validate());
	ut_a(buf_block_get_state(fix_block) == BUF_BLOCK_FILE_PAGE);
#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */

	/* We have to wait here because the IO_READ state was set
	under the protection of the hash_lock and not the block->mutex
	and block->lock. */
	buf_wait_for_read(fix_block);

	/* Mark block as dirty if requested by caller. If not requested (false)
	then we avoid updating the dirty state of the block and retain the
	original one. This is reason why ?
	Same block can be shared/pinned by 2 different mtrs. If first mtr
	set the dirty state to true and second mtr mark it as false the last
	updated dirty state is retained. Which means we can loose flushing of
	a modified block. */
	if (dirty_with_no_latch) {
		fix_block->made_dirty_with_no_latch = dirty_with_no_latch;
	}

	mtr_memo_type_t	fix_type;

	switch (rw_latch) {
	case RW_NO_LATCH:

		fix_type = MTR_MEMO_BUF_FIX;
		break;

	case RW_S_LATCH:
		rw_lock_s_lock_inline(&fix_block->lock, 0, file, line);

		fix_type = MTR_MEMO_PAGE_S_FIX;
		break;

	case RW_SX_LATCH:
		rw_lock_sx_lock_inline(&fix_block->lock, 0, file, line);

		fix_type = MTR_MEMO_PAGE_SX_FIX;
		break;

	default:
		ut_ad(rw_latch == RW_X_LATCH);
		rw_lock_x_lock_inline(&fix_block->lock, 0, file, line);

		fix_type = MTR_MEMO_PAGE_X_FIX;
		break;
	}

	mtr_memo_push(mtr, fix_block, fix_type);

	if (mode != BUF_PEEK_IF_IN_POOL && !access_time) {
		/* In the case of a first access, try to apply linear
		read-ahead */

		buf_read_ahead_linear(page_id, page_size, ibuf_inside(mtr));
	}

#ifdef UNIV_IBUF_COUNT_DEBUG
	ut_a(ibuf_count_get(fix_block->page.id) == 0);
#endif

	ut_ad(!rw_lock_own(hash_lock, RW_LOCK_X));
	ut_ad(!rw_lock_own(hash_lock, RW_LOCK_S));

	return(fix_block);
}

这个函数很长六七百行,而且里面调用的函数实现也复杂,需要反复的理解。牵扯的有hash,加锁,解压缩,LRU获取与维护等等。关键函数是buf_LRU_get_free_block ,值得深入学习。右图是网上找的:https://www.cnblogs.com/wingsless/p/5578727.html

以下来自taobao.mysql的解释。

  • 首先通过buf_pool_get函数依据page_id查找指定的数据页在那个Buffer Pool Instance里面,本页前面有源码。这里有个小细节page_id.page_no() >> 6,page_no的第六位被砍掉,这是为了保证一个extent的数据能被缓存到同一个Buffer Pool Instance中,便于后面的预读操作。
  • 接着,调用buf_page_hash_get_low函数在page hash中查找这个数据页是否已经被加载到对应的Buffer Pool Instance中,如果没有找到这个数据页且mode为BUF_GET_IF_IN_POOL_OR_WATCH则设置watch数据页(buf_pool_watch_set),接下来,如果没有找到数据页且mode为BUF_GET_IF_IN_POOL、BUF_PEEK_IF_IN_POOL或者BUF_GET_IF_IN_POOL_OR_WATCH函数直接返回空,表示没有找到数据页。如果没有找到数据但是mode为其他,就从磁盘中同步读取(buf_read_page)。在读取磁盘数据之前,我们如果发现需要读取的是非压缩页,则先从Free List中获取空闲的数据页,如果Free List中已经没有了,则需要通过刷脏来释放数据页,这里的一些细节我们后续在LRU模块再分析,获取到空闲的数据页后,加入到LRU List中(buf_page_init_for_read)。在读取磁盘数据之前,我们如果发现需要读取的是压缩页,则临时分配一个buf_page_t用来做控制体,通过伙伴系统分配到压缩页存数据的空间,最后同样加入到LRU List中(buf_page_init_for_read)。做完这些后,我们就调用IO子系统的接口同步读取页面数据,如果读取数据失败,我们重试100次(BUF_PAGE_READ_MAX_RETRIES)然后触发断言,如果成功则判断是否要进行随机预读(随机预读相关的细节我们也在预读预写模块分析)。
  • 接着,读取数据成功后,我们需要判断读取的数据页是不是压缩页,如果是的话,因为从磁盘中读取的压缩页的控制体是临时分配的,所以需要重新分配block(buf_LRU_get_free_block),把临时分配的buf_page_t给释放掉,用buf_relocate函数替换掉,接着进行解压,解压成功后,设置state为BUF_BLOCK_FILE_PAGE,最后加入Unzip LRU List中。
  • 接着,我们判断这个页是否是第一次访问,如果是则设置buf_page_t::access_time,如果不是,我们则判断其是不是在Quick List中,如果在Quick List中且当前事务不是加过Hint语句的事务,则需要把这个数据页从Quick List删除,因为这个页面被其他的语句访问到了,不应该在Quick List中了。
  • 接着,如果mode不为BUF_PEEK_IF_IN_POOL,我们需要判断是否把这个数据页移到young list中buf_page_make_young_if_needed,具体细节在后面LRU模块中分析。
  • 接着,如果mode不为BUF_GET_NO_LATCH,我们给数据页加上读写锁rw_lock_s_lock_inline。
  • 最后,如果mode不为BUF_PEEK_IF_IN_POOL且这个数据页是第一次访问,则判断是否需要进行线性预读(buf_read_ahead_linear线性预读相关的细节我们也在预读预写模块分析)。

4.2 LRU List中young list和old list的维护


        当LRU List链表大于512(BUF_LRU_OLD_MIN_LEN)时,在逻辑上被分为两部分,前面部分存储最热的数据页,这部分链表称作young list,后面部分则存储冷数据页,这部分称作old list,一旦Free List中没有页面了,就会从冷页面中驱逐。两部分的长度由参数innodb_old_blocks_pct控制。每次加入或者驱逐一个数据页后,都要调整young list和old list的长度(buf_LRU_old_adjust_len),同时引入BUF_LRU_OLD_TOLERANCE来防止链表调整过频繁。当LRU List链表小于512,则只有old list。 新读取进来的页面默认被放在old list头,在经过innodb_old_blocks_time后,如果再次被访问了,就挪到young list头上。一个数据页被读入Buffer Pool后,在小于innodb_old_blocks_time的时间内被访问了很多次,之后就不再被访问了,这样的数据页也很快被驱逐。这个设计认为这种数据页是不健康的,应该被驱逐。 此外,如果一个数据页已经处于young list,当它再次被访问的时候,不会无条件的移动到young list头上,只有当其处于young list长度的1/4(大约值)之后,才会被移动到young list头部,这样做的目的是减少对LRU List的修改,否则每访问一个数据页就要修改链表一次,效率会很低,因为LRU List的根本目的是保证经常被访问的数据页不会被驱逐出去,因此只需要保证这些热点数据页在头部一个可控的范围内即可。相关逻辑可以参考函数buf_page_peek_if_too_old(源码在/innobase/include/buf0buf.ic,限于篇幅不贴了)。

控制全表扫描不增加cache数据到Buffer Pool
全表扫描对Buffer Pool的影响比较大,即使有old list作用,但是old list默认也占Buffer Pool的3/8。因此,阿里云RDS引入新的语法ENGINE_NO_CACHE(例如:SELECT ENGINE_NO_CACHE count(*) FROM t1)。如果一个SQL语句中带了ENGINE_NO_CACHE这个关键字,则由它读入内存的数据页都放入Quick List中,当这个语句结束时,会删除它独占的数据页。同时引入两个参数。innodb_rds_trx_own_block_max这个参数控制使用Hint的每个事物最多能拥有多少个数据页,如果超过这个数据就开始驱逐自己已有的数据页,防止大事务占用过多的数据页。innodb_rds_quick_lru_limit_per_instance这个参数控制每个Buffer Pool Instance中Quick List的长度,如果超过这个长度,后续的请求都从Quick List中驱逐数据页,进而获取空闲数据页。
删除指定表空间所有的数据页
函数(buf_LRU_remove_pages)提供了三种模式,第一种(BUF_REMOVE_ALL_NO_WRITE),删除Buffer Pool中所有这个类型的数据页(LRU List和Flush List)同时Flush List中的数据页也不写回数据文件,这种适合rename table和5.6表空间传输新特性,因为space_id可能会被复用,所以需要清除内存中的一切,防止后续读取到错误的数据。第二种(BUF_REMOVE_FLUSH_NO_WRITE),仅仅删除Flush List中的数据页同时Flush List中的数据页也不写回数据文件,这种适合drop table,即使LRU List中还有数据页,但由于不会被访问到,所以会随着时间的推移而被驱逐出去。第三种(BUF_REMOVE_FLUSH_WRITE),不删除任何链表中的数据仅仅把Flush List中的脏页都刷回磁盘,这种适合表空间关闭,例如数据库正常关闭的时候调用。这里还有一点值得一提的是,由于对逻辑链表的变动需要加锁且删除指定表空间数据页这个操作是一个大操作,容易造成其他请求被饿死,所以InnoDB做了一个小小的优化,每删除BUF_LRU_DROP_SEARCH_SIZE个数据页(默认为1024)就会释放一下Buffer Pool Instance的mutex,便于其他线程执行。
LRU_Manager_Thread
这是一个系统线程,随着InnoDB启动而启动,作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Free List中,防止用户线程去做同步刷脏影响效率。线程每隔一定时间去做BUF_FLUSH_LRU,即首先尝试从LRU中驱逐部分数据页,如果不够则进行刷脏,从Flush List中驱逐(buf_flush_LRU_tail)。线程执行的频率通过以下策略计算:我们设定max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances,如果Free List中的数量小于max_free_len的1%,则sleep time为零,表示这个时候空闲页太少了,需要一直执行buf_flush_LRU_tail从而腾出空闲的数据页。如果Free List中的数量介于max_free_len的1%-5%,则sleep time减少50ms(默认为1000ms),如果Free List中的数量介于max_free_len的5%-20%,则sleep time不变,如果Free List中的数量大于max_free_len的20%,则sleep time增加50ms,但是最大值不超过rds_cleaner_max_lru_time。这是一个自适应的算法,保证在大压力下有足够用的空闲数据页(lru_manager_adapt_sleep_time)。

Hazard Pointer
在学术上,Hazard Pointer是一个指针,如果这个指针被一个线程所占有,在它释放之前,其他线程不能对他进行修改,但是在InnoDB里面,概念刚好相反,一个线程可以随时访问Hazard Pointer,但是在访问后,他需要调整指针到一个有效的值,便于其他线程使用。我们用Hazard Pointer来加速逆向的逻辑链表遍历。 先来说一下这个问题的背景,我们知道InnoDB中可能有多个线程同时作用在Flush List上进行刷脏,例如LRU_Manager_Thread和Page_Cleaner_Thread。同时,为了减少锁占用的时间,InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。这两个因素叠加在一起导致同一个刷脏线程刷完一个数据页A,就需要回到Flush List末尾(因为A之前的脏页可能被其他线程给刷走了,之前的脏页可能已经不在Flush list中了),重新扫描新的可刷盘的脏页。另一方面,数据页刷盘是异步操作,在刷盘的过程中,我们会把对应的数据页IO_FIX住,防止其他线程对这个数据页进行操作。我们假设某台机器使用了非常缓慢的机械硬盘,当前Flush List中所有页面都可以被刷盘(buf_flush_ready_for_replace返回true)。我们的某一个刷脏线程拿到队尾最后一个数据页,IO fixed,发送给IO线程,最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中,它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷,跳过,开始刷倒数第二个数据页,同样IO fixed,发送给IO线程,然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完),直到扫描到倒数第三个数据页。所以,存在一种极端的情况,如果磁盘比较缓慢,刷脏算法性能会从O(N)退化成O(N*N)。 要解决这个问题,最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决,方法如下:遍历找到一个可刷盘的数据页,在锁释放之前,调整Hazard Pointer使之指向Flush List中下一个节点,注意一定要在持有锁的情况下修改。然后释放锁,进行刷盘,刷完盘后,重新获取锁,读取Hazard Pointer并设置下一个节点,然后释放锁,进行刷盘,如此重复。当这个线程在刷盘的时候,另外一个线程需要刷盘,也是通过Hazard Pointer来获取可靠的节点,并重置下一个有效的节点。通过这种机制,保证每次读到的Hazard Pointer是一个有效的Flush List节点,即使磁盘再慢,刷脏算法效率依然是O(N)。 这个解法同样可以用到LRU List驱逐算法上,提高驱逐的效率。相应的Patch是在MySQL 5.7上首次提出的,阿里云RDS把其Port到了我们5.6的版本上,保证在大并发情况下刷脏算法的效率。

Page_Cleaner_Thread
这也是一个InnoDB的后台线程,主要负责Flush List的刷脏,避免用户线程同步刷脏页。与LRU_Manager_Thread线程相似,其也是每隔一定时间去刷一次脏页。其sleep time也是自适应的(page_cleaner_adapt_sleep_time),主要由三个因素影响:当前的lsn,Flush list中的oldest_modification以及当前的同步刷脏点(log_sys->max_modified_age_sync,有redo log的大小和数量决定)。简单的来说,lsn - oldest_modification的差值与同步刷脏点差距越大,sleep time就越长,反之sleep time越短。此外,可以通过rds_page_cleaner_adaptive_sleep变量关闭自适应sleep time,这是sleep time固定为1秒。 与LRU_Manager_Thread每次固定执行清理innodb_LRU_scan_depth个数据页不同,Page_Cleaner_Thread每次执行刷的脏页数量也是自适应的,计算过程有点复杂(page_cleaner_flush_pages_if_needed)。其依赖当前系统中脏页的比率,日志产生的速度以及几个参数。innodb_io_capacity和innodb_max_io_capacity控制每秒刷脏页的数量,前者可以理解为一个soft limit,后者则为hard limit。innodb_max_dirty_pages_pct_lwm和innodb_max_dirty_pages_pct_lwm控制脏页比率,即InnoDB什么脏页到达多少才算多了,需要加快刷脏频率了。innodb_adaptive_flushing_lwm控制需要刷新到哪个lsn。innodb_flushing_avg_loops控制系统的反应效率,如果这个变量配置的比较大,则系统刷脏速度反应比较迟钝,表现为系统中来了很多脏页,但是刷脏依然很慢,如果这个变量配置很小,当系统中来了很多脏页后,刷脏速度在很短的时间内就可以提升上去。这个变量是为了让系统运行更加平稳,起到削峰填谷的作用。相关函数,af_get_pct_for_dirty和af_get_pct_for_lsn。

4.3预读和预写


如果一个数据页被读入Buffer Pool,其周围的数据页也有很大的概率被读入内存,与其分开多次读取,还不如一次都读入内存,从而减少磁盘寻道时间。在官方的InnoDB中,预读分两种,随机预读和线性预读。

随机预读:
这种预读发生在一个数据页成功读入Buffer Pool的时候(buf_read_ahead_random)。在一个Extent范围(1M,如果数据页大小为16KB,则为连续的64个数据页)内,如果热点数据页大于一定数量,就把整个Extend的其他所有数据页(依据page_no从低到高遍历读入)读入Buffer Pool。这里有两个问题,首先数量是多少,默认情况下,是13个数据页。接着,怎么样的页面算是热点数据页,阅读代码发现,只有在young list前1/4的数据页才算是热点数据页。读取数据时候,使用了异步IO,结合使用OS_AIO_SIMULATED_WAKE_LATER和os_aio_simulated_wake_handler_threads便于IO合并。随机预读可以通过参数innodb_random_read_ahead来控制开关。此外,buf_page_get_gen函数的mode参数不影响随机预读。

线性预读:
这中预读只发生在一个边界的数据页(Extend中第一个数据页或者最后一个数据页)上(buf_read_ahead_linear)。在一个Extend范围内,如果大于一定数量(通过参数innodb_read_ahead_threshold控制,默认为56)的数据页是被顺序访问(通过判断数据页access time是否为升序或者逆序来确定)的,则把下一个Extend的所有数据页都读入Buffer Pool。读取的时候依然采用异步IO和IO合并策略。线性预读触发的条件比较苛刻,触发操作的是边界数据页同时要求其他数据页严格按照顺序访问,主要是为了解决全表扫描时的性能问题。线性预读可以通过参数innodb_read_ahead_threshold来控制开关。此外,当buf_page_get_gen函数的mode为BUF_PEEK_IF_IN_POOL时,不触发线性预读。 InnoDB中除了有预读功能,在刷脏页的时候,也能进行预写(buf_flush_try_neighbors)。当一个数据页需要被写入磁盘的时候,查找其前面或者后面邻居数据页是否也是脏页且可以被刷盘(没有被IOFix且在old list中),如果可以的话,一起刷入磁盘,减少磁盘寻道时间。预写功能可以通过innodb_flush_neighbors参数来控制。不过在现在的SSD磁盘下,这个功能可以关闭。

Double Write Buffer(dblwr)


服务器突然断电,这个时候如果数据页被写坏了(例如数据页中的目录信息被损坏),由于InnoDB的redolog日志不是完全的物理日志,有部分是逻辑日志,因此即使奔溃恢复也无法恢复到一致的状态,只能依靠Double Write Buffer先恢复完整的数据页。Double Write Buffer主要是解决数据页半写的问题,如果文件系统能保证写数据页是一个原子操作,那么可以把这个功能关闭,这个时候每个写请求直接写到对应的表空间中。 Double Write Buffer大小默认为2M,即128个数据页。其中分为两部分,一部分留给batch write,另一部分是single page write。前者主要提供给批量刷脏的操作,后者留给用户线程发起的单页刷脏操作。batch write的大小可以由参数innodb_doublewrite_batch_size控制,例如假设innodb_doublewrite_batch_size配置为120,则剩下8个数据页留给single page write。 假设我们要进行批量刷脏操作,我们会首先写到内存中的Double Write Buffer(也是2M,在系统初始化中分配,不使用Buffer Chunks空间),如果dblwr写满了,一次将其中的数据刷盘到系统表空间指定位置,注意这里是同步IO操作,在确保写入成功后,然后使用异步IO把各个数据页写回自己的表空间,由于是异步操作,所有请求下发后,函数就返回,表示写成功了(buf_dblwr_add_to_batch)。不过这个时候后续的写请求依然会阻塞,知道这些异步操作都成功,才清空系统表空间上的内容,后续请求才能被继续执行。这样做的目的就是,如果在异步写回数据页的时候,系统断电,发生了数据页半写,这个时候由于系统表空间中的数据页是完整的,只要从中拷贝过来就行(buf_dblwr_init_or_load_pages)。 异步IO请求完成后,会检查数据页的完整性以及完成change buffer相关操作,接着IO helper线程会调用buf_flush_write_complete函数,把数据页从Flush List删除,如果发现batch write中所有的数据页都写成了,则释放dblwr的空间。

Buddy伙伴系统
与内存分配管理算法类似,InnoDB中的伙伴系统也是用来管理不规则大小内存分配的,主要用在压缩页的数据上。前文提到过,InnoDB中的压缩页可以有16K,8K,4K,2K,1K这五种大小,压缩页大小的单位是表,也就是说系统中可能存在很多压缩页大小不同的表。使用伙伴体统来分配和回收,能提高系统的效率。 申请空间的函数是buf_buddy_alloc,其首先在zip free链表中查看指定大小的块是否还存在,如果不存在则从更大的链表中分配,这回导致一些列的分裂操作。例如需要一块4K大小的内存,则先从4K链表中查找,如果有则直接返回,没有则从8K链表中查找,如果8K中还有空闲的,则把8K分成两部分,低地址的4K提供给用户,高地址的4K插入到4K的链表中,便与后续使用。如果8K中也没有空闲的了,就从16K中分配,16K首先分裂成2个8K,高地址的插入到8K链表中,低地址的8K继续分裂成2个4K,低地址的4K返回给用户,高地址的4K插入到4K的链表中。假设16K的链表中也没有空闲的了,则调用buf_LRU_get_free_block获取新的数据页,然后把这个数据页加入到zip hash中,同时设置state状态为BUF_BLOCK_MEMORY,表示这个数据页存储了压缩页的数据。 释放空间的函数是buf_buddy_free,相比于分配空间的函数,有点复杂。假设释放一个4K大小的数据块,其先把4K放回4K对应的链表,接着会查看其伙伴(释放块是低地址,则伙伴是高地址,释放块是高地址,则伙伴是低地址)是否也被释放了,如果也被释放了则合并成8K的数据块,然后继续寻找这个8K数据块的伙伴,试图合并成16K的数据块。如果发现伙伴没有被释放,函数并不会直接退出而是把这个伙伴给挪走(buf_buddy_relocate),例如8K数据块的伙伴没有被释放,系统会查看8K的链表,如果有空闲的8K块,则把这个伙伴挪到这个空闲的8K上,这样就能合并成16K的数据块了,如果没有,函数才放弃合并并返回。通过这种relocate操作,内存碎片会比较少,但是涉及到内存拷贝,效率会比较低。

Buffer Pool预热
这个也是官方5.6提供的新功能,可以把当前Buffer Pool中的数据页按照space_id和page_no dump到外部文件,当数据库重启的时候,Buffer Pool就可以直接恢复到关闭前的状态。

Buffer Pool Dump:
遍历所有Buffer Pool Instance的LRU List,对于其中的每个数据页,按照space_id和page_no组成一个64位的数字,写到外部文件中即可(buf_dump)。

Buffer Pool Load:
读取指定的外部文件,把所有的数据读入内存后,使用归并排序对数据排序,以64个数据页为单位进行IO合并,然后发起一次真正的读取操作。排序的作用就是便于IO合并(buf_load)。

********************************

总结,本来书上只是简单介绍了Buffer Pool的初始化过程及数据结构,看了眼taobao.mysql的介绍,发现太晦涩难懂了,这么多知识点,即使整理完看一遍源码脑子也只残留了LRU List和Flush List ,其他那些hash啊,加锁优化啊,解压缩啥的,都看不懂了。来日再战吧。先哭一会。

参考:

http://mysql.taobao.org/monthly/2017/05/01/

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/81299183