前段时间,我在 B 站上看到一个技术视频,题目叫做《机票报价高并发场景下的一些解决方案》。
up 主是 Qunar技术大本营,也就是我们耳熟能详的“去哪儿”。
视频链接在这里:
当时其实我是被他的这个图片给吸引到了(里面的 12 qps 应该是 12k qps):
他介绍了两个核心系统在经过一个“数据压缩”的操作之后,分别节约了 204C 和 2160C 的服务器资源。
共计就是 2364C 的服务器资源。
如果按照一般标配的 4C8G 服务器,好家伙,这就是节约了 591 台机器啊,你想想一年就节约了多大一笔开销。
视频中介绍了几种数据压缩的方案,其中方案之一就是用了高性能集合:
因为他们的系统设计中大量用到“本地缓存”,而本地缓存大多就是使用 HashMap 来帮忙。
所以他们把 HashMap 换成了性能更好的 IntObjectHashMap,这个类出自 Netty。
为什么换了一个类之后,就节约了这么多的资源呢?
换言之,IntObjectHashMap 性能更好的原因是什么呢?
我也不知道,所以我去研究了一下。
拉源码
研究的第一步肯定是要找到对应的源码。
你可以去找个 Netty 依赖,然后找到里面的 IntObjectHashMap。
我这边本地刚好有我之前拉下来的 Netty 源码,只需要同步一下最新的代码就行了。
但是我在 4.1 分支里面找这个类的时候并没有找到,只看到了一个相关的 Benchmark 类:
点进去一看,确实没有 IntObjectHashMap 这个类:
很纳闷啊,我反正也没搞懂为啥,但是我直接就是一个不纠结了,反正我现在只是想找到一个 IntObjectHashMap 类而已。
4.1 分支如果没有的话,那么就 4.0 上看看呗:
于是我切到了 4.0 分支里面去找了一下,很顺利就找到了对应的类和测试类:
能看到测试类,其实也是我喜欢把项目源码拉下来的原因。如果你是通过引入 Netty 的 Maven 依赖的方式找到对应类的,就看不到测试类了。
有时候配合着测试类看源码,事半功倍,一个看源码的小技巧,送给你。
而我要拉源码的最重要的一个目的其实是这个:
可以看到这个类的提交记录,观察到这个类的演变过程,这个是很重要的。
因为一次提交绝大部分情况下对应着一次 bug 修改或者性能优化,都是我们应该关注的地方。
比如,我们可以看到这个小哥针对 hashIndex 方法提交了三次:
在正式研究 IntObjectHashMap 源码之前,我们先看看只关注 hashIndex 这个局部的方法。
首先,这个地方现在的代码是这样的:
我知道这个方法是获取 int 类型的 key 在 keys 这个数组中的下标,支持 key 是负数的情况。
那么为啥这一行代码就提交了三次呢?
我们先看第一次提交:
非常清晰,左边是最原始的代码,如果 key 是负数的话,那么返回的 index 就是负数,很明显不符合逻辑。
所以有人提交了右边的代码,在算出 hash 值为负数的时候,加上数组的长度,最终得到一个正数。
很快,提交代码的哥们,发现了一个更好的写法,进行了一次优化提交:
拿掉了小于零的判断。不管 key%length 算出的值是正还是负,都将结果加上一个数组的长度后再次对数组的长度进行 % 运行。
这样保证算出来的 index 一定是一个正数。
第三次提交的代码就很好理解了,代入变量:
所以,最终的代码就是这样的:
return (key % keys.length + keys.length) % keys.length;
这样的写法,不比判断小于零优雅的多且性能也好一点吗?而且这也是一个常规的优化方案。
如果你看不到代码提交记录,你就看不到这个方法的演变过程。我想表达的是:在代码提交记录中能挖掘到非常多比源码更有价值的信息。
又是一个小技巧,送给你。
IntObjectHashMap
接下来我们一起探索一下 IntObjectHashMap 的奥秘。
关于这个 Map,其实有两个相关的类:
其中 IntObjectMap 是个接口。
它们不依赖除了 JDK 之外的任何东西,所以你搞懂原理之后,如果发现自己的业务场景下有合适的场景,完全可以把这两个类粘贴到自己的项目中去,一行代码都不用改,拿来就用。
在研究了官方的测试用例和代码提交记录之后,我选择先把这两个类粘出来,自己写个代码调试一下,这样的好处