当前位置: 首页 > 科技观察

一篇不完整的MySQL介绍指南

时间:2023-03-12 02:04:32 科技观察

由于MySQL整个体系过于庞大,文章篇幅有限,无法面面俱到。所以我会尝试用一种更贴近我们日常使用的方式来解释它。小白眼中的MySQL首先MySQL对于我们来说是什么?让我们从最简单的例子来回顾一下。这可能就是一开始大家熟知的MySQL。那么MySQL是如何处理这个查询语句的呢?也就是说,它是如何感知这个字符串是一个查询语句的呢?它如何感知从哪个表中获取数据?它如何感知如何获取数据?到目前为止,我不知道。接下来我们一步步补分析。连接池首先,如果要在MySQL中执行命令,就必须连接到MySQL服务器,就像我们通过“用户名”和“密码”登录网站一样。因此,我们首先要知道的就是连接池。这种池化技术的作用很明显,多路复用连接,避免了频繁销毁和创建线程带来的开销。此外,在这一层,您还可以根据您的账户密码验证用户的合法性和权限。每个连接对应一个线程,“服务器”和“MySQL”是一样的,服务器的一个线程从服务器的连接池中拿一个连接,发起一条查询语句。MySQL服务器的线程从连接池中取出一个线程,继续后面的处理。那么接下来的过程是怎样的呢?当然是分析SQL语句了。分析器很明显,MySQL必须知道这条SQL是不是合法的SQL语句,这条SQL语句要干什么?就像一个哥们疯狂敲你的门,门开了,下一步是什么?肯定问他是谁?为什么?所以,下一步就是解析SQL。解析完成后,我们就知道当前的SQL是否符合语法。它有什么作用?您要查询数据吗?还是更新数据?还是删除数据?很简单,我们用肉眼就可以清楚的看出一条SQL语句是为了什么。但是计算机不是人脑。我们要让计算机理解这条SQL语句,来帮助我们做下面的事情。知道了这个SQL语言的用途之后,我们就可以开始执行操作了吗?它不是优化器。MySQL需要知道这个SQL的目的。在执行之前,它要决定怎么做,怎么做最好。解开。还是刚才一样的例子,隔壁哥们敲你家门说,哥们,我家没水了,想求你给(白)屋(漂)24瓶矿泉水水。先不讨论他为什么要24瓶矿泉水。我们要商量的是怎么把24瓶矿泉水拿给他。因为你才想起来矿泉水是之前被你扔到柜子里的。你要每次拿4瓶跑6次吗?或者找个箱子,装满24瓶,搬出去给他。这几乎就是优化器所做的,优化器分析您的SQL并找到最佳解决方案。再举一个严肃的例子,假设执行SELECTnameFROMstudentwhereid=1语句时,数据库中有1W条数据。这时候有两种选择:找出所有列的name值,然后遍历比较,找到id=1的name值,直接找到id=1的数据,然后取值姓名。如果你用屁股想,你应该选择选项2。在你弄清楚如何去做之后,你需要在行动中实施它。因此,下一步是执行器。ExecutorExecutor会使用底层存储引擎的接口来真正执行SQL语句。这里的例子是查询操作。至此,MySQL的黑匣子已经被我们一步步揭开。但是当我们揭开最后的面纱时,我们发现了一个新的黑匣子。那就是存储引擎。到目前为止,我们只知道它的名字,但我们不知道如何存储数据或查询数据。存储引擎MySQL中有多种类型的存储引擎,适用于不同的场景。你可以理解为存储引擎是进行数据操作的接口,底层实现有很多种。InnoDB,MyISAM,Memory,CSV,Archive,Blackhole,Merge,Federated,Example应用最广,InnoDB应用最广。从MySQL5.5开始,InnoDB一直是MySQL的默认存储引擎。因此,存储引擎其实并不是什么高端的东西。离一些大学用来交作业的图书馆管理系统只有一个亿点的距离。或者,让我再举一个例子。当我们在电脑上向文件写入数据时,此时由于操作系统的优化,数据并没有直接写入磁盘,因为I/O操作是相当昂贵的。数据会先进入OS的缓存,然后被OS刷入磁盘。就整个逻辑结构而言,MySQL类似于用电脑写文件。从上面的例子可以看出,存储引擎可以分为两部分:内存磁盘那么,从宏观上看,MySQL是把数据缓存在内存中,摆弄一下,然后在特定的时间刷新到磁盘,仅此而已事物。接下来,让我们深入探讨存储引擎InnoDB底层原理中非常重要的一部分——内存架构。简单来说,InnoDB的内存由以下两部分组成:BufferPoolLogBuffer从上图可以看出,BufferPool是InnoDB非常重要的一个部分。MySQL之所以这么快,一个重要的原因就是它的数据都存储在内存中,而这块内存就是BufferPool。BufferPool一般来说,宿主机80%的内存都会分配给BufferPool。这很容易理解。内存越大,可以存储的数据就越多。这样可以直接在内存中读取更多的数据,可以大大提高运行效率。那么BufferPool是如何存储数据的呢?如果底层数据存储没有经过特殊设计和优化,那么InnoDB除了取数据时的整个遍历外,没有其他捷径。如果当时MySQL就不会取得今天的地位。Page如果我们可以想象一下InnoDB是如何组织内存中的数据的。试想,图书馆里的书是一本一本地摊在地上方便查找,还是按类别、名称分类,放在相应的书架上,然后编号,方便查找?结论是不言而喻的。BufferPool也采用了同样的数据整合措施。InnoDB把BufferPool一个一个的分成页(Pages),还有一个ChangeBuffer(后面会详细介绍,先知道这里就行)。把数据分页分页能不能提高查询效率?那么这个页面里有什么?从上图可以看出,页面和页面之间其实是有关系的,它们是通过双向链表连接起来的。您可以轻松地从一页跳到下一页。页面中的数据是如何存储的?用户记录当然,跳来跳去说明不了什么,下面我们来揭开Pages的黑匣子。可以看到,主要分为上一页指针、下一页指针、UserRecords等字段,方便理解。关键是我们要了解UserRecords。UserRecords是最终存放一行一行数据**(Rows)的地方,行与行之间形成一个单向链表**。看完这篇单向链表,不知道大家有没有什么疑问。我们知道,在聚簇索引中,Key实际上会按照PrimaryKey的顺序排列。在用户记录中会相同吗?当我们向UserRecords中插入一条新数据时,我们是否也会将现有数据按照PrimaryKey的顺序重新排序?如果我们每次都需要在UserRecords中插入数据,如果数据重新排序,那么MySQL在江湖中的地位将再次失去。虽然图中看起来是按照“主键”的顺序存储的,但实际上是按照数据插入的顺序存储的。先到的数据在前面,后到的数据在后面。只是每个UserRecords的数据指针不是指向物理的下一个,而是指向逻辑的下一个。用图来表示,大致如下:可以理解为数组和链表的区别。看到这里,问题来了,不遍历呢?这不是打脸吗?因为从上图可以看出,如果我要查找当前页面(Pages)中是否存在某个数据,只能从头遍历这个单向链表。就是这样?敢标榜高性能?当然,InnoDB绝对不是这样的。现在有必要从“其他字段”中取出一些字段来说明。Infimum和Supreme分别代表当前页(Pages)中最大和最小的记录。可以看出,有了Infimum和Supremum,我们不需要遍历UserRecords就可以知道我们要找的数据是否在当前页面,大大提高了效率。但实际上,还是有问题的。比如我要查询的数据不在当前页面也可以。如果是怎么办?还是逃不过O(N)的链表遍历?这是一个临时解决方案吗?这时候,我们就有必要从“其他领域”中提取一个概念。顾名思义,PageDirectory就是一个“目录”,如下图所示。可以看出PageDirectory中每隔一条记录就会有一条记录,这条记录就是指向UserRecords中记录的指针。不知道这样的设计有没有让你想起SkipList。那么PageDirectory中的目录是做什么用的呢?有了PageDirectory,就可以像跳转表一样查询一个页面中的数据。在PageDirectory中找到对应的“location”后,根据指针跳转到UserRecords上对应的单链表进行查询。这样可以避免遍历所有数据。上面说的“仓位”其实有个专业术语叫“槽位”。每个槽的数据是一个指向用户记录中的记录的指针。当我们添加每一条数据时,PageDirectory中的slots都会被同步维护。InnoDB规定每6条记录创建一个slot。了解了这些,你就知道如何在MySQL中高效查询数据了。如果你想知道“其他字段”是什么以及“页面”的完整外观,你可以看我之前写的那篇文章。MySQL页面完全攻略-浅入深出原理,不再赘述。索引一旦您了解了页面,就很清楚索引是什么了。InnoDB底层存储使用的数据结构是B+树,B树的变种。MySQL中有两种索引,聚簇索引和非聚簇索引,听起来很宏大。其实,了解了“页面”的底层原理之后,区分它们就变得很简单了。在聚簇索引的叶子节点上,存储着“页”。在**非聚集索引(二级索引)**的叶子节点上,存储着“主键ID”。很多时候,我们需要通过非聚集索引获取主键,然后通过这个主键获取“聚集索引”中的完整数据。这个过程还有一个有趣的名字叫做“返回表”。至于底层数据结构为什么使用B+树和B树,大概是因为以下三点:B+树可以减少I/O次数,查询效率更稳定,可以更好的支持范围查询.详细原因可以参考之前写的浅为什么下一步要看怎么更新数据?因为上面说的“分页”原理主要是建立在“查询”的前提下的,看完之后,你应该清楚上去的查询过程了。让我们看看更新时会发生什么。首先,如果我们插入一条id=100的数据,然后更新它,那么这条数据肯定在BufferPool中。这句话好像是废话(我在数据库里写过,所以肯定存在)。让我换一种说法。更新时,id=100的数据可能不在BufferPool中。为什么之前写入了BufferPool,之后却没有更新BufferPool?答案是内存是有限的,我们不可能无限往BufferPool中插入数据。熟悉Redis的都知道,Redis在运行时会有一个“过期策略”,有以下三种:定时过期、惰性过期、定时过期和BufferPool也是基于内存的,同样需要一个“过期”策略”来清理一些不经常使用的。访问数据以便为新数据和热数据腾出空间。当然,这里的清理不是指删除,而是当它们刷入磁盘更新数据时,如果对应的数据不存在,就会将数据所在的页加载到BufferPool中。请注意,这不仅仅是加载id=100的行,而是加载它所在的整个“页面”数据。加载到BufferPool中后,更新BufferPool中的数据。当然对于我们开发者来说,这种情况是针对聚簇索引而言的。“非聚集索引”还有另一种情况。更改缓冲区非常简单。当我们更新一些字段的时候,假设这些字段是组成非聚集索引的字段,就会涉及到非聚集索引的更新,可惜非聚集索引所在的页面不在Buffer中水池。根据前面的说法,需要将相应的页面(Pages)加载到BufferPool中。但是这里有一个大问题。将来可能根本不会使用此二级索引。**这样一来,刚才昂贵的I/O操作就白白浪费了。积少成多,如果每次涉及到更新二级索引,发现BufferPool中不存在,就进行I/O操作,也是一个相当大的开销。因此,InnoDB设计了??ChangeBuffer。ChangeBuffer专门用来存储“非聚集索引”所在页不在BufferPool中时的变化。也就是说,当相应的非聚集索引被修改且相应的页面(Pages)不在BufferPool中时,这些变化会暂时存储在ChangeBuffer中,直到相应的页面被其他请求加载到BufferPool中.ChangeBuffer中暂存的数据会与BufferPool中的数据合并。当然,ChangeBuffer的设计也不是没有缺点。当ChangeBuffer中的数据较多时,可能需要几个小时的时间才能将它们全部合并到BufferPool中,而在合并过程中,磁盘I/O操作会比较频繁,导致部分CPU的占用资源。它对MySQL的整体性能有影响。那么你可能会问,是不是只有缓存的页面加载到BufferPool中才会触发merge操作呢?那么如果还没有加载,ChangeBuffer会被爆掉吗?显然,InnoDB在设计的时候就考虑到了这一点。除了相应的页面加载、提交事务、服务宕机、服务重启都会触发合并。AdaptiveHash自适应哈希索引(AdaptiveHashIndex)是一个与BufferPool配合使用的函数。自适应哈希索引使MySQL的性能更接近于内存服务器。如果要启用自适应哈希索引,可以通过更改配置innodb_adaptive_hash_index来开启它。如果不想开启,也可以在启动时通过命令行参数--skip-innodb-adaptive-hash-index关闭。自适应哈希索引是根据索引键的前缀构建的。InnoDB有自己的索引监控机制。当检测到为当前索引页建立哈希索引可以提高效率时,就会创建相应的哈希索引。如果一个表的数据量很小,它的所有数据都在缓冲池中,那么自适应哈希索引就变成了我们熟悉的指针的作用。当然,自适应哈希索引的创建和维护会带来一些开销,但是和它带来的性能提升相比,这个开销可以直接忽略。但是,是否启用自适应哈希索引取决于具体的业务情况。例如,当我们的业务具有大量并发连接查询的特点时,访问自适应哈希索引就会造成竞争。而如果业务中还使用了LIKE或%等通配符,哈希索引根本用不上,那么此时自适应哈希索引就成了系统的负担。因此,为了尽可能减少并发带来的竞争,InnoDB对自适应哈希索引进行了分区,每个索引绑定到特定的分区,每个分区由单独的锁保护。其实通俗的理解就是降低锁的粒度。我们可以通过配置innodb_adaptive_hash_index_parts来改变分区的个数,其可配置范围是[8,512]。Expirationpolicy上文提到,BufferPool也有自己的过期策略,定期将不需要的数据刷回磁盘,为后续请求腾出空间。那么,InnoDB是如何知道哪些数据是不需要的呢?答案是LRU算法。LRU是**(L**eastRecentlyUsed)的缩写,表示最近最少使用。Redis的内存淘汰策略中也用到了LRU。但是,InnoDB使用的LRU算法与传统的LRU算法并不相同。InnoDB使用LRU的改进版本。那为什么要改进呢?这需要了解MySQL中原生LRU有什么问题。在实际业务场景中,很有可能会出现全表扫描。如果数据量很大,很可能会将之前缓存在BufferPool中的热数据全部换出。这样,当再次访问热点数据时,需要进行I/O操作,这将导致这段时间MySQL性能出现断崖式下降。而这种情况有一个专门的名词,叫做——缓冲池污染。这就是InnoDB优化LRU算法的原因。优化后的链表分为NewSublist和OldSublist两部分,分别占据BufferPool的3/4和1/4。链表的前3/4,即NewSublist存放访问频率较高的页面。而最后的1/4,也就是OldSublist,是比较少被反问的页面。当后续BufferPool空间不足或新增页面时,OldSublist中的数据将被移除。了解了链表的整体结构和组成后,我们从新页面加入链表开始,走一遍整个流程。首先,一个新的页面放入BufferPool后,会被插入到链表中NewSublist和OldSublist的交点处,称为MidPoint。链表中存储的数据来源有两种,即:MySQL预读线程用户操作的预加载数据,如Query查询默认情况下,受用户操作影响进入BufferPool的数据会立即stored转到链表的前端,也就是NewSublist的Head部分。但是,如果数据在MySQL启动时预加载,就会被放入MidPoint。如果这部分数据被用户再次访问,则将其放在链表的最前面。这样,虽然页面数据在链表中,但由于没有被访问过,所以会被移动到OldSublist的最后1/4,直到被清理干净。LogBufferLogBuffer用于存放即将被flush到磁盘文件中的日志,比如RedoLog,这块区域也是InnoDB内存的重要组成部分。LogBuffer的默认值为16M。如果需要调整,可以通过配置参数innodb_log_buffer_size来调整。LogBuffer越大,可以存放的RedoLog越多,所以我们在提交事务前不需要将RedoLogs刷到磁盘,直接丢到LogBuffer中即可。因此,更大的LogBuffer可以更好地支持更大的事务;同样,如果一个事务会更新、插入或删除大量的行,那么适当增加??LogBuffer的大小也可以有效减少一些DiskI/O操作。至于将LogBuffer中的数据刷新到磁盘的频率,可以通过参数innodb_flush_log_at_trx_commit来确定。