那个深夜,我登上了公司的服务器,在 Redis 命令行里敲入 keys* 后,线上开始报警,服务瞬间被卡死。

我只能举起双手,焦急地等待几千万 key 被慢慢扫描,束手无策万念俱灰的时候,我收到了 Leader 的短信:你明天不用来上班了。

虽然上面是我的臆想,事实上很多公司的运维也会禁用这些命令,来防止开发出错。
但我在群里依然看到有同学在问“为什么 Redis 不能用 keys?我觉得挺好的呀”时,为了不让上面的情况发生,我决定写下这篇文章。
如何才能优雅地遍历 Redis?作为一种可以称为数据库的组件,这是多么理所应当的要求。
终于,Redis 在 2.8.0 版本新增了众望所归的 scan 操作,从此再也不用担心敲入了 keys*,然后等着定时炸弹的引爆。
学会使用 scan 并不困难,那么问题又来了,它是如何遍历的?当遍历过程中加入了新的 key,当遍历过程中发生了扩容,Redis 是如何解决的?
抱着深入学习的态度,以及为了能够将来在面试官面前谈笑风生,让我们一起来借此探索 Redis 的设计原理。

开门见山,首先让我们来总结一下 scan 的优缺点。
优点如下:
提供键空间的遍历操作,支持游标,复杂度 O(1),整体遍历一遍只需要 O(N)。
提供结果模式匹配。
支持一次返回的数据条数设置,但仅仅是个 hints,有时候返回更多。
弱状态,所有状态只需要客户端维护一个游标。
无法提供完整的快照遍历,也就是中间如果有数据修改,可能有些涉及改动的数据遍历不到。
每次返回的数据条数不一定,极度依赖内部实现。
返回的数据可能有重复,应用层需要能够处理重入逻辑。
解析 count 和 match 参数,如果没有指定 count,默认返回 10 条数据。
开始迭代集合,如果是 key 保存为 ziplist 或者 intset,则一次性返回所有数据,没有游标(游标值直接返回 0)。
由于 Redis 设计,只有数据量比较小的时候才会保存为 ziplist 或者 intset,所以此处不会影响性能。
游标在保存为 hash 的时候发挥作用,具体入口函数为 dictScan,下文详细描述。
根据 match 参数过滤返回值,并且如果这个键已经过期也会直接过滤掉(Redis 中键过期之后并不会立即删除)。
从迭代开始到结束,哈希表没有进行 rehash。
从迭代开始到结束,哈希表进行了 rehash,但是每次迭代时,哈希表要么没开始 rehash,要么已经结束了 rehash。
从迭代开始到结束,某次或某几次迭代时哈希表正在进行 rehash。
新的键值对会存放到 ht[1] 中并且会逐步将 ht[0] 的数据转移到 ht[1]。全部 rehash 完毕后,ht[1] 赋值给 ht[0] 然后清空ht[1]。

模拟问题
①迭代过程中,没有进行 rehash
②迭代过程中,进行过 rehash
如图,当字典大小从 4 扩容到 8 时,原先在 0 slot 的数据会分散到 0(000) 与 4(100) 两个 slot,对应关系表如下:

③迭代过程中,正在进行 rehash
解决方法
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, void *privdata){
if (!dictIsRehashing(d)) {
t0 = (d->ht[0]);
m0 = t0->sizemask;
/* Emit entries at cursor */
while (de) {
fn(privdata, de);
de = de->next;
}
} else {
m0 = t0->sizemask;
m1 = t1->sizemask;
de = t0->table[v & m0];
while (de) {
fn(privdata, de);
de = de->next;
}
do {
de = t1->table[v & m1];
while (de) {
fn(privdata, de);
de = de->next;
}
v = (((v | m0) + 1) & ~m0) | (v & m0);
} while (v & (m0 ^ m1));
}
v |= ~m0;
v = rev(v);
v++;
v = rev(v);
return v;
}
接下来关注这个片段:
v |= ~m0;
v = rev(v);
v++;
v = rev(v);
接下来看图:




我们继续看源码,之前提到过 dictIsRehashing 这个函数用来判断是否正在进行 rehash。
m0 = t0->sizemask;
m1 = t1->sizemask;
de = t0->table[v & m0];
while (de) {
fn(privdata, de);
de = de->next;
}
do {
de = t1->table[v & m1];
while (de) {
fn(privdata, de);
de = de->next;
}
v = (((v | m0) + 1) & ~m0) | (v & m0);
} while (v & (m0 ^ m1));
v & (m0 ^ m1)

作者:寒食君
编辑:陶家龙、孙淑娟
出处:转载自微信公众号寒食君(ID:program_hacker)
参考:https://segmentfault.com/a/1190000018218584

精彩文章推荐:




