07. Java 索引

1. 前言

对于常见的应用系统,读的流量远远高于写的流量,比如电商网站,商家在数据库中写入商品的价格和库存之后,访问页面的顾客会产生大部分的读流量。所以常见的现象是当应用系统的流量逐渐增加时,写操作不会成为数据库的性能瓶颈,但是复杂查询语句消耗的查询时间会越来越长,读操作更容易触碰数据库的查询性能瓶颈。MySQL 自身为了优化查询效率,更快的查询目标集合,定义了索引,也就是常用的 "键"(Key),MySQL 中的索引是单独存储在磁盘上的数据结构,使用索引可以快速查询满足特定条件的记录。

2. 谈一谈 InnoDB 存储引擎的索引数据结构

2.1 不同引擎的数据结构

面试官提问: MySQL 中 InnoDB 存储引擎底层的数据结构是什么?

题目解析:

以 MySQL 5.7 为例,首先查询官方文档,可以发现存储引擎和索引数据结构的对应关系,例如 InnoDB 对应 BTREE 索引,MEMORY 存储引擎对应哈希索引和 BTREE 索引,注意这里的 BTREE 实际指代的是 B+ 树,我们重点关注树的数据结构。

2.2 B 树和 B + 树

如果能正确回答 InnoDB 索引的底层数据结构是 B+ 树,面试官接下来可能会先考察候选人对数据结构本身的理解程度。

面试官:学过数据结构课程的同学,应该都听过 B 树和 B+ 树吧,这两种树有什么区别呢?

题目解析:我们尽量需要通过在白纸上画出 B 树和 B+ 树,画图的同时给面试官解释两种树的区别,需要从数据结构、优缺点方面分析(一般来说不需要深入到节点的插入和删除流程,因为比较复杂)
首先我们画出一个简化后的 B 树,如下图:

B 树,图中绿色节点表示具体数据,蓝色节点表示指针,黄色表示键值,整个节点指代一个磁盘块

参考上图,我们定义一个 m 阶的 B 树的数据结构:

① 根结点至少有两个子节点;
② 除了根节点外,每个子节点都包含 n-1 个元素(数据)和 n 个子节点指针,其中 m/2 <= n <= m;
③ 所有的叶子结点都位于同一层;
④ 有序性:每个节点中的元素从小到大排列,节点当中 k-1 个元素正好是 k 个孩子包含的元素的值域分划。

画出 B 树只是为了衬托 B+ 树,B 树不会是面试的重点,接下来我们在白纸上画出一个典型的 B+ 树结构:

B + 树,图中绿色节点表示具体数据,蓝色节点表示指针,黄色表示键值,整个节点指代一个磁盘块

对于一个 m 阶的 B+ 树,基本定义同 B 树相同:
① 除了根之外的每个节点都包含最少 m/2 个元素最多 m-1 个元素,对于任意的结点有最多 m 个子指针;
② 所有的叶子节点都在同一层;

除此之外,B+ 树相对于 B 树,需要特别区分的不同点有:
① 数据存储方式不同:B+ 树中间节点并不存储真正的数据,而是保存其叶子节点中最小值作为索引。例如上图中磁盘块 2 和磁盘块 3 中并没有黄色的 data 节点;
② 数据查找方式不同:每个叶子节点存在一个 next 指针,指向下一个叶子节点,形成了有序双向链表,从图中能明显看出来。所以 B 树只能由根节点往下二分查找,B+ 树除了这种查找方式,还支持在叶子节点中直接顺序遍历查找。

2.3 为什么使用 B + 树

在给面试官阐述清楚了 B 树和 B+ 树数据结构的区别之后,接下来面试官大概率会追根溯源,引出更深一步的问题:

** 面试官:** 你能说说为什么 MySQL 要选择 B+ 树作为索引的数据结构实现吗?

题目解析:

结合我们画出的数据结构图示,这个问题翻译过来其实是相对于 B 树,为什么要选择 B+ 树?
首先要分析数据库的读写瓶颈受限原因:计算机的存储是分层次的,CPU 里的 Cache 访问速度最快,速度更慢的是内存(容量小,断电情况下数据会丢失),读写最慢的是硬盘(容量大,数据可长期存储)。MySQL 的数据是持久化存储在硬盘中的,硬盘访问慢是因为:CPU 访问硬盘时会经过三个耗时步骤:

(1)寻道耗时:磁臂移动到磁道;
(2)旋转耗时:例如一个 7200 转的磁盘,表示每分钟能转 7200 次;
(3)数据传输耗时:从磁盘读出数据或者写入数据的过程。前两者都依赖于磁盘的机械运动,耗时非常久。所以磁盘 I/O 是一种非常昂贵的操作,数据库的读写操作需要经过尽可能少的磁盘 I/O。

我们在这个硬件基础上,我们依据 B 树的图示,模拟查询关键词 6 的一次查询过程:

(1)磁盘第一次 I/O 操作:找到磁盘块 1,读入内存;
(2)磁盘第二次 I/O 操作:比较关键词 6 在区间(0,7)之间,找到磁盘块 1 的左指针,根据指针找到磁盘块 2,读入内存;
(3) 磁盘第三次 I/O 操作:比较关键词 6 在区间(5,7)之间,找到磁盘块 2 的最右侧指针,根据指针找到磁盘块 6,读入内存;
(4) 寻找关键词:在磁盘块 6 中找到关键词 6,以及对应 data 数据。

可以发现,树的深度越深,查找需要的磁盘 I/O 次数就越多。现在我们再来分析 B + 树的优势:

(1) B + 树的磁盘 I/O 次数相对更少:利用 B 树 / B+ 树的有序性,从根节点每往子节点每查找一次,都要经过一次磁盘 I/O。相对 B 树,B+ 树的内部节点只包含下级指针,并不存放数据信息,所以对于同样大小的磁盘块,能够存储的记录个数更多(树的阶 m 更大),B+ 树的深度更低,所以磁盘 I/O 次数更少。
(2) B+ 树更适合范围查找:在进行范围查询时,B 树查询只能通过从根节点开始的递归查询,因为相邻节点在硬盘中不一定连续,缓存命中率差,而 B+ 树因为叶子节点形成有序链表,可以直接进行线性遍历。

综上,回答本题的核心点要抓住:
(1)明确磁盘 I/O 是数据库读写的硬件瓶颈。
(2)能够结合两种树不同的数据结构,分析得到 B+ 树的优势。

3. 小结

本章节介绍了 MySQL 中 InnoDB 存储引擎的底层 B+ 树数据结构,候选人需要区分 B 树和 B+ 树,以及从查询效率和硬件成本角度分析为什么在 MySQL 中会优选使用 B+ 树作为支撑。

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1. 使用Java操作ES创建user_new索引,查看索引和删除索引的步骤如下: 创建user_new索引: ```java RestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http"))); CreateIndexRequest createRequest = new CreateIndexRequest("user_new"); CreateIndexResponse createResponse = client.indices().create(createRequest, RequestOptions.DEFAULT); boolean acknowledged = createResponse.isAcknowledged(); if (acknowledged) { System.out.println("user_new索引创建成功"); } else { System.out.println("user_new索引创建失败"); } client.close(); ``` 查看索引: ```java RestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http"))); GetIndexRequest getRequest = new GetIndexRequest("user_new"); boolean exists = client.indices().exists(getRequest, RequestOptions.DEFAULT); if (exists) { System.out.println("user_new索引存在"); } else { System.out.println("user_new索引不存在"); } client.close(); ``` 删除索引: ```java RestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http"))); DeleteIndexRequest deleteRequest = new DeleteIndexRequest("user_new"); AcknowledgedResponse deleteResponse = client.indices().delete(deleteRequest, RequestOptions.DEFAULT); boolean acknowledged = deleteResponse.isAcknowledged(); if (acknowledged) { System.out.println("user_new索引删除成功"); } else { System.out.println("user_new索引删除失败"); } client.close(); ``` 2. 使用Java操作ES插入数据的步骤如下: 插入数据: ```java RestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http"))); IndexRequest request = new IndexRequest("user_new"); request.id("1"); String jsonString = "{\"name\":\"John Doe\",\"age\":25}"; request.source(jsonString, XContentType.JSON); IndexResponse response = client.index(request, RequestOptions.DEFAULT); System.out.println("插入数据成功,索引名称:" + response.getIndex() + ",id:" + response.getId()); client.close(); ``` 以上代码是向user_new索引插入一个id为1的文档,文档内容为{"name":"John Doe","age":25}。使用Java的RestHighLevelClient发送IndexRequest请求,并将待插入的文档以JSON格式传入请求的source,然后通过client.index方法执行插入操作。接收到的IndexResponse对象可以获取插入的文档的索引名称和id。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红薯不甜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值