前言
谈存储引擎前,希望读者先去了解事务与锁的基本概念,这样会对阅读InnoDB存储引擎有更好的帮助。
特性
- 行锁。如果你的数据库想应用在高并发的场景下,那么你用来保证事务安全的锁粒度必须尽可能小.对比表锁,InnoDB提供了行锁设计。这使得发生INSERT、UPDATE、DELETE的操作时,InnoDB只需要锁定很小的一块区域即可保证事务安全,当然,想要达到这个效果,想要开发者了解索引知识和更好地应用索引。
- MVCC,多版本并发控制。
- 外键约束,用来保证数据的逻辑一致性,高并发的场景下一把不推荐使用外键约束,在业务层实现相关逻辑。
- 一致性非锁定读,即在InnoDB下,SELECT操作是默认不加锁的,除非你显示声明SELECT FOR UPDATE等操作.
结构
InnoDB内部维护了一个缓冲池:
- 主要负责维护所有进程/线程需要访问的多个内部数据结构.
- 缓存磁盘上的数据,方便快速地读取,同时在对磁盘文件的数据修改之前在内存池缓存.
- redo log缓冲
同时,InnoDB是多线程的模型,多个线程处理不同的任务.这些后台线程的作用如下:
image.png
- 负责刷新内存池中的数据,让缓冲池的数据与磁盘保持一致.
- 由于内存池中缓存了日志文件,所以事务发生异常的时候,后台线程会保证InnoDB能恢复正常.
这里简单介绍一下每个线程的作用:
- Master Thread
负责将缓冲池的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收。- IO Thread
InnoDB中大量使用了AIO来处理写IO请求,进而提升数据库的性能。这个线程的任务就是处理这些请求的回调。可以通过以下指令来查看当前数据库的IO ThreadSHOW ENGINE INNODB STATUS
- Purge Thread
事务提交后,所产生的undolog可能需要被回收。根据InnoDB的版本不同,老一些的版本这件事情是在Master Thread中进行的,在InnoDB 1.1后,这项任务交由Purage Thread来完成。- Purge Thread
这个线程是InnoDB 1.2后引入。负责分担Master Thread的脏页刷新操作。
内存
1. 缓冲池
InnoDB是基于磁盘存储的,使用页来管理其中的记录。由于CPU、内存、磁盘之间的速度有着量级的差异,所以为了让查询可以被更快地返回,通常需要引入内存来提高数据库的性能。
内存读
image.png更新内存中的数据
对于数据库中页的修改,会先修改缓冲池中的页,再以一定的频率刷新到磁盘上.
InnoDB并不保证每次页发生改变就立刻刷新回磁盘,而是内置了一种Checkpoint机制进行磁盘的回刷。
从上面的分析中我们知道,缓冲池的大小直接影响数据库的整体性能。同时,内存大小的上限也受到操作系统的影响:32位的操作系统,最多设置为3G。因此,如果你希望MySQL性能更好,一定要使用64位的操作系统.
查看缓冲池信息
# 查看缓冲池参数,这是以字节为基础单位的
mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
# 如果你希望看到以GB的形式展现,可以这样写
mysql> SELECT @@innodb_buffer_pool_size/1024/1024/1024;
# 查看InnoDB缓冲池中包含数据的页面数
mysql> SHOW STATUS LIKE 'innodb_buffer_pool_pages_data';
# 查看InnoDB页面大小(默认为16KB)。
mysql> SHOW STATUS LIKE 'innodb_page_size';
# 查看InnoDB缓冲池中的页数量。
mysql> SHOW STATUS LIKE 'innodb_buffer_pool_pages_total';
# 查看缓冲池实例数量
mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';
配置缓冲池
这里涉及到缓冲池、缓冲池块(innodb_buffer_pool_chunk_size)、缓冲池实例的概念.
缓冲池块
当InnoDB缓冲池很大时,可以通过从内存中检索来满足许多数据请求。使用散列函数,将存储在缓冲池中或从缓冲池读取的每个页面随机分配给其中一个缓冲池。每个缓冲池管理自己的空闲列表,刷新列表,LRU和连接到缓冲池的所有其他数据结构,并受其自己的缓冲池互斥量保护。
你可以直接设置整个缓冲池的大小,但是会受到缓冲池块的影响。MySQL官方规定:
缓冲池的大小 = 缓冲池块大小 * 缓冲池实例数量 * N
这如何理解呢?假设一个缓冲池总共分配了8G,然后分配了8个缓冲池实例,然后缓冲池块大小为128M,这是有效的。因为8G是8*128M的倍数,此时每个缓冲池实例占用1G。
然而,如果你设置缓冲池实例为9G,那么MySQL会帮你调整为10G.
# MySQL默认的缓冲池只有128M
# 设置缓冲池的大小,这里为2G,根据实际情况而定.
# 如果你的服务器是16G以上的,建议加大到4G或者8G,当你加大到8G的时候,建议将缓冲池实例数也同时加大。
mysql> SET GLOBAL innodb_buffer_pool_size = 2147483648;
my.cnf中配置缓冲池块大小
[mysqld]
innodb_buffer_pool_chunk_size=134217728
注意,设置的缓冲池块大小不可以超过缓冲池实例的大小,同时要考虑是否切合公式(缓冲池的大小 = 缓冲池块大小 * 缓冲池实例数量 * N),否则会被MySQL强制调整.
配置完成后,可以通过SHOW ENGINE INNODB STATUS ;
指令查看当前MySQL的情况。
InnoDB内存数据对象
image.png内存管理LRU
数据库的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法进行管理的。最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取的页的时候,将首先释放LRU列表中尾端的页。
MySQL特殊的LRU算法
MySQL的LRU算法是区别于传统的LRU算法的,通常我们称为midpoint insertion strategy算法:
它会在LUR列表中标记一个位置,这个位置为整个LRU长度的5/8.这是由innodb_old_blocks_pct
参数来控制的,你可以通过以下指令来查看这个参数.
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
如果你认为自己的热点数据不止63%,那么在你可以将阈值进行调整
mysql> SET GLOBAL innodb_old_blocks_pct=20;
- 当InnoDB要将页面写入缓冲池中时,它首先将其插入中点。
- 前面5/8的数据,视为年轻代,后面3/8的数据,视为老年代。
-
当访问老年代的数据时,会使页变得年轻,将其移至年轻代的头部,而如果长时间没有受到访问,那么页将会老化,直到内存紧张时从LRU列表老年代中移除。
image.png
为什么不直接使用LRU?
目的是确保经常访问的(“ 热 ”)页保留在缓冲池中,即使预读和 全表扫描会带来新的块,这些块以后也可能不会被访问
- 防止因某些SQL操作将大量的页被刷出,从而造成缓冲池污染,影响缓冲池的效率。常见于索引或者数据的扫描操作,它们往往需要访问表中的许多页,甚至所有页。而这些页并不是需要真正返回的或者仅仅是当前查询需要返回,那么这不是活跃的热点数据。而真正的热点数据此时被移除了,那么InnoDB需要再次访问硬盘。
- 预读失效.MySQL会将未来要读取的页提前加载进内存,省去后续的IO。但是这些加载的数据可能并不是应用程序所需要的,所以不能判定为是热点数据.MySQL将将预读数据加载进缓冲池后,并不马上把它放入LRU的首部,而是从midpoint进行插入,真正被读取的页才放入LRU的首部.
MySQL官方解释道:
防止缓冲池被预读搅动的优化,可以避免由于表或索引扫描而引起的类似问题。在这些扫描中,通常快速连续地访问数据页面几次,并且再也不会被访问。
ok,MySQL采用了midpoint来区分年轻代和老年代解决了预读失效的问题,那么由于大量的扫描导致内存的热数据被移除这件事,MySQL是如何解决的呢?
MySQL使用了
innodb_old_blocks_time
来决定数据是否数据是否加入LRU列表的热端。这个参数用于表示页读取到mid位置后需要等待多久才会被加入热点数据区域。
你可以通过以下指令开设定这个时间
SET GLOBAL innodb_old_blocks_time=1000;
LRU列表
image.pngLRU列表用来管理已经读取的页,当数据库启动的时候,LRU列表是空的,而Free列表则是存放所有的页。每当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,如果有则将页从Free列表放入LRU列表中,否则LRU列表则根据LRU算法进行内存淘汰。
你可以通过以下指令查看LRU列表和Free列表的使用情况
mysql> SHOW ENGINE INNODB STATUS;
其中,Database pages表示LRU列表中的页数量,Free buffers则表示Free列表中的页数量,此外你应该还关注到一个变量-Buffer pool hit rate,表示缓冲池的命中率。通常来说,这个数值不应该小于95%。如果出现异常,需要观察是否由于全表扫描引起的LRU列表被污染的问题。
在InnoDB1.2版本后,还支持通过查询INNODB_BUFFER_POOL_STATS
来获取缓冲池的运行状态.
mysql> SELECT pool_id,hit_rate,pages_made_young,pages_not_made_young FROM
-> information_schema.INNODB_BUFFER_POOL_STATS;
压缩页
InnoDB存储引擎从1.0.x版本开始支持压缩页的功能,即将原来16KB的页压缩为1KB、2KB、3KB、4KB和8KB.而由于页的大小发生了变化,LRU列表也发生了改变。其中,对于小于16KB的页,InnoDB是通过unzip_LRU列表进行管理的。通过SHOW ENGINE INNODB STATUS可以获悉详情。LRU列表中的页包含了unzip_LRU列表的页。
unzip_LRU列表获取内存的过程
比如当前unzip_LRU列表需要申请4KB的大小。
- 检查当前4KB的unzip_LRU列表,检查是否有可用的空闲页。
- 若有,则直接使用。否则检查8KB的列表中是否有可用的空闲页。
- 如果8KB中有空闲页,那么将8KB拆分成2个4KB,一个使用,另一个存入4KB列表的空闲区。
- 如果没有,则继续往上查找空闲页,进行拆分。
脏页
在LRU列表中的页被修改后,那么该页就视为脏页(dirty page):
例如现在InnoDB进行了内存插入操作,但是该数据还没有持久化到硬盘,那这个刚插入的页就是脏页。这是由于当前内存的数据与硬盘的数据不一致导致的。
MySQL提供了一种CheckPoint机制将脏页刷新到硬盘。而Flush列表中的页即为脏页。需要注意的是,脏页即存在于LRU列表中,也存在于Flush列表中。
你可以通过SHOW ENGINE INNODB STATUS
来查看当前Flush列表的情况,其中的Modified db pages参数表示了当前脏页的数量.
redo log buffer(重做日志缓冲)
InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区中,然后按一定频率将其刷新到重做日志文件中。redo log buffer一般不需要设置得过大,因为它刷新的频率是很高的(1S),InnoDB默认设置为8MB,通常情况下,这是足够的。
触发redo log buffer刷新的时机
- Master Thread每一秒会刷新一次
- 每个事务提交的时候会刷新一次
- 当redo log buffer剩余空间不足一半的时候,刷新一次
额外的内存池
InnoDB存储引擎中,对内存的管理是通过一种称为内存堆的方式进行的。在对数据结构本身的内存进行分配时,也需要从额外的内存池中进行申请,当这个区域不够的时候,会从缓冲池中申请。主要用于记录:缓冲池中的帧缓冲,缓冲控制对象(LRU、lock、wait)。
对缓冲池进行扩展的时候,建议也同时扩展这个区域
网友评论