redis与memcache php rehash机制比较分析

 1,Redis rehash

    Redis的核心数据结构就是字典(dict),dict在数据量不断增大的过程中,会遇到HASH(key)碰撞的问题,如果DICT不够大,碰撞的概率增大,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢。如果数据量从几千万变成几万,不断减小的过程,DICT内存却会造成不必要的浪费。Redis的dict在设计的过程中充分考虑了dict自动扩容和收缩。

    a, rehash的流程

         首先为ht[1]分配空间,当负载因子大于3/2时进行扩容操作,扩容大小为第一个大于ht[0].used*2的2^n,当负载因子小于0.1进行缩容,大小为第一个大于ht[0].used的2^n。将保存在ht[0]中的所有键值对重新进行hash运算,rehash到ht[1]中。最后释放ht[0],将ht[0]指向ht[1],并为ht[1]创建空白hash表。

        rehashidx是下一个需要rehash的项在ht[0]中的索引,不需要rehash时置为-1。也就是说-1时,表示不进行rehash。iterators记录当前dict中的迭代器数,主要是为了避免在有迭代器时rehash,在有迭代器时rehash可能会造成值的丢失或重复。

    b, rehash 方式

        由于Redis主要使用单线程做数据管理和消息效应,它的rehash数据迁移过程采用的是渐进式的数据迁移模式,这样做是为了防止rehash过程太长堵塞数据处理线程。

        lazy rehashing:在每次对dict进行操作的时候执行一个slot的rehash

        active rehashing:每100ms里面使用1ms时间进行rehash。

        为了避免一次性转移带来的开销,Redis采用了平摊开销的策略,即:将转移代价平摊到每个基本操作中,如:dictAdd、dictReplace、dictFind中,每执行一次这些基本操作会触发一个桶中元素的迁移操作。从ht[0]中删除,ht[1]中添加。新添加的键值一律会保存在ht[1]中,ht[0]不添加任何操作,使ht[0]只减不增,直至变为空表。

        若数据量很大,为了提高前移速度,Redis有一个周期性任务serverCron,每隔100ms进行1ms的rehash操作,每次仅处理少量的转移任务(100个元素)。这样有点类似于操作系统的时间片轮转的调度算法。具体源码解决可参考这里       

    c, 其他

        查找某一key时,当key在ht[0]不存在且不在rehashing状态时,可以速度返回空。如果在rehashing状态,当在ht[0]没值的时候,还需要在ht[1]里查找。

2,memcache rehash机制

    与redis结构不同,memcache使用预分配内存的方式来为数据分配空间,对数据的增删改查通过一个hashtable来完成,复杂度为O(1),memcached中定义了primary_hashtable和old_hashtable两个哈希表,当hashtable的填装因子(memcached中硬编码为 3/2),assoc_maintenance_thread线程会将old_hashtable中的items以hash_bulk_move个buckets为单位,逐步移到primary_hashtable中。但hashtable不会进行缩容。

    a, rehash流程。

        未进行rehash时,primary_hashtable提供服务,当填充因子超过3/2时进行rehash,将primary_hashtable赋给old_hashtable,并为primary_hashtable分配原hashtable两倍的内存空间。

        与redis不同,memcache是多线程模型,memcache提供一个维护线程专门处理rehash中数据的转移。hash_bulk_move是数据转移粒度,即每次移动的bucket数量,线程每次处理hash_bulk_move个槽中每个桶中每个链表,重新按照新的hashpower,计算它属于哪个桶,并将其从old_hashtable中删除。当移动hash_bulk_move后整个rehash没有结束,则会等待继续转移。若转移结束,将old_hashtable的空间释放,线程挂起,等待下一轮转移。

    b, rehash 过程中的操作

        与redis相同,memcache也提供两个变量标志rehash的状态和进度,expanding标志是否在rehash过程中,expand_bucket是下一个需要进行转移的bulk。在进行增删改查时,将对应的key的槽与expand_bulk比较,若大于等于,则表示对应的槽还未转移至新hashtable,因此在old_hashtable中进行,否则则表示已经迁移至primary_hashtable中。redis是将新插入的数据只插入到ht[1]中,这样减少了转移的数据,但查找数据时需在两个表中查询,而memcache只需根据expand_bulk即可定位到数据所在hashtable。

        同时由于是多线程模型,因此存在数据并发访问的可能。memcached在扩容操作时,加的都是全局锁,就是所有item(所有hash桶中的)都是一把锁,在扩容操作中,item的操作,例如hash表的删除,插入,touch,查询都是去竞争那个全局锁,因为原来的元素在old_table中的元素需要rehashprimary_table,虽然可以在old_table中的每个桶上加锁,但是没法控制primary_table的多进程操作,小于expand_bucket的元素会直接进入primary_table,old_table的元素会按照新的hash值进入到primary中,不能确定rehash到primary_table的哪个桶中,所以这时侯只能获取全局锁。在扩容结束时,item锁被重新切换回hash桶上的锁,这里锁是分段加锁的(几个桶一个锁,这个具体数值取决与初始的worker的数量,worker数量越多,锁越细,越少hash桶公用一个锁)。

        同时当数据量非常大时,为了减少rehash过程的时间,维护线程没移动hash_bulk_move个桶后会释放全局锁,再去竞争全局锁,这样减少可以数据访问(增删改查)的等待时间,避免rehash中出现并发的情况。

3,php rehash

    PHP主要应用于WEB场景,在WEB场景针对单次请求数据之间是隔离的,并且哈希的数量是有限的,那么进行一次rehash也是很快的。所以PHP内核使用阻塞形式rehash,即rehash进行中将不能对当前哈希表进行任何操作。
    在来看Redis,常驻进程,接收客户端请求处理各项事务,并且操作的数据是相关且数据量较大的,如果使用PHP内核的那种方式就会出现:对哈希表进行rehash时,此时将阻塞所有客户端请求,并发性能会大大下降。(详细待补充)

猜你喜欢

转载自wangjixiang.iteye.com/blog/2230205