换个数据结构,一不小心节约了 591 台机器! (上)

简介: 换个数据结构,一不小心节约了 591 台机器! (上)

你好呀,我是歪歪。

前段时间,我在 B 站上看到一个技术视频,题目叫做《机票报价高并发场景下的一些解决方案》。

up 主是 Qunar技术大本营,也就是我们耳熟能详的“去哪儿”。

视频链接在这里:

https://www.bilibili.com/video/BV1eX4y1F7zJ?p=2

当时其实我是被他的这个图片给吸引到了(里面的 12 qps 应该是 12k qps):

image.png

他介绍了两个核心系统在经过一个“数据压缩”的操作之后,分别节约了 204C 和 2160C 的服务器资源。

共计就是 2364C 的服务器资源。

如果按照一般标配的 4C8G 服务器,好家伙,这就是节约了 591 台机器啊,你想想一年就节约了多大一笔开销。

image.png

image.png

因为他们的系统设计中大量用到“本地缓存”,而本地缓存大多就是使用 HashMap 来帮忙。

所以他们把 HashMap 换成了性能更好的 IntObjectHashMap,这个类出自 Netty。

为什么换了一个类之后,就节约了这么多的资源呢?

换言之,IntObjectHashMap 性能更好的原因是什么呢?

我也不知道,所以我去研究了一下。

image.png


拉源码


研究的第一步肯定是要找到对应的源码。

你可以去找个 Netty 依赖,然后找到里面的 IntObjectHashMap。

我这边本地刚好有我之前拉下来的 Netty 源码,只需要同步一下最新的代码就行了。

但是我在 4.1 分支里面找这个类的时候并没有找到,只看到了一个相关的 Benchmark 类:

image.png

image.png

image.png

image.png

image.png

image.png

所以,最终的代码就是这样的:

return (key % keys.length + keys.length) % keys.length;

这样的写法,不比判断小于零优雅的多且性能也好一点吗?而且这也是一个常规的优化方案。

如果你看不到代码提交记录,你就看不到这个方法的演变过程。我想表达的是:在代码提交记录中能挖掘到非常多比源码更有价值的信息。

又是一个小技巧,送给你。


image.png



IntObjectHashMap


接下来我们一起探索一下 IntObjectHashMap 的奥秘。

关于这个 Map,其实有两个相关的类:

image.png

其中 IntObjectMap 是个接口。

它们不依赖除了 JDK 之外的任何东西,所以你搞懂原理之后,如果发现自己的业务场景下有合适的场景,完全可以把这两个类粘贴到自己的项目中去,一行代码都不用改,拿来就用。

在研究了官方的测试用例和代码提交记录之后,我选择先把这两个类粘出来,自己写个代码调试一下,这样的好处就是可以随时修改其中的源码,以便我们进行研究。

在安排 IntObjectHashMap 源码之前,我们先关注一下它 javadoc 里面的这几句话:

image.png

第一句话就非常的关键,这里解释了 IntObjectHashMap 针对 key 冲突时的解决方案:

它对于 key 使用的是 open addressing 策略,也就是开放寻址策略。

为什么使用开放寻址呢,而不是采用和 HashMap 一样挂个链表呢?

这里也回答了这个问题:To minimize the memory footprint,也就是为了最小化内存占用。

怎么就减少了内存的占用呢?

这个问题下面看源码的时候会说,但是这里提一句:你就想想如果用链表,是不是至少得有一个 next 指针,维护这个东西是不是又得占用空间?

不多说了,说回开放寻址。

开放寻址是一种策略,该策略也分为很多种实现方案,比如:

  • 线性探测方法(Linear Probing)
  • 二次探测(Quadratic probing)
  • 双重散列(Double hashing)

从上面划线部分的最后一句话就可以知道,IntObjectHashMap 使用的就是 linear probing,即线性探测。

现在我们基本了解到 IntObjectHashMap 这个 map 针对 hash 冲突时使用的解决方案了。

接下来,我们搞个测试用例实操一把。代码很简单,就一个初始化,一个 put 方法:

image.png

就这么几行代码,一眼望去和 HashMap 好像没啥区别。但是仔细一想,还是发现了一点端倪。

如果我们用 HashMap 的话,初始化应该是这样的:

HashMap<Integer,Object> hashMap = new HashMap<>(10);

你再看看 IntObjectHashMap 这个类定义是怎么样的?

只有一个 Object:

image.png

这个 Object 代表的是 map 里面装的 value。

那么 key 是什么,去哪儿了呢?是不是第一个疑问就产生了呢?

查看 put 方法之后,我发现 key 竟然就是 int 类型的值:


image.png

也就是这个类已经限制住了 key 就是 int 类型的值,所以不能在初始化的时候指定 key 的泛型了。

这个类从命名上也已经明确说明这一点了:我是 IntObjectHashMap,key 是 int,value 是 Object 的 HashMap。

那么我为什么用了个“竟然”呢?

因为你看看 HashMap 的 key 是个啥玩意:

网络异常,图片无法展示
|

image.png

首先进来就是两个 if 判断,对参数合法性进行了校验。

接着看标号为 ① 的地方,从方法名看是要做容量调整:

image.png

至于容量为什么不能是偶数,从注释上给了一个解释:

Even capacities can break probing.

意思是容量为偶数的时候会破坏 probing,即我们前面提到的线性探测。

额...

我并没有考虑明白为什么偶数的容量会破坏线性探测,但是这不重要,先存疑,接着往下梳理主要流程。

从标号为 ② 的地方可以看出这是在做数据初始化的操作。前面我们得到了 capacity 为 9,这里就是初始两个数组,分别是 key[] 和 values[],且这两个数组的容量是一样的,都是 9:

image.png

image.png


目录
相关文章
|
算法
换个数据结构,一不小心节约了 591 台机器! (下)
换个数据结构,一不小心节约了 591 台机器! (下)
85 0
换个数据结构,一不小心节约了 591 台机器! (下)
|
Java
换个数据结构,一不小心节约了 591 台机器! (中)
换个数据结构,一不小心节约了 591 台机器! (中)
103 0
换个数据结构,一不小心节约了 591 台机器! (中)
|
18天前
|
消息中间件 存储 搜索推荐
深入理解栈和队列(二):队列
深入理解栈和队列(二):队列
33 0
|
1月前
【栈】数据结构栈的实现
【栈】数据结构栈的实现
|
1月前
|
存储
数据结构--栈和队列
数据结构--栈和队列
|
1月前
|
存储 算法 数据处理
数据结构从入门到精通——栈
栈,作为一种后进先出(LIFO)的数据结构,在计算机科学中扮演着重要的角色。它的特性使得它在处理函数调用、括号匹配、表达式求值等问题时具有得天独厚的优势。然而,如果我们跳出传统思维的束缚,会发现栈的用途远不止于此。
58 0
|
1月前
|
C语言
数据结构之栈详解(C语言手撕)
数据结构之栈详解(C语言手撕)
37 1
|
1月前
|
存储 算法
数据结构— —栈的基本操作(顺序栈和链栈)
数据结构— —栈的基本操作(顺序栈和链栈)
58 0
|
1天前
|
C语言
数据结构中顺序栈的进栈和出栈用C语言表示
数据结构中顺序栈的进栈和出栈用C语言表示
10 1