Redis“热Key”和“BigKey”

115 阅读5分钟

Redis的BigKey

如图避免了bigkey就能避免80%的问题,那还等什么直接开锤。

1、概念:

Redis是基于内存的Key-Value数据存储系统,"Big Key" 通常指的是键的值(Value)过大,而不是键(Key)本身过大。

2、多大算大

参考《阿里云Redis开发规范》

3、有哪些危害

1)、内存分部不均匀,数据倾斜

如下图所示:

集群中有4个分片,每个分片大约 有102个key,实际上是均匀分布。图中第三个key叫key301,hash301,中间有一个放了200w的hash,但因为根据 hash301打散的这个key是个 bigkey,严重造成数据倾斜。

别的key只用了10%或20%的内存,key301用了约80%,而且大概率是热点。上图的使用用法,有可能造成有一个分片 内存满了,访问出了问题,但是其他分片却用的很闲。问题分片的访问比较热,造成网卡打满,或者CPU打满,导致限 流,服务可能就夯住了。

2)、网络传输

  • 传输延迟:读取或写入大型值需要更多的时间,尤其是在网络延迟较高的情况下,会导致操作的响应时间变长。
  • 网络拥塞:大型值的读取或写入可能会占用大量的网络带宽,可能导致网络拥塞,影响其他客户端的通信。
  • 传输错误:在网络不稳定的情况下,大型值的传输可能会导致数据包丢失或错误,需要额外的重传操作。
  • 客户端超时:如果一个键的值过大,可能会导致客户端的超时错误,因为在网络传输大量数据时可能会超过客户端的超时阈值。

3)、超时删除

  • 删除操作超时:当尝试删除一个大型键时,可能会因为数据量过大而导致删除操作超时。
  • 删除操作阻塞其他操作:当尝试删除一个大型键时,可能会导致Redis服务器在执行删除操作期间阻塞其他操作。
  • 内存占用过高:在执行删除操作期间,可能会导致内存占用过高,影响Redis服务器的性能。
  • 持久化问题:如果使用持久化方式如RDB或AOF,大型键的删除可能会导致持久化文件变得非常大。

4、产生原因举例

  • 使用Redis作为缓存来存储特别热门的商品信息,它有成百上千的评论和多张高清图片。
  • 汇总统计某个日积月累的报表的数据。
  • 社交类,粉丝列表数据等

5、排查bigkey

(1)、MEMORY USAGE指令:

MEMORY USAGE 是Redis提供的一个命令,用于获取指定键的内存占用情况。它可以用来查看Redis中某个键所占用的内存大小,以字节(bytes)为单位。

注意,MEMORY USAGE 命令仅适用于Redis 4.0及以上版本

(2)、redis-cli --bigkeys

  • 遍历所有的键(keys)。
  • 对每个键执行 MEMORY USAGE 命令以获取其内存占用情况。
  • 如果某个键的内存占用超过了阈值(默认为1024字节),则将其列为大型键。
redis-cli --bigkeys
# 可以通过 --threshold <value> 参数来设置
redis-cli --bigkeys --threshold 2048

6、如何删除bigkey

普通命令的删除操作

String一般用del,过于庞大unlink
Hashscan获取少量的field-value,在使用hdel删除
List使用itrim渐进式逐步删除
Setsscan获取部分元素再使用srem删除每一个元素
Zsetzscan获取部分元素再使用ZREMRANGEBYRANK删除每一个

1、hash删除:hscan + hdel

public void delBigHash(String host, int port, String password, String bigHashKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
        List<Entry<String, String>> entryList = scanResult.getResult();
        if (entryList != null && !entryList.isEmpty()) {
            for (Entry<String, String> entry : entryList) {
                jedis.hdel(bigHashKey, entry.getKey());
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    //删除bigkey
    jedis.del(bigHashKey);
}

2、List删除: ltrim

public void delBigList(String host, int port, String password, String bigListKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    long llen = jedis.llen(bigListKey);
    int counter = 0;
    int left = 100;
    while (counter < llen) {
        //每次从左侧截掉100个
        jedis.ltrim(bigListKey, left, llen);
        counter += left;
    }
    //最终删除key
    jedis.del(bigListKey);
}

3、Set删除: sscan + srem

public void delBigSet(String host, int port, String password, String bigSetKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
        List<String> memberList = scanResult.getResult();
        if (memberList != null && !memberList.isEmpty()) {
            for (String member : memberList) {
                jedis.srem(bigSetKey, member);
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    //删除bigkey
    jedis.del(bigSetKey);
}

4、SortedSet删除: zscan + zrem

public void delBigZset(String host, int port, String password, String bigZsetKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
        List<Tuple> tupleList = scanResult.getResult();
        if (tupleList != null && !tupleList.isEmpty()) {
            for (Tuple tuple : tupleList) {
                jedis.zrem(bigZsetKey, tuple.getElement());
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    //删除bigkey
    jedis.del(bigZsetKey);
}

7、bigkey生产调优

1、lazyfree-lazy-server-del

lazyfree-lazy-server-del 控制在Redis的服务器(redis-server)上进行懒惰删除(lazy eviction)时的策略。懒惰删除是指在服务器需要内存空间时,Redis会查找并删除一些过期的键来释放内存,但并不会立即释放被删除键所占用的内存空间,而是在有需要时才会释放。

这个配置选项有两个可能的值:

  • lazyfree:表示启用懒惰删除策略。
  • no:表示关闭懒惰删除策略,即在删除键时会立即释放相应的内存空间。

默认情况下,Redis会使用lazyfree,也就是启用懒惰删除策略。

2、replica-lazy-flush

replica-lazy-flush 控制了Redis从节点是否允许执行懒惰清空(Lazy Flushing)操作。懒惰清空是指在从节点上执行数据清空操作时,可以选择等待一段时间再执行,以便批量处理多个清空操作,从而减少对主节点的负担。

这个配置选项有两个可能的值:

  • yes:表示从节点允许执行懒惰清空操作。
  • no:表示从节点不允许执行懒惰清空操作,即立即执行清空操作。

默认情况下,replica-lazy-flush 的值为 yes,也就是允许从节点执行懒惰清空操作。

3、lazyfree-lazy-user-del

  • lazyfree-lazy-user-del支持yes或者no。默认是no
  • 如果设置为yes,那么del命令就等价于unlink,也是非阻塞删除。