【最初的尝试与性能下降】
Redis作者在最初的尝试是在主线程中使用类似字典渐进式搬迁的方式来实现渐进式删除回收,这样可以达到删除大对象时不阻塞主线程的效果。
但是渐进式回收需要仔细控制回收的频率,不能回收的太猛,这会导致CPU占用过多,也不能回收太慢,因为内存回收不及时可能导致内存消耗持续增长。但是这样的方案实现后会导致服务繁忙时,QPS下降到正常水平的65%左右,这很致命。
所以作者才使用了现在的异步线程。异步线程不需要为每种数据结构适配一套渐进式的释放策略,也不用搞自适应算法来控制回收频率,只是将对象从全局字典中摘掉,往队列里一扔,主线程就做别的去了。
当然,两个线程在内存回收器的使用上会存在竞争,但是这点竞争消耗与主线程在内存回收和花费的时间比可以忽略不计。
【异步方案带来的问题】
Redis内部对象有共享机制,如图:

 


懒惰删除必须要保证彻底删除,如果底层对象是共享的,那就做不到彻底删除。
//TODO 为什么懒惰删除需要保证彻底删除?

这里我的理解是,懒惰删除意味着需要开一个异步处理的线程,如果对象共享,那就涉及到数据安全的问题,共享的对象就必须上锁。这个时候这些对象的处理实际上是串行化的,考虑到线程的来回切换,可能性能还不如一个主线程直接去处理。只有保证了对象是彻底删除,才会把两个线程的处理隔离开,也就不需要再对这些对象加锁,从而利用多线程带来的性能上的提升。
Redis作者为了支持懒惰删除,将对象共享彻底抛弃,他将这种对象结构称为share-nothing。
【删除实现】
主线程需要将删除任务传递给异步线程,它是一个普通的双向链表来传递,由于链表需要支持多线程的并发操作,所以需要锁。
执行懒惰删除时,Redis将删除操作的相关参数封装成了一个bio_job结构,然后追加到链表尾部。异步线程通过遍历链表摘取job来挨个执行异步任务。
当主线程将任务追加到队列之前需要给它加锁,追加完毕再释放锁,如果异步线程还在休眠,那么需要唤醒这个异步线程。

【参考】

《Redis深度历险 核心原理与应用》

 

posted on 2022-01-24 21:33  长江同学  阅读(155)  评论(0编辑  收藏  举报