innodb增删改查过程解析

Innodb运行时结构

*innodb引擎架构大致如下图:

 

可以看到主要是两部分组成,一个是许多后台线程,一个是innodb的缓存内存池。

*后台线程主要包括如下:1.master thread,这是innodb最核心的一个线程,在早期的innodb版本master thread承担着脏页刷新、合并插入缓冲、刷新redo日志、回收undo日志等作用,在之后的版本用page cleaner thread来进行脏页刷新,purge thread来进行undo日志回收,从而减轻了master thread的负担,也减少了查询线程的阻塞。

master thread执行顺序的伪代码可以形容如下:

loop:

循环10次:

睡眠1秒;

刷新redo日志到硬盘;

如果最近1秒io负载不大则merge5%innodb_io_capacity的插入缓冲;

如果脏页比例大于75%(innodb_max_dirty_pages_pct)则刷新innodb_io_capacity的脏页;

如果没有任何用户活动则进入backgroud loop;

循环结束;

合并5%innodb_io_capacity的插入缓存;

刷新redo日志到硬盘;

回收undo日志页;

goto loop;

background loop:

回收undo日志页;

合并100%innodb_io_capacity的插入缓冲;

如果有用户活动goto loop,否则goto flush loop;

flush loop:

刷新innodb_io_capacity的脏页直到脏页比例小于innodb_max_dirty_pages_pct;

goto suspend thread();

waiting event

goto loop;

 

2.IO thread,它包括read ,write,insert buffer和log IO thread。这些线程基本上都使用的是异步IO来处理IO请求。可以通过innodb_xx_io_threads来控制read和write线程数量,默认都是4。实际的写入硬盘操作都是交给IO thread完成。

3.purge thread,完成对undo页的回收。由于mysql默认事务级别为可重复读,因而MVCC更新操作会留下一个旧版本数据,删除操作也仅仅是标记删除,使得事务可以根据版本id来判断是否读取过期数据,从而实现可重复读。当事务提交后,undo日志之前记录的就可能不再需要了,这就需要purge线程来删除,当然,如果仍然有事务在使用旧版本数据,purge会跳过这些数据。purge的大概过程是定期扫描undo,从老的undo往新undo一路读取,对于每条undo记录判断其对应记录是否可以被purge(通过read view,这个read view应该是在purge刚开始运行时生成的,与undo log的事务id进行判断是否可以purge),purge的顺序是先purge次级索引,然后聚集索引。次级索引purge是个耗时的过程,因为undo中只记录了索引列,并不像聚集索引记录了spaceid page no,因此需要进行查找。

4.page cleaner thread,将master thread的刷新脏页工作分到了这里。主要工作就是循环判断脏页比例是否大于某一定值,如果大于那么是否server active(是否有用户活动)或者是否有挂起的读io请求或者上次刷新的页是否为0,如果这三个有一个成立那么thread先休眠一阵子,如果不满足则开始刷新活动,任然先检查是否有用户活动,如果没有那么先检查是否需要回收LRU尾部100个页,然后再刷新最多max io capacity个脏页,如果有用户活动那么只刷新io capacity个脏页。这里的用户活动(比如一个update)对刷新影响很大,感觉如果是需要全力刷新脏页的时候(比如shutdown时)会造成瓶颈。

*innodb的内存分布主要包括1.buffer pool。2..redo log buffer.。3.额外内存池。

1.buffer pool的主要结构如下图:

 

其中的数据页和索引页主要通过LRU LIST、FREE LIST和FLUSH LIST数据结构来维护。LRU即latest recent used最近最少使用链表,总是把使用最频繁的数据放在链表最前面,也就是前5/8,把使用最少的放在后3/8,新读取的数据总是放在分界点,以避免类似全表扫描等操作读入了这一次数据后以后就不再使用了,这样非热点数据放在链表首部,反而热点数据被排到后面甚至被丢弃。FREE LIST用来管理空闲的内存空间,这个数据结构在很多地方都有应用,比如说SGI STL的次级空间分配器,主要好处是管理方便可以减少空间浪费和空间碎片,每次请求内存都从这儿分配页,释放后的页也重新回到这里统一管理。FLUSH LIST主要是一些要刷新的脏页。

LRU和FLU LIST中的脏页刷新主要由checkpoint机制实现,checkpoint主要包括两种,一个是sharp checkpoint,是在innodb关闭的时候刷新缓冲池到硬盘,一种是fuzzy checkpoint,分为四种master checkpoint,flush_lru_list checkpoint,async/sync checkpoint,dirty page too much checkpoint。fuzzy checkpoint在innodb正常运行时出现,其中第一种即前面介绍的每1/10秒刷新机制,一种是当LRU LIST中空闲页数少于一定值时就会释放最后的一部分页,如果其中有脏页则触发该checkpoint,第三种是重做日志快要满的时候(循环写入发现将要写入的部分还没刷新到硬盘),强制刷新脏页,第四种前面有介绍。

Checkpoint的版本由LSN(log sequence number)控制,缓冲池中的每个页都有一个LSN,redo日志中也有LSN,当前最新刷新到硬盘的LSN记录在redo日志的最前面,redo日志根据LSN可以知道哪些数据已经刷新到了硬盘,从而判断日志空间是否足够,当innodb崩溃后,redo日志可以根据最后刷新的LSN来知道该从哪里开始恢复数据。

 

查询过程

*查询语句在优化器优化后生成执行计划,do_select()根据要查询的每一条记录的条件调用相应引擎API,这里是调用ha_innobase::rnd_next()读取下一条记录,rnd_next()调用相关函数从数据库中读取数据,大致的过程是首先调用row_sel_try_search_shortcut_for_mysql()从自适应hash索引中查询相关记录,如果查询到了则将行记录返回,如果没有则根据页号(有个疑问就是页号怎么知道的?如果知道页号了那不就知道了页在硬盘中的位置了吗)查询缓冲池LRU等链表中的数据,如果再没有就只有去硬盘中查找了。

自适应hash索引由innodb自己生成,不需要人为控制。自适应索引以页号为键值,只有当页满足它的访问模式是固定的,并且该模式访问了100次,且页通过该模式访问了1/16*页中记录次才能将页加入自适应索引中。访问模式是指where a=xxx的语句模式,即不能where a=xxx和where a=xxx and b=xxx这两种模式交替。

 

插入过程

*innodb引擎插入对于主键索引来说是简单的,因为主键索引中的数据按照主键自增顺序排列,插入的时候只需要在最后按照顺序插入即可,但是每次插入不仅仅需要更新主键索引,还需要更新其他的次级索引,次级索引的数据插入顺序就不一定是顺序的了,因此每次插入都需要查询位置,并且每次插入的位置都是随机的,因此若是每一次插入都要进行一次这些步骤就会造成很大的资源消耗。Innodb对于这个问题的解决方法是插入缓冲(insert buffer,后来随着update和delete缓冲加入改为change buffer)。

每当插入一条新的记录时,先在缓冲池中查找是否有对应的索引叶子页,如果有则直接将记录加入该页中,如果没有则先把记录放在插入缓冲中,插入缓冲是一个存在于缓冲池中的b+树,树的键值是表号和页号,通过它可以唯一确定一个页。B+树最小单位是页,记录就存在这些页中,合并操作会在目标页读入缓冲或者每1/10s的循环中进行。插入缓冲的使用需要满足索引是非唯一的这个条件,因为如果唯一索引插入需要去表查询记录是否唯一,这样与避免回表查询的初衷相悖了。

记录的结构包括4字节的space id对应表,1字节marker,4字节的页号对应表中具体页的位置,还包括2字节counter表明这是该页的第几条insert buffer记录,用来表示顺序,1字节表明操作类型(insert,delete,purge),1字节flag,总共13字节的多余,后面跟具体的data。

为了保证合并的时候一定能够成功,而不会出现空间不足而分裂等操作,共享表空间维护了一个insert buffer bitmap,其中每一个数据结构都对应一个页,大小为4bit,其中2bits表明页中还有多少空间,1bit表明该页是否存在与插入缓冲中,每次有新页读入缓冲池都会根据该值判断是否需要读取插入缓冲页中的记录进行合并,最后1bit标记缓冲类型(insert,delete还是update buffer)。

合并的操作主要分为两步:1.主线程发出异步IO请求,异步读取需要被merge的页面。2.在收到IO完成后进行merge操作。第一步首先读取出插入缓存中对应页的所有记录以及space id,page no,根据页号等去硬盘异步io读取相应的页。第二步是在相应页被读取过来后根据插入缓存中的修改记录对读取页进行修改,并将index page的最大键值系统列也进行修改,完成后删除插入缓存中的记录,并修改bitmap,将修改后的页放回缓冲池。

更新过程

*由于多版本控制,实际上对于一条记录的更新分为两个步:1.处理旧纪录。2.插入一条新的纪录。旧记录由purge线程控制回收。对于更新操作,插入缓存也有优化,具体是否对更新操作使用插入缓存由innodb_change_buffering控制。具体过程类似插入过程,即首先判断需要更新的页是否在缓冲池,如果在则根据上面的两步直接更新,如果不存在,则先放入插入缓存。

有些人可能有疑问就是如果更新操作放在插入缓存,那么其他事务怎么知道记录已经更新的事实呢?根据我的理解,如果有事务要查询某一页,根据前面查询过程的分析,如果页在缓冲池中,那么直接获取该页,因为该页在缓冲池中,那么更新操作肯定已经合并到了该页中,所以只需要根据事务版本来判断是否读取记录即可;如果不在缓冲池中,那么查询线程先从硬盘中读取对应页到缓冲池中,此时会触发插入缓存的merge操作,还是会将插入缓存中的页记录merge到刚读取的页中。

删除过程

*删除操作也不是直接就删除了,而是先标记为delete_mark,由purge线程来控制回收标记为delete_mark的记录。需要注意的是,如果当前有事务正在使用标记为删除的记录,那么purge线程在该轮回收中是不会删除该记录的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值