目录

一般对于单实例的redis或者一主一备的redis来说,不需要考虑hot key的问题。但是随着业务量的上升,redis集群也自然而然的会成为一个选择。

hot key的概念

当使用redis集群来作为缓存的时候,如果在业务上碰到大促,或者正好有一个非常热的帖子的时候,对应的缓存会被频繁访问。而这个缓存会落在redis集群的同一台集群上,导致数据频繁的访问同一台机器,造成集群性能的不均衡。这个就是所谓的hot key的问题。

hot key的解决办法

很自然的,如果要频繁的访问同一个key,但是有不想让这个key一直落到同一台机器上,我们就需要在其他机器上复制这个key的备份,让请求进来的时候,会去随机的到各台机器上访问缓存。所以剩下的问题就是如何做到让请求平均的分布到各台机器上。

简单的,假设有N台机器,当请求到来的时候,我们随机从1到N之间随机选择一台机器进行访问。如何选择呢?因为redis集群中其实是根据key来做对应的,所以我们可以产生一个随机值作为key的后缀,由key变成key_suffix。

同时,为了防止有相同的后缀在做了映射之后仍旧会集中在某些机器上,一般需要把随机值的上限放大,比如取集群数量N的2倍。

因此有了最简单的版本:

const M = N * 2
random = GenRandom(0, M)
bakHotKey = hotKey + “_” + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = GetFromDB()
    redis.SET(bakHotKey, expireTime)
}

解决过期带来的滚雪球效应

虽然有了最初的版本,但是生产环境这么用肯定是有问题的。既然是hot key,短时间内必然有大量的访问请求, 上边的代码设置了redis集群中的各台机器都有相同的过期时间。 如果redis集群中的缓存集中过期,必然都会把压力转移到DB上。 为了防止这种雪崩效应,需要将各台机器的过期时间都尽量设置的不一样。所以可以在过期时间上再加上一个随机值,有了第二个版本。

const M = N * 2
random = GenRandom(0, M)
bakHotKey = hotKey + “_” + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = GetFromDB()
    redis.SET(bakHotKey, expireTime + GenRandom(0,5))
}

为了进一步提升性能,既然集群中的机器都有这个key的备份,为什么每台机器上bakHotKey不存在的时候都要去访问DB呢, 完全可以在redis自己内部解决。因此仍旧可以保存一份原来的hotKey在缓存中,所有的bakHotKey失效的时候,都从hotKey去同步数据。只有当hotKey也失效的时候,才去DB中访问数据。这样即使先后有好几台机器里边的缓存失效了,只有一台机器真正的去DB访问了数据,其他的redis都从之后的hotKey的缓存中备份一份数据出来。

const M = N * 2
random = GenRandom(0, M)
bakHotKey = hotKey + “_” + random
// 拉取备份的bakHotKey
data = redis.GET(bakHotKey)
if data == NULL {
    // bakHotKey失效,尝试拉取hotKey
    data = redis.GET(hotKey)
    if data == NULL {
        // hotKey也失效,从DB拉取
        data = GetFromDB()
        // 写入主数据
        redis.SET(hotKey, data, expireTime)
        // 写入备份数据
        redis.SET(bakHotKey, data, expireTime + GenRandom(0,5))
    } else {
        // 否则只需要直接写入bakHotKey,无需访问DB
         redis.SET(bakHotKey, data, expireTime + GenRandom(0,5))
    }
}

hotKey的检测

Redis 4.0新增了一类内存逐出策略:LFU(Least Frequently Used)。LFU表示最不经常使用。相比LRU,LRU对于不怎么使用的key,如果偶然用了一下,也是类似激活了一下这个key,不会短时间内删除;但是LFU是统计key的总体的使用频率,删除使用频率最小的key。

redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可,示例如下:

$./redis-cli --hotkeys

# Scanning the entire keyspace to find hot keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Hot key 'counter:000000000002' found so far with counter 87
[00.00%] Hot key 'key:000000000001' found so far with counter 254
[00.00%] Hot key 'mylist' found so far with counter 107
[00.00%] Hot key 'key:000000000000' found so far with counter 254
[45.45%] Hot key 'counter:000000000001' found so far with counter 87
[45.45%] Hot key 'key:000000000002' found so far with counter 254
[45.45%] Hot key 'myset' found so far with counter 64
[45.45%] Hot key 'counter:000000000000' found so far with counter 93

-------- summary -------

Sampled 22 keys in the keyspace!
hot key found with counter: 254    keyname: key:000000000001
hot key found with counter: 254    keyname: key:000000000000
hot key found with counter: 254    keyname: key:000000000002
hot key found with counter: 107    keyname: mylist
hot key found with counter: 93    keyname: counter:000000000000
hot key found with counter: 87    keyname: counter:000000000002
hot key found with counter: 87    keyname: counter:000000000001
hot key found with counter: 64    keyname: myset

关于LFU的实现可以参考《Redis 4.0之基于LFU的热点key发现机制》