Skip to content

Commit 4574b0b

Browse files
authored
关于对象池的设计
1 parent cd26979 commit 4574b0b

1 file changed

Lines changed: 99 additions & 2 deletions

File tree

readMe.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class RedisObject{
238238
##### <li> 异步线程的逻辑:
239239
遍历所有老的slot,将这些slot添加到新的map上面
240240
关键点: 每一个slot都需要加锁来保证正确性
241-
241+
</br>
242242
EasyLock lock = lockArr[index & (lockNum - 1)];
243243
lock.lock(); // 需要lock住固定的index
244244
Node node = map.table[index];
@@ -252,7 +252,7 @@ class RedisObject{
252252
map.table[index] = null;//所有的都设置为null,这样就成功将map本身给解决了,map本身的size也应该修改
253253
tmpSize.addAndGet(-nodeNum);
254254
lock.unlock();
255-
255+
</br>
256256
##### <li> 线程安全和性能的讨论
257257
首先我们引入了一个新的中间层 `RedisDict`
258258
这个东西负责 rehash操作和 普通RedisHashMap到RedisConcurrentHashMap的转换,这样对于使用RedisDict的操作就完全屏蔽了。
@@ -327,3 +327,100 @@ class RedisObject{
327327
在开始rehash的时候,也要根据「是否已经处于并发状态」,「是否可以直接执行rehash」
328328

329329
死锁,注意到获取size的时候同样存在线程安全的问题,但是size加的是「readLock」,如果调用的上层恰好使用「writeLock」,就会导致死锁。所以还需要一个无锁版本的size
330+
331+
---
332+
#### Version-4
333+
##### 改动: 针对GC频繁的问题,将最常用的数据结构String改造为RedisString,并且实现了一套对象池的技术,手动管理很多对象的分配,释放,统计信息等策略;
334+
335+
##### 首先说明一下idea的来源,由于运行的时候发现GC在高负载的时候非常的频繁,差不多1s就会产生一次GC,所以思考怎么解决这个问题;
336+
##### 思路如下: 首先我们需要知道GC频繁的原因,因为每一次请求都会生成大量的临时对象:
337+
338+
339+
1 requestId 2 type 3 content
340+
然后 content又会根据命令的类型,被解析成 key 和 其它参数,中间的一些复制过程都会产生很多新的String
341+
342+
2 hash里面的entry对象,这个对象只有在有新数据加入的时候才会被生成
343+
344+
##### 那么将新生代扩大是不是能够解决这个问题呢?
345+
346+
答案是不能,因为cache类型的应用本质上大部分对象就是会存储很久的,从这个主线上来说老年代应该尽可能的大才对;
347+
所以思考得出,那么只可能尽量减少这些临时对象的生成;
348+
进一步的,String本身是immutable的,这个特点有数不清的好处,但是在这里不适用,第一导致无法像redis那样对字符串自己进行操作,第二,没有办法将对象池化(不能修改内容所以不能复用)
349+
350+
351+
##### 这就得出一个结论,必须将所有的String都替换成一个我们自己设计的,可以池化的类型: RedisString
352+
##### 这里实现的时候有这么一些难点:
353+
<li>1 所有String都要替换,那么只能从netty bytebuf那里开始就不产生String,同时要实现在不new 新对象的情况下将RedisString从大对象拆分成小对象;另外不能使用String,那么fastJson也没有办法直接使用了,所以还需要考虑怎么自定义简单协议进行parse;
354+
355+
<li>2 RedisString本身是可以修改的,这确实也导致一些诡异的bug,根本原因就是还有对象在引用它,但是缺把它回收,这会导致各种奇怪问题: 死锁,对象被破坏.解析错误等等;所以分配和释放的时机要非常的小心才可以;
356+
357+
<li> 3 分配的策略,对象池的分配算法比内存池简单,因为对象的大小一般是一组固定的大小;但是要考虑一些问题:
358+
对象的大小设置为多少合适?
359+
对象池的大小需要动态的变化,怎么设计变化的策略?
360+
对象释放的时候有线程竞争,怎么保证性能和安全?
361+
如何为一个分配的请求快速定位到合适的对象大小?
362+
如何设计一套合理的接口,将对象池本身逻辑抽象出来,这样实现其它的池化对象也相对简单清晰?
363+
364+
---
365+
下面就简单描述一下解决了哪些问题,和解决的思路:
366+
<li>1 替换所有String,体力活,要求是细心不出错,替换的时候还要重写hashcode,equal,parseInt等各种方法
367+
</br>
368+
<li>2 如何设计一套接口体系,首先设计AbstractPooledObject这个抽象类,然后声明好length(),size(),newInstance(),release(),等抽象方法;然后设计一个AbstractObjectPool这样的抽象类,这个抽象类主要实现对象池,它具有很多组不同大小的对象container,分配的时候就迅速定位到合适大小的container,释放的时候也一样;如果找不到合适大小的container或者说没有对象可以分配,那么就调用newInstance来分配新的对象;
369+
</br>
370+
<li>3 怎么设计策略?
371+
</br>
372+
```
373+
首先我们将对象池理解为一个管道,一边是流入的对象(release),一边是流出(allocate),那么我们取一段时间之内这两者的调和平均数作为对象池的大小,就可以比较好的适应需求; 所以需要进行统计,每次allocate的时候对申请对应大小的数组位置+1,release的时候也对对应大小的数组的位置+1; 这样就相当于统计一段时间之内分配次数和释放次数;
374+
我们考虑这样一种情况: 将JavaRedis作为一个分布式的锁的实现,也就是set一下然后很快就释放掉;这种情况下中间有一个时间差delay,可以想到在delay结束之前,是不会有release被调用的,但是只要过了这个delay,那么就有数据被释放了,中间阶段都会达成一个平衡的状态;大概像下面一样:
375+
allocate allocate ..................... allocate ........ allocate...
376+
[------ delay(3s)--------] release release ..... release ......
377+
当然中间可能存在一些波动,比如delay变化了,或者一段时间没有收到这样情况的请求;
378+
所以需要将统计的时间稍微扩大一点,变化平缓一点: 所以把这个数据当成一个时间序列我们进行加权:
379+
380+
allocateAccumulation[index] = allocateAccumulation[index] * scaleDown + allocateArray[index]
381+
releaseAccumulation[index] = releaseAccumulation[index] * scaleDown + releaseArray[index]
382+
383+
这样开启一个定时任务,每s运行一次,就可以得到过去一段时间里面 分配 和 释放的估计值,然后求调和平均数,得到这个大小下的对象池的估计大小;
384+
有了这个大小就可以实现各种策略了: 连续N次估计值大于threshold才分配对象,如果发现估计值大于当前的对象池大小,可以设计一套策略来调整大小;(比如大于原来的1.5倍就申请新的大小的对象池进行迁移)
385+
如果估计值太小,说明当前场景不适合对象池(分配对象也要开销的),所以可以适当减少对象池大小,连续N次这样就直接释放,但是
386+
如果又有新的分配需求,那么又满满将数据加回去(比如连续1024次不满足就释放对象池,设置这个值为-1024.如果有新的大量请求出现,那么就折半衰减负数,这样可以快速恢复到对象池分配的状态)
387+
```
388+
389+
<li>4 怎么迅速定位?
390+
我们将设计对象池的长度为一系列长度 s1,s2,s3....sn,然后在中间插入 一些 2^n大小的数据,这样给定一个len,就利用JDK里面的办法先定位到第一个大于len的2^k,然后往回找一下,看哪个是第一个大于len且在我们初始化好的lengthTable里面,返回对应的index即可; 如果该池没有启用或者没有合适对象,可以尝试去更大的对象池找一下,但是不得超过所需的两倍大小;
391+
392+
释放的时候也是一样的策略,关键是找到一个合适的长度s,使得 s <= len(注意这里和分配的要求恰恰相反,可以思考下为什么),
393+
然后尝试将数据返回对应的容器,如果满了就算了;
394+
395+
<li>5 怎么确保释放和分配线程安全?
396+
首先要考虑异步线程allocate和release会怎么样? 首先我们当然可以使用并发容器来控制分配或者加锁,但是这对性能的损耗比较大,所以一个办法是: 只在主线程进行对象池的分配,事实上目前的异步线程没有要分配对象的需求,只有释放的需求;
397+
释放的时候将数据放到一个并发队列里面,每过一段时间主线程就从这个队列统一将数据放到真正的对象池里面;
398+
这样就不会影响太多主线程分配和释放的性能;(还可以优化,就是用普通队列 lazyRemovedQue,但是异步线程每次放到一个线程私有的队列里面,然后定时将这个队列放入lazyRemovedQue,这样就只要加锁一次,然后主线程也只要加锁一次从lazyRemovedQue里面移动)
399+
另外为了性能,就只需要将这个对象池的容器使用数组实现的普通Queue即可(目前是手动实现一个,因为功能比较少,也可以用ArrayDeque)这样缓存更加友好;性能更好
400+
401+
402+
<li>6 怎么保证一个线程里面release的时候是安全的?
403+
首先明确问题是什么: 假设我有一个key(RedisString),这个key在什么时候应该被释放呢? 有这些场景:
404+
1 get请求,只读请求,显然可以释放
405+
2 set请求,如果这个ke已经重复了,那么可以直接释放,同时还可以释放老的value
406+
难点在于: 我们如何保证这个RedisString释放的时候下面没有使用了? 比如如果现在处于 rehash的状态,那么
407+
我们有这样的操作 map.put(key,val); rehashmap.remove(key); 那么如果你在RedisHashMap这个层次上操作,就没有办法知道
408+
上层的情况(也不应该知道上层的调用逻辑); 有时候我们还利用key获取了EasyLock,如果你在里面就释放,会导致接下来获取的lock不正确从而产生死锁;
409+
总结一下 Command -> RedisDb -> RedisDict -> RedisConcurrentHashMap -> RedisHashMap
410+
-> RedisHashMap
411+
ExpireHelper -> RedisDict也使用到了对应的调用链
412+
我们要找到一个合适的位置释放该释放的东西,避免提前释放的错误;
413+
</br>
414+
415+
目前暂时先手动把逻辑写对,但是最好的做法将所有的释放集中到一个层面来管理:
416+
</br>
417+
```
418+
* 1 所有从CommnadHandler里面传入的key和value,我们设置一个isUsed的标记,如果这个key没有被使用过
419+
* 那么就直接在RedisDb这个api的层面就进行释放;如果被使用了那么就不进行释放
420+
* 这样对于那些只读操作的release管理就非常简单,对于set key,val这样的操作,如果key已经存在了,那么设置为isUsed为false即可
421+
*
422+
* 2 对于老的key,value,这个比较麻烦,因为在几个地方都有释放的规则,所以对于老的key,value需要我们将这个数据返回到最上层
423+
* 这样就避免了依赖,同时在最上层进行释放;
424+
* 另外有些地方直接调用了RedisDict而没有通过走RedisDb的接口,这个也比较麻烦,所以可能需要重构一下,所有对这两个map的操作都要走
425+
* RedisDb,否则容易出现release之后下面的逻辑又用到这个key的情况,较难管理
426+
```

0 commit comments

Comments
 (0)