Redis的内存存储结构是个大的字典存储,也就是我们通常说的哈希表。Redis小到可以存储几万记录的CACHE,大到可以存储几千万甚至上亿的记录(看内存而定),这充分说明Redis作为缓冲的强大。Redis的核心数据结构就是字典(dict),dict在数据量不断增大的过程中,会遇到HASH(key)碰撞的问题,如果DICT不够大,碰撞的概率增大,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢。如果数据量从几千万变成几万,不断减小的过程,DICT内存却会造成不必要的浪费。Redis的dict在设计的过程中充分考虑了dict自动扩大和收缩,实现了一个称之为rehash的过程。使dict出发rehash的条件有两个:
1)总的元素个数 除 DICT桶的个数得到每个桶平均存储的元素个数(pre_num),如果 pre_num > dict_force_resize_ratio,就会触发dict 扩大操作。dict_force_resize_ratio = 5。
2)在总元素 * 10 < 桶的个数,也就是,填充率必须<10%,DICT便会进行收缩,让total / bk_num 接近 1:1。
dict rehash扩大流程:
源代码函数调用和解析:
dictAddRaw->_dictKeyIndex->_dictExpandIfNeeded->dictExpand,这个函数调用关系是需要扩大dict的调用关系,
_dictKeyIndex函数代码:
- static int _dictKeyIndex(dict *d, const void *key)
- {
- unsigned int h, idx, table;
- dictEntry *he;
-
-
- if (_dictExpandIfNeeded(d) == DICT_ERR)
- return -1;
-
-
- h = dictHashKey(d, key);
-
-
- for (table = 0; table <= 1; table++) {
-
-
-
- idx = h & d->ht[table].sizemask;
-
-
-
-
- he = d->ht[table].table[idx];
- while(he) {
-
- if (dictCompareKeys(d, key, he->key))
- return -1;
-
- he = he->next;
- }
-
-
-
- if (!dictIsRehashing(d)) break;
- }
-
- return idx;
- }
_dictExpandIfNeeded函数代码解析:
- static int _dictExpandIfNeeded(dict *d)
- {
-
- if (dictIsRehashing(d)) return DICT_OK;
-
-
-
- if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
-
-
-
-
-
-
-
-
-
- if (d->ht[0].used >= d->ht[0].size &&
- (dict_can_resize ||
- d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
- {
- return dictExpand(d, d->ht[0].used*2);
- }
-
- return DICT_OK;
- }
dict rehash缩小流程:
源代码函数调用和解析:
serverCron->tryResizeHashTables->dictResize->dictExpand
serverCron函数是个心跳函数,调用tryResizeHashTables段为:
- int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
- ....
- if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
-
- tryResizeHashTables();
- if (server.activerehashing) incrementallyRehash();
- }
- ....
- }
tryResizeHashTables函数代码分析:
- void tryResizeHashTables(void) {
- int j;
-
- for (j = 0; j < server.dbnum; j++) {
-
-
- if (htNeedsResize(server.db[j].dict))
- dictResize(server.db[j].dict);
-
-
- if (htNeedsResize(server.db[j].expires))
- dictResize(server.db[j].expires);
- }
- }
htNeedsResize函数是判断是否可以需要进行dict缩小的条件判断,填充率必须>10%,否则会进行缩小,具体代码如下:
- int htNeedsResize(dict *dict) {
- long long size, used;
-
-
- size = dictSlots(dict);
-
-
- used = dictSize(dict);
-
-
-
-
- return (size && used && size > DICT_HT_INITIAL_SIZE &&
- (used*100/size < REDIS_HT_MINFILL));
- }
dictResize函数代码:
- int dictResize(dict *d)
- {
- int minimal;
-
-
-
- if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
-
- minimal = d->ht[0].used;
-
- if (minimal < DICT_HT_INITIAL_SIZE)
- minimal = DICT_HT_INITIAL_SIZE;
-
- return dictExpand(d, minimal);
- }
以上两个过程最终调用了dictExpand函数,这个函数主要是产生一个新的HASH表(dictht),并让将dict.rehashidx= 0。表示开始进行rehash动作。具体的rehash动作是将ht[0]的数据按照hash隐射的规则重新隐射到 ht[1]上.具体代码如下:
- int dictExpand(dict *d, unsigned long size)
- {
- dictht n;
-
-
- unsigned long realsize = _dictNextPower(size);
- if (dictIsRehashing(d) || d->ht[0].used > size || d->ht[0].size == realsize)
- return DICT_ERR;
-
-
- n.size = realsize;
- n.sizemask = realsize-1;
- n.table = zcalloc(realsize*sizeof(dictEntry*));
- n.used = 0;
-
-
-
- if (d->ht[0].table == NULL) {
- d->ht[0] = n;
- return DICT_OK;
- }
-
-
-
-
- d->ht[1] = n;
- d->rehashidx = 0;
-
- return DICT_OK;
- }
字典dict的rehashidx被设置成0后,就表示开始rehash动作,在心跳函数执行的过程,会检查到这个标志,如果需要rehash,就行进行渐进式rehash动作。函数调用的过程为:
serverCron->incrementallyRehash->dictRehashMilliseconds->dictRehash
incrementallyRehash函数代码:
-
-
-
-
- void incrementallyRehash(void) {
- int j;
-
- for (j = 0; j < server.dbnum; j++) {
-
- if (dictIsRehashing(server.db[j].dict)) {
- dictRehashMilliseconds(server.db[j].dict,1);
- break;
- }
- ...
- }
dictRehashMilliseconds函数是按照指定的CPU运算的毫秒数,执行rehash动作,每次一个100个为单位执行。代码如下:
-
-
-
- int dictRehashMilliseconds(dict *d, int ms) {
- long long start = timeInMilliseconds();
- int rehashes = 0;
-
- while(dictRehash(d,100)) {
- rehashes += 100;
- if (timeInMilliseconds()-start > ms) break;
- }
- return rehashes;
- }
-
-
-
-
-
-
-
-
-
- int dictRehash(dict *d, int n) {
- if (!dictIsRehashing(d)) return 0;
-
- while(n--) {
- dictEntry *de, *nextde;
-
-
- if (d->ht[0].used == 0) {
-
-
- zfree(d->ht[0].table);
-
-
- d->ht[0] = d->ht[1];
-
-
- _dictReset(&d->ht[1]);
-
- d->rehashidx = -1;
-
- return 0;
- }
- assert(d->ht[0].size > (unsigned)d->rehashidx);
-
- while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
-
- de = d->ht[0].table[d->rehashidx];
-
-
-
- while(de) {
- unsigned int h;
-
- nextde = de->next;
-
-
-
- h = dictHashKey(d, de->key) & d->ht[1].sizemask;
-
-
- de->next = d->ht[1].table[h];
- d->ht[1].table[h] = de;
-
-
- d->ht[0].used--;
- d->ht[1].used++;
-
- de = nextde;
- }
-
-
- d->ht[0].table[d->rehashidx] = NULL;
-
-
- d->rehashidx++;
- }
-
-
- return 1;
- }
总结,Redis的rehash动作是一个内存管理和数据管理的一个核心操作,由于Redis主要使用单线程做数据管理和消息效应,它的rehash数据迁移过程采用的是渐进式的数据迁移模式,这样做是为了防止rehash过程太长堵塞数据处理线程。并没有采用memcached的多线程迁移模式。关于memcached的rehash过程,以后再做介绍。从redis的rehash过程设计的很巧,也很优雅。在这里值得注意的是,redis在find数据的时候,是同时查找正在迁移的ht[0]和被迁移的ht[1]。防止迁移过程数据命不中的问题。