热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

mysql图片的存储和读取_MySQL是如何对LRU算法进行优化的?又该如何对MySQL进行调优?...

1.开篇MySQL在查询数据时,对于InnoDB存储引擎而言,会先将磁盘上的数据以页为单位,先将数据页加载进内存,然后以缓存
2cf8cd3bf34be96bf6afe9528157ee9c.png

1. 开篇

MySQL 在查询数据时,对于 InnoDB 存储引擎而言,会先将磁盘上的数据以页为单位,先将数据页加载进内存,然后以缓存页的形式存放在「Buffer Pool」中。Buffer Pool 是 InnoDB 的一块内存缓冲区,在 MySQL 启动时,会按照配置的缓存页的大小,将 Buffer Pool 缓存区初始化为许多个缓存页,默认情况下,缓存页大小为 16KB。

为了方便理解,对于磁盘上的数据所在的页,叫做数据页,当加载进 Buffer Pool 中后,叫做缓存页,这两者是一一对应的

在 MySQL 启动初期,Buffer Pool 中的这些缓存页都是处于空闲状态,也就是还没有被使用,而随着 MySQL 的运行时间越来越长,这些缓存页渐渐地都被使用了,用来存放从磁盘上加载的数据页,导致空闲的缓存页就越来越少。当某一时刻,所有空闲的缓存页都被使用了,那么这个时候,从磁盘加载到内存中的数据页该怎么办呢?

b8adef0f6c853118e296fa90ddbf875c.png

2. LRU算法

既然没有空闲缓存页了,而又想使用缓存页,那么最简单的办法就是淘汰一个缓存页。应该淘汰谁呢?当然是淘汰那个最近一段时间最少被访问过的缓存页了,这种思想就是典型的 LRU 算法了。

LRU 是 Least Recently Used 的简写,这个算法的实现就是淘汰最久未使用的数据,它通过维护一个链表,每当访问了某个数据时,就将这个数据加入到链表的头部,如果数据本身存在于链表中,那么就将数据从链表的中间移动到链表的头部,这样最终下来,在链表尾部的数据一定就是最久未被使用的数据了,因此可以将其淘汰。

将 LRU 算法应用到缓存页的淘汰策略上,那么就是在 InnoDB 存储引擎层内部,维护了一个链表,这些链表中的元素存储的就是指向缓存页的指针。

在 MySQL 启动的时候,这个链表为空。MySQL 启动以后,在进行数据查询时,InnoDB 会先判断要查询的数据所在的数据页,是否存在于 Buffer Pool 的缓存页当中,如果不存在,就从磁盘中读取对应数据页,存放到 Buffer Pool 一个空闲的缓存页当中,然后将这个缓冲页放入到链表的头部;如果要查询的数据已经存在于 Buffer Pool 当中了,那么就将对应的缓存页从链表的中间移动到链表头部。

这样随着 MySQL 的运行,空闲的缓存页越来越少,LRU 链表越来越长,直到某一时刻,剩余的空闲缓存页数为 0,当需要申请一个新的空闲缓存页的时候,就需要淘汰一个缓存页了,此时只需要把链表尾部的那个缓存页刷入到磁盘,然后清空缓存页里面的数据,这样就空余出一个新的缓存页了。

3. LRU 链表带来的问题

咋一看,似乎 LRU 链表完美解决了缓存页淘汰的问题。但理想很丰满,现实却很骨感,如果直接使用这种 LRU 算法的话,在 MySQL 中就会存在很大的问题。

3.1 全表扫描

在 MySQL 中经常会出现全表扫描,一种是开发人员对索引的使用不当导致的,一种是业务如此,无法避免。

当出现全表扫描时,InnoDB 会将该表的数据页全部从磁盘文件加载进缓存页中,这些缓存页会被加入到 LRU 链表中。如果进行全表扫描的对象是一张非常大的表,可能是几十 GB 的数据,而且这张表记录的是类似于账户流水、操作日志等使用不频繁的数据,这个时候如果 LRU 链表已经满了,现在我们就要淘汰一部分缓存页,腾出空间来存放全表扫描出来的数据。这样就会因为全表扫描的数据量大,需要淘汰的缓存页多,导致在淘汰的过程中,极有可能将需要频繁使用到的缓存页给淘汰了,而放进来的新数据却是使用频率很低的数据,甚至是这一次使用之后,后面几乎再也不用,如操作日志等。

最终导致的现象就是,当我们在对这些使用不频繁的大表进行全表扫描之后,在一段时间内,Buffer Pool 缓存的命中率明显下降,SQL 的性能也明显下降,因为常用的缓存页被淘汰了,再进行查询时,需要从重新磁盘读取,发生磁盘 IO,性能下降。所以,如果 MySQL 只是简单的使用 LRU 算法,那么碰到全表扫描时,就会存在性能下降的问题,甚至在高并发场景下,成为性能瓶颈。

3.2 预读

预读是 InnoDB 引擎的一个优化机制,当你从磁盘上读取某个数据页,InnoDB 可能会将与这个数据页相邻的其他数据页也读取到 Buffer Pool 中。

InnoDB 为什么要这样做呢?因为从磁盘读取数据时发生的磁盘 IO 是随机 IO,性能差。当你读取某一个数据页时,InnoDB 会猜测你可能也需要下一个数据页的数据,如果一次能连着读取多个数据页,那么对其他的数据页而言,这是顺序读取(节省了寻道时间、磁头旋转时间),相对较快,那这样就能提升一点性能。

在两种情况下会触发预读机制:

  1. 顺序的访问了磁盘上一个区的多个数据页,当这个数量超过一个阈值时,InnoDB 就会认为你对下一个区的数据也感兴趣,因此触发预读机制,将下一个区的数据页也全都加载进 Buffer Pool。这个阈值由参数 「innodb_read_ahead_threshold」 控制,默认为 56。 可以通过如下命令查看:

show variables like 'innodb_read_ahead_threshold';

  1. 如果 Buffer Pool 中已经缓存了同一个区数据页的个数超过 13 时,InnoDB 就会将这个区的其他数据页也读取到 Buffer Pool 中。这个开关由参数 「innodb_random_read_ahead」 控制,默认是关闭的。

show variables like 'innodb_random_read_ahead';

如果 MySQL 仅是简单的使用 LRU 算法,那么预读机制和全表扫描带来的问题类似,预读机制会将其他的数据页也加载进内存,当 LRU 链表满时,可能将我们频繁访问的缓存页给淘汰,从而导致性能下降。

4. 冷热分离

实际上,MySQL 确实没有直接使用 LRU 算法,而是在 LRU 算法上进行了优化。

从上面的全表扫描和预读机制的问题分析中,我们可以看到,根本原因就是从磁盘上新读取到的数据页,在加载进 Buffer Pool 时,可能将我们频繁访问的数据给淘汰,也就是出现了冷热数据的现象。因此,MySQL 的优化思路就是:对数据进行冷热分离,将 LRU 链表分成两部分,一部分用来存放冷数据,也就是刚从磁盘读进来的数据,另一部分用来存放热点数据,也就是经常被访问到数据。

其中,存放冷数据的区域占这个 LRU 链表的多少呢?这由参数 「innodb_old_blocks_pct」 控制,默认是 37%(约八分之三)。冷热分离的 LRU 链表示意图如下(图片来自于MySQL官方文档)。

show variables like 'innodb_old_blocks_pct';

cba65cfd484f53f7914f3026833e08e2.png

优化过后的 LRU 链表,又是如何进行数据页的存放的呢?

「当从磁盘读取数据页后,会先将数据页存放到 LRU 链表冷数据区的头部,如果这些缓存页在 1 秒之后被访问,那么就将缓存页移动到热数据区的头部;如果是 1 秒之内被访问,则不会移动,缓存页仍然处于冷数据区中。1 秒这个数值,是由参数 innodb_old_blocks_time 控制。」**

show variables like 'innodb_old_blocks_time';

当遇到全表扫描或者预读时,如果没有空闲缓存页来存放它们,那么将会淘汰一个数据页,而此时淘汰地是冷数据区尾部的数据页。冷数据区的数据就是不经常访问的,因此这解决了误将热点数据淘汰的问题。如果在 1 秒后,因全表扫描和预读机制额外加载进来的缓存页,仍然没有人访问,那么它们会一直待在冷数据区,当再需要淘汰数据时,首先淘汰地就是这一部分数据。

至此,基于冷热分离优化后的 LRU 链表,完美解决了直接使用 LRU 链表带来的问题。

5. LRU 链表的极致优化

实际上,MySQL 在冷热分离的基础上还做了一层优化。

当一个缓存页处于热数据区域的时候,我们去访问这个缓存页,这个时候我们真的有必要把它移动到热点数据区域的头部吗?

从代码的角度来看,将链表中的数据移动到头部,实际上就是修改元素的指针指向,这个操作是非常快的。但是为了安全起见,在修改链表的时候,我们需要对链表加上锁,否则容易出现并发问题。

当并发量大的时候,因为要加锁,会存在锁竞争,每次移动显然效率就会下降。因此 MySQL 针对这一点又做了优化,如果一个缓存页处于热数据区域,且在热数据区域的前 1/4 区域(注意是热数据区域的 1/4,不是整个链表的 1/4),那么当访问这个缓存页的时候,就不用把它移动到热数据区域的头部;如果缓存页处于热数据的后 3/4 区域,那么当访问这个缓存页的时候,会把它移动到热数据区域的头部。

6. 生产上的 MySQL 调优

理解了上面的原理,下面则基于这些原理,说一些MySQL可以优化的方案。

MySQL 的数据最终是存储在磁盘上的,每次查询数据时,我们先需要把数据加载进缓存,然后读取,如果每次查询的数据都已经存在于缓存了,那么就不用去磁盘读取,避免了一次磁盘 IO,这是我们最期望的。因此为了尽量在 LRU 链表中缓存更多的缓存页,我们「可以根据服务器的配置,尽量调大 Buffer Pool 的大小」

另外,在进行增删改查的时候,需要涉及到对 Buffer Pool 中 LRU 链表、Free 链表、Flush 链表的修改,为了线程安全,我们需要进行加锁。因此为了提高并发度,MySQL 支持配置多个 Buffer Pool 实例。当有多个Buffer Pool实例时,就能将请求分别分摊到这些Buffer Pool中,减少了锁的竞争。

可以通过如下命令去查看 Buffer Pool 的大小以及 Buffer Pool 实例的个数。

# buffer pool大小
show variables like 'innodb_buffer_pool_size';
# buffer pool实例个数
show variables like 'innodb_buffer_pool_instances';

Free 链表是维护空闲缓存页的列表,Flush 链表是维护脏页的链表。什么是脏页,感兴趣的同学可以先自己查阅相关资料

另外在实际应用中,在没有外部监控工具的情况下,我们该如何知道 MySQL 的一些状态信息呢?如:缓存命中率、缓存页的空闲数、脏页数量、LRU 链表中缓存页个数、冷热数据的比例、磁盘 IO 读取的数据页数量等信息。可以通过如下命令查看:

show engine innodb status;

这个命令的查询结果是一个很长的字符串,可以复制出来,放在文本文件中查看分析,部分信息截图如下:

943ecd9f0f330490a86551f38911c499.png

如果看到 「youngs/s」 这个值较高,说明数据从冷数据区移到热数据的频率较大,因此可以适当调大热数据所占的比例,也就是减小「innodb_old_blocks_pct」参数的值,也可以调大「innodb_old_blocks_time」参数的值

如果看到 「non-youngs/s」 这个值较高,说明数据被加载进缓存当中后,没有被移动到热数据区,这是因为在 1s 内被访问了,这很可能是全表扫描造成的,这个时候就可以去检查一下代码,是不是SQL语句写得不恰当。

7. 总结

总结一下,本文详细说明了普通的 LRU 链表并不适用于 MySQL,全表扫描和预读机制均会导致热点数据被淘汰,从而导致性能下降的问题。MySQL 在 LRU 算法的基础上做了优化,将链表拆分为冷、热两部分,从而解决了冷热数据的问题。最后介绍了几种 MySQL 优化的方法,可以通过调到 Buffer Pool 的大小以及个数来提升性能,也可以结合 MySQL 的运行状态信息来决定是否需要调整 LRU 链表的冷热数据区的比例。

另外,将数据进行冷热分离的这种思路,非常值得借鉴。

最后,实践是检验理论的唯一标准,MySQL 相关的原理明白了,至于生产环境的 MySQL 应该如何优化,还需要结合实际情况以及机器的配置来决定如何配置 MySQL 的参数。

作者:天堂同志
原文链接:https://juejin.im/post/5ec0b4435188256d6d6bc0eb



推荐阅读
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 本文介绍了Paxos的世界中关于复制日志与状态机的概念和重要性。通过存储日志来实现数据的持久化,并通过日志流来记录数据的变化,而不是直接持久化数据本身。这样做的好处是简化了持久化存储的操作,并且方便多机之间的数据同步。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
author-avatar
果丽珍
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有