web analytics
Redis中的BIGKEY问题的概念和解决方法

BIGKEY问题

什么才叫BIGKEY呢?

    根据阿里Redis规约
  • string是value,最大512MB但是≥10KB就是bigkey

  • list、hash、set和zset,个数超过5000就是bigkey

BIGKEY危害

  1. 内存消耗:大键需要占用大量的内存空间,在Redis服务器的内存限制下,使用大量内存可能导致服务器运行缓慢或崩溃。

  2. 网络带宽消耗:当从Redis服务器传输大键时,会消耗较多的网络带宽。这可能导致延迟增加,并影响其他客户端对Redis服务器的访问速度。

  3. 整体性能下降:由于大键可能需要更长的时间来读写,会导致Redis服务器整体的响应时间增加。这可能会影响其他请求的处理速度,并降低系统的吞吐量。

  4. 数据分片不均衡:当采用Redis集群或分片技术时,大键可能导致数据在不同节点之间分布不均衡。这可能导致某些节点负载过重,而其他节点负载过轻,从而降低了整体的性能和可伸缩性。

如何发现BIGKEYS

  1. redis-cli -a password --bigkeys

    给出每种数据结构Top 1 bigkey,同时给出每种数据类型的键值个数+平均大小

    想查询大于10kb的所有key, --bigkeys参数就无能为力了,需要用到memory usage来计算每个键值的字节数

  2. MEMORY USAGE

    MEMORY USAGE 命令给出一个 key 和它的值在 RAM 中所占用的字节数。

    返回的结果是 key的值以及为管理该key分配的内存总字节数。

    对于嵌套数据类型,可以使用选项 SAMPLES,其中 count 表示抽样的元素个数,默认值为5。当需要抽样所有元素时,使用 SAMPLES 0。

    语法:MEMORY USAGE key [SAMPLES count]

如何删除

    string类型控制在10KB以内,hash、list、 set、zset元素个数不要超过5000。

反例:一个包含200万个元素的list。

    **非字符串的bigkey,不要使用del删除,使用hscan,sscan、zscan方式渐进式删除**,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del 操作,造成阻塞,而且该操作不会出现在慢查询中(latency可查))。

为什么不能一次性删除?

    Redis的BigKey指的是占用较大内存空间的键,例如存储大量数据的集合或哈希表。删除BigKey时,如果一次性删除,可能会导致Redis服务器在删除过程中出现长时间的阻塞,影响其他客户端对Redis的访问。
    当Redis执行删除操作时,它会先将该键标记为删除状态,然后通过后续的异步方式逐步释放该键占用的内存空间。这种异步释放的方式能够避免大规模内存回收对Redis服务器的性能造成负面影响。

另外,一次性删除BigKey的过程可能会导致网络传输数据量巨大,特别是当该键所占用内存很大时。这可能会对Redis服务器的带宽和网络负载造成压力,导致网络延迟增加,甚至造成服务不可用的情况。

删除命令

  1. String

    一般用del 如果过于庞大采用unlink

  2. hash

    使用hscan每次获取少量field-value,再使用hdel删除每个field。(scan内容在下方)

    public void delBigHash(String host, int port, String password, String bigHashKey) {
       // 创建 Jedis 对象,连接 Redis 服务器
       Jedis jedis = new Jedis(host, port);
    
       // 如果密码不为空,则进行身份验证
       if (password != null && !"".equals(password)) {
           jedis.auth(password);
       }
    
       // 设置扫描参数,每次扫描 100 条数据
       ScanParams scanParams = new ScanParams().count(100);
        // 设置游标
       String cursor = "0";
    
       // 循环执行扫描操作,直到游标变为 "0" 才结束
       do {
           // 执行散列数据的扫描操作
           ScanResult> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
           List> entryList = scanResult.getResult();
    
           // 如果扫描结果不为空,则逐个删除散列中的键值对
           if (entryList != null && !entryList.isEmpty()) {
               for (Entry entry : entryList) {
                   jedis.hdel(bigHashKey, entry.getKey());
               }
           }
    
           // 获取下一次扫描的游标
           cursor = scanResult.getStringCursor();
       } while (!"0".equals(cursor));
    
       // 最后删除大键
       jedis.del(bigHashKey);
    }
  3. list

    使用Itrim渐进式逐步删除,直到全部删除完成

    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);
       }   
       Pipeline pipeline = jedis.pipelined();
       // 定义每次截取的数量
       int chunkSize = 100;
       long delCount = 0;
       while (true) {
           // 获取当前剩余列表长度
           long llen = jedis.llen(bigListKey);
           if (llen <= 0) {
               break;
           }
           // 计算本次截取的范围
           long start = 0;
           long end = Math.min(llen - 1, start + chunkSize - 1);
           // 执行截取操作
           pipeline.ltrim(bigListKey, start, end);
           delCount += (end - start + 1);
           // 如果截取的范围已超过总长度,跳出循环
           if (end >= llen - 1) {
               break;
           }
       }
       // 提交管道执行
       pipeline.sync();
       // 最终删除列表
       jedis.del(bigListKey);
       // 关闭连接
       jedis.close();
    }
  4. set

    使用sscan每次获取部分元素,再使用srem命令删除每个元素

    public void delBigSet(String host, int port, String password, String bigSetKey) {
       // 创建Jedis对象并连接Redis
       Jedis jedis = new Jedis(host, port);
       // 如果密码不为空,则进行身份验证
       if (password != null && !"".equals(password)) {
           jedis.auth(password);
       }
       // 设置每次扫描的数量为100
       ScanParams scanParams = new ScanParams().count(100);
       // 设置初始游标为0
       String cursor = "0";
       do {
           // 扫描大集合数据
           ScanResult scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
           // 获取扫描结果中的成员列表
           List memberList = scanResult.getResult();
    
           // 如果成员列表不为空,则逐个删除成员
           if (memberList != null && !memberList.isEmpty()) {
               for (String member : memberList) {
                   jedis.srem(bigSetKey, member);
               }
    
               // 获取下一次扫描的游标
               cursor = scanResult.getStringCursor();
           }
       } while (!"0".equals(cursor));  // 当游标为0时表示扫 描结束
       // 删除大集合的键
       jedis.del(bigSetKey);
    } 
  5. zset

    使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK | 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);
       }
       // 设置扫描参数,每次扫描100个元素
       ScanParams scanParams = new ScanParams().count(100);
       String cursor = "0";
       // 循环扫描并删除元素
       do {
           ScanResult scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
           List 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);
    }

BIGKEY生产调优

    Redis.config 配置文件中的LAZY FREEING相关选项,阻塞型和非阻塞型删除。

    阻塞型:del   非阻塞型:unlink
lazyfree-lazy-server-del yes
replica-lazy-flush yes
lazyfree-lazy-user-del yes

重要事项

限制关键命令

    生产上限制 keys */flushdb/flushall 等危险命令防止误删误用

解决方法:通过配置设置禁用或者重命名这些命令,redis.conf在security这一项中

rename command 命令 想要改成的命令
# 举例子
rename command keys ""
rename command flushdb ""

设置后就输入被设置的命令就无法使用

image-20230807144554423

通过scan遍历一部分key

介绍

    SCAN 命令是一个基于游标的迭代器,每次被调用之后,都会向用户返回一个新的游标,用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。SCAN 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境。

有四种类型

  • SCAN 用于遍历当前数据库中的键。
  • SSCAN 用于遍历集合键中的元素。
  • HSCAN 用于遍历哈希键中的键值对。
  • ZSCAN 用于遍历有序集合中的元素(包括元素成员和元素分值)。

使用语法

SCAN cursor [MATCH pattern] [COUNT count]
  • cursor - 游标。
  • pattern - 匹配的模式。
  • count - 指定从数据集里返回多少元素,默认值为 10 。

返回值

    SCAN 返回一个包含两个元素的数组,
  • 第一个元素是用于进行下一次迭代的新游标,

  • 第二个元素则是一个数组,这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。

    当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的遍历,而当服务器向用户返回值为 0 的游标时, 表示遍历已结束

非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。

redis 127.0.0.1:6379> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"
注意事项
对元素的模式匹配工作是在命令从数据集中取出元素之后, 向客户端返回元素之前的这段时间内进行的, 所以如果被遍历的数据集中只有少量元素和模式相匹配, 那么遍历命令或许会在多次执行中都不返回任何元素。
redis 127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
redis 127.0.0.1:6379> scan 288 MATCH *11*
1) "224"
2) (empty list or set)
redis 127.0.0.1:6379> scan 224 MATCH *11*
1) "80"
2) (empty list or set)
redis 127.0.0.1:6379> scan 80 MATCH *11*
1) "176"
2) (empty list or set)
redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000
1) "0"
2)  1) "key:611"
    2) "key:711"
    3) "key:118"
    4) "key:117"
    5) "key:311"
    6) "key:112"
    7) "key:111"
    8) "key:110"
    9) "key:113"
   10) "key:211"
   11) "key:411"
   12) "key:115"
   13) "key:116"
   14) "key:114"
   15) "key:119"
   16) "key:811"
   17) "key:511"
   18) "key:11"
redis 127.0.0.1:6379>

我们可以看到, 以上的大部分遍历都不返回任何元素。

在最后一次遍历, 我们通过将 COUNT 选项的参数设置为 1000 , 强制命令为本次遍历扫描更多元素, 从而使得命令返回的元素也变多了。

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇