Hazelcast源码剖析之Eviction

最后更新于:2022-04-01 20:33:29

### 1 奇怪的现象 在使用Hazelcast的Eviction时,发现观察到的现象与想象的不同。按照官方文档介绍,Eviction有这样几个配置选项: ~~~    ...    0    0    LRU    5000    25    ... ~~~ 看后三项,按照参数的描述应为:当每个结点的entry数达到5000时,使用LRU策略,剔除25%的entry,即1250个。可是观察到的现象一般是entry数达到4200左右就止步不前,继续大量高并发的insert测试,既不会增长也不会减少。这是怎么回事?而且相比Redis,Hazelcast在达到eviction临界条件后继续并发插入和读写时,性能表现依旧良好,就像没有发生eviction一样。是使用了多线程还是什么神奇的算法?源码之前,了无秘密,还是从代码中寻找答案吧。 ### 2 代码剖析 我们客户端使用的,也是最顶层的API,就是IMap了,这里以比put()更为高效的set()作为分析的起点。后面其实能够看到,因为使用了命令模式(command),两者都是继承一个父类,evict都是在一个地方触发的。 ### 2.1 MapProxyImpl包装器 IMap的直接实现类是MapProxyImpl,但它只是个Wrapper,负责转换key和value。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13ed4dc3.jpg) ### 2.2 MapProxySupport封装命令对象 真正的实现都是在它继承的MapProxySupport类中,例如set()调用的setInternal()就能在Support类中找到。各种internal方法将操作包装成Operation类,这方便了远程调用的实现,例如set()要执行的结点不在本地。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13eea968.jpg) ### 2.3 SetOperation命令模式 invokeOperation()中会确定操作应该在哪个分区执行,这个分区位于哪个结点上。这里Hazelcast维护了一个线程池,每个分区都有对应的线程去执行本分区的操作。因为过程比较复杂,所以这里直接略过,继续关注我们重点想知道的eviction过程的实现。那么直接看一下SetOperation中的逻辑。SetOperation很简单,直接调用RecordStore保存键值对,但afterRun()中有一些隐含的后处理。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13f0b9a5.jpg) ### 2.4 BasePutOperation触发eviction 果然,在afterRun()中除了广播事件、使Near缓存失效外,还有触发eviction过程。Evict()调用的就是RecordStore的evictEntries()方法。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13f1e69e.jpg) ### 2.5 AbstractEvictableRecordStore控制eviction 真正的evict控制逻辑就在这里。首先,shouldEvict()会判断是否满足了我们之前配置的eviction的触发条件,如PER_NODE=5000。如果满足则调用removeEvictableRecords()开始剔除数据。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13f3420a.jpg) ### 2.6 EvictionOperator事有蹊跷 最终removeEvictableRecords调用的是EvictionOperator,具体的实现都在这里。但仔细看这段代码却看不出有什么高明之处,只是简单地迭代RecordStore的记录,将满足条件的entry剔除掉。既没用多线程,也没什么特殊的算法,这到底是怎么回事? ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13f49286.jpg) ### 2.7 答案揭晓 谜底其实就在Operation类对应的RecordStore初始化上。我们知道,默认情况下,Hazelcast将map分为271个partition。其实RecordStore也是按这些partition划分的,而不是使用一个大的RecordStore。所以从BasePutOperation的evict()到后续处理的都只是当前key对应分区的RecordStore。也就是说:当key要被处理时,Eviction发生在对应的partition里,而不会evict所有数据的25%(Redis就是处理database中的所有数据,所以延迟会有所增加)。所以,当我们继续压力测试时,不断有key继续插入,这些分区就会不断发生eviction,导致整体的内存使用会保持不变。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13f6589e.jpg) ### 3 类图全貌 梳理了上面的执行流程后,我们最后整理一下这些类之间的关系。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-31_57c6b13f78ac8.jpg)
';