八、Redis之BigKey

目录

模拟一个100万条数据的环境

1.生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中

linux 复制代码
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;

2.通过redis提供的管道--pipe命令插入 10ow大批量数据

linux 复制代码
#插入一百万条数据数据
cat /tmp/redisTest.txt | /usr/local/bin/redis-cli -h 127.0.0.1 -p 6312 -a 111111 --pipe

#查看redis-cli可执行文件的路径
which redis-cli

#查看redis库中数据的条数
redis-cli -a 111111 -p 6312 dbsize

命令中的/usr/local/bin/redis-cli是redis的可执行文件路径,会存在不一样的情况,不清楚的可以用which redis-cli查看

新闻事故 :最近安全事故濒发啊:前几天发生了"某高级运维工程师的删库事件",今天又看到了PHP工程师在线执行了Redis危险命令导致某公司损失400万.

什么样的Redis命令 会有如此威力,造成如此大的损失?具体消息如下:

据云头条报道,某公司技术部发生2起本年度P0级特大事故,造成公司资金损失400万,原因如下:

由于PHP工程师直接操作上线redis,执行键keys *wxdb*cf8 这样的命令,导致redis锁住,导致CPU额升,引起所有支付链路卡住,等十几秒结束后,所有的请求流量全部挤压到了rds数据库中,使数据库产生了雪崩效应 ,发生了数据库客机事件。

该公司表示,如再犯类似事故,将直接开除,并表示之后会逐步收回运维部各项权限。看完这个消息后,我心又一惊,为什么这么低级的问题还在犯?为什么
线上的危险命令没有被禁用?

这事件报道出来

真是觉得很低级

且不说是哪家公司,发生这样的事故,不管是大公司还是小公司,我觉得都不应该,相关负责人应该引答辞职!
对Redis稍微有点使用经验的人都知道线上是不能执行keys*相关命令的,

虽然其模糊匹配功能使用非常方便也很强
数据量大会导致Redis锁住及CPU升,在生产环境建议禁用或者重命名!

key *遍历一百万条数据要花费多久?

刚好我们有一个库,直接执行

经过上图我们了解到,keys * 遍历一百万条数据要花二十多秒

这个指令没有 offset、limit 参数,是要一次性吐出所有满足条件的 key,由于 redis 是单线程的,其所有操作都是原子的,而 keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写Redis的其它的指令都会被延后甚至会超时报错,可能会引起缓存雪崩甚至数据库宕机。PS:offset:跳过多少条,可以理解从 第几条开始取。limit:最多取多少条

说白了,就是keys算法是遍历算法,复杂度表较高,且redis工作线程是单线程,在大数据量面前非常容易造成阻塞

清空一百万条数据要多久呢?

一秒不到,这些非常容易造成生产事故的命令,统统都是危险操作,得想个办法限制它们。

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

想要禁用危险操作,我们只需要在配置文件中加入一个配置就好了。

配置:rename-command 禁用的指令 ""

linux 复制代码
rename-command keys ""
rename-command flushdb ""
rename-command flusall ""


禁用keys *,我们要如何遍历呢?

SCAN

linux 复制代码
SCAN cursor [MATCH pattern] [COUNT count]

是Redis提供的非阻塞式全局键遍历命令,用于替代高危的 KEYS * 命令,核心是通过「游标迭代」的方式渐进式遍历 Redis 实例中的所有键,避免长时间阻塞主线程。

参数 是否必选 核心含义 通俗解释
cursor 遍历游标(整数),遍历的 "进度标记":1. 初始遍历用 0 作为游标;2. 每次执行返回新游标,用新游标继续遍历;3. 返回游标 0 表示遍历完成。 像看书的 "书签":- 游标 0 = 从第一页开始看;- 返回游标 150 = 下次从第 150 页继续;- 返回游标 0 = 整本书看完了。
MATCH pattern 模糊匹配规则,只返回符合规则的键名(支持 * 通配符)。 只找 "符合条件的键",比如 MATCH user:* 只遍历以 user: 开头的键。
COUNT count 遍历力度建议值(整数):1. 告诉 Redis"本次遍历尽量扫描 count 个哈希桶";2. 不保证返回 count 个键(最终数量由哈希桶分布 / 匹配规则决定);3. 默认值为 10。 告诉 Redis"每次尽量翻 count 页书",但最终找到的 "符合条件的内容" 可能多 / 少 / 为 0(比如翻 10 页没找到匹配的键,就返回空)。

核心特性(为什么比 KEYS 安全)

非阻塞:

  • 不同于 KEYS * 一次性遍历所有键(O (n) 复杂度,千万级键会阻塞几十秒),SCAN 每次只遍历一小部分键,执行
    耗时毫秒级,中间可穿插处理其他客户端的命令(如 GET/SET),不会导致 Redis 卡顿。
  • 游标迭代:
    遍历不是 "一次性完成",而是通过「游标」记录进度 ------ 第一次用 0 开始,后续用上次返回的游标继续,直到游标返回 0 才完成全量遍历。
  • 结果非精准性:
    • COUNT count 是 "建议值" 而非 "强制值":比如 COUNT 100 可能返回 80/120/0 个键(取决于哈希桶里的键数量、MATCH 筛选结果);
    • 遍历过程中若有键新增 / 删除,可能出现 "重复扫" 或 "漏扫"(不保证绝对完整性,适合统计 / 清理场景,不适合强一致性场景)。

哈系桶:哈希桶是 Redis 用来分类存储键的最小单元,一个桶里可以放多个键(也可以空)。

使用示例(直观理解执行流程)

redis 复制代码
# 第一步:初始遍历(游标0,匹配user:*,建议遍历100个哈希桶)
127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 100
1) "150"          # 返回新游标150(下次用这个游标)
2) 1) "user:101"  # 本次匹配到的键列表
   2) "user:102"

# 第二步:继续遍历(用游标150)
127.0.0.1:6379> SCAN 150 MATCH user:* COUNT 100
1) "320"
2) 1) "user:103"

# 最后一步:遍历完成(返回游标0)
127.0.0.1:6379> SCAN 320 MATCH user:* COUNT 100
1) "0"            # 游标0 = 全量遍历完成
2) 1) "user:999"

#以上带条件筛选的遍历,无条件筛选直接使用,从游标0开始,遍历100个哈系桶
SCAN 0 COUNT 100

scan只会遍历出key,而不会遍历出value,需要结合type key来获取键值对的类型,如果是Hash或者Set或者是Zset则分别需要用hscan、sscan、zscan指令来遍历。
遍历 Set → SSCAN;
遍历 Hash → HSCAN;
遍历 ZSet → ZSCAN;

使用方式和scan一模一样,只是scan是遍历库里所有key,而hscan、sscan、zscan遍历对应的集合。

sscan示例

linux 复制代码
SSCAN set键名 游标 [MATCH pattern] [COUNT count]

SSCAN 是 Redis 专为 Set(无序集合) 设计的非阻塞遍历命令**加粗样式**,用于渐进式遍历单个 Set 键内的所有元素(替代一次性遍历的 SMEMBERS,避免阻塞主线程),语法和 SCAN 一致
基础场景:遍历小 Set 键(无筛选 / 指定 COUNT)

redis 复制代码
# 1. 先插入测试数据
127.0.0.1:6379> SADD user_ids 101 102 103 104 105 201 202 203
(integer) 8

# 2. 初始遍历(游标0,无MATCH/COUNT,默认COUNT=10)
127.0.0.1:6379> SSCAN user_ids 0
1) "0"          # 返回游标0 → 遍历完成(Set元素少,一次扫完)
2) 1) "101"     # 本次返回的元素列表
   2) "102"
   3) "103"
   4) "104"
   5) "105"
   6) "201"
   7) "202"
   8) "203"

# 3. 带MATCH筛选(只遍历以1开头的用户ID)
127.0.0.1:6379> SSCAN user_ids 0 MATCH 1*
1) "0"
2) 1) "101"
   2) "102"
   3) "103"
   4) "104"
   5) "105"

# 4. 带COUNT指定遍历力度(建议遍历20个哈希桶)
127.0.0.1:6379> SSCAN user_ids 0 COUNT 20
1) "0"
2) 1) "101" 
   2) "102"
   3) "103" 
   4) "104" 
   5) "105" 
   6) "201" 
   7) "202" 
   8) "203"

核心场景:遍历大 Set 键(渐进式游标迭代)

如果 Set 键有 1 万 + 元素,必须用「循环游标」遍历,避免一次性遍历阻塞 Redis:

redis 复制代码
# 假设 Set 键 big_set 有 10000 个元素
# 第一步:初始遍历(游标0,COUNT=1000,建议遍历1000个哈希桶)
127.0.0.1:6379> SSCAN big_set 0 COUNT 1000
1) "5000"       # 返回新游标5000 → 下次用这个游标继续
2) 1) "elem1"   # 本次返回约1000个元素(实际数量由哈希桶分布决定)
   2) "elem2"
   ...

# 第二步:继续遍历(用游标5000)
127.0.0.1:6379> SSCAN big_set 5000 COUNT 1000
1) "8000"       # 新游标8000
2) 1) "elem1001"
   2) "elem1002"
   ...

# 最后一步:遍历完成(返回游标0)
127.0.0.1:6379> SSCAN big_set 9900 COUNT 1000
1) "0"          # 游标0 = 全量遍历完成
2) 1) "elem9999"
   2) "elem10000"

实战场景:筛选 + 分批遍历

遍历 Set 键 product_codes,只匹配以 PRO 开头的编码,每次遍历 50 个哈希桶:

redis 复制代码
# 1. 插入测试数据
127.0.0.1:6379> SADD product_codes PRO1001 PRO1002 PRO2001 PRO2002 TEST3001 TEST3002
(integer) 6

# 2. 初始遍历(游标0,MATCH PRO*,COUNT=50)
127.0.0.1:6379> SSCAN product_codes 0 MATCH PRO* COUNT 50
1) "0"
2) 1) "PRO1001"
   2) "PRO1002"
   3) "PRO2001"
   4) "PRO2002"

zscan示例

linux 复制代码
ZSCAN zset键名 游标 [MATCH pattern] [COUNT count]

ZSCAN 是 Redis 专为 ZSet(有序集合) 设计的非阻塞遍历命令,用于渐进式遍历单个 ZSet 键内的「元素 + 分值」(替代一次性遍历的 ZRANGE/ZREVRANGE,避免阻塞主线程),语法如下:

redis 复制代码
# 假设 ZSet 键 big_rank 有 10000 个元素(用户ID+积分)
# 第一步:初始遍历(游标0,COUNT=1000,建议遍历1000个哈希桶)
127.0.0.1:6379> ZSCAN big_rank 0 COUNT 1000
1) "5000"       # 返回新游标5000 → 下次用这个游标继续
2) 1) "user101" # 元素1
   2) "850"     # 元素1的分值
   3) "user102" # 元素2
   4) "920"     # 元素2的分值
   ... (约1000个元素+分值对,数量不绝对精准)

# 第二步:继续遍历(用游标5000)
127.0.0.1:6379> ZSCAN big_rank 5000 COUNT 1000
1) "8000"       # 新游标8000
2) 1) "user1001"
   2) "780"
   3) "user1002"
   4) "890"
   ...

# 最后一步:遍历完成(返回游标0)
127.0.0.1:6379> ZSCAN big_rank 9900 COUNT 1000
1) "0"          # 游标0 = 全量遍历完成
2) 1) "user9999"
   2) "999"
   3) "user10000"
   4) "1000"

核心场景:遍历大 ZSet 键(渐进式游标迭代)

如果 ZSet 键有 1 万 + 元素(比如全站用户积分排名),必须用「循环游标」遍历,避免一次性遍历阻塞

redis 复制代码
# 假设 ZSet 键 big_rank 有 10000 个元素(用户ID+积分)
# 第一步:初始遍历(游标0,COUNT=1000,建议遍历1000个哈希桶)
127.0.0.1:6379> ZSCAN big_rank 0 COUNT 1000
1) "5000"       # 返回新游标5000 → 下次用这个游标继续
2) 1) "user101" # 元素1
   2) "850"     # 元素1的分值
   3) "user102" # 元素2
   4) "920"     # 元素2的分值
   ... (约1000个元素+分值对,数量不绝对精准)

# 第二步:继续遍历(用游标5000)
127.0.0.1:6379> ZSCAN big_rank 5000 COUNT 1000
1) "8000"       # 新游标8000
2) 1) "user1001"
   2) "780"
   3) "user1002"
   4) "890"
   ...

# 最后一步:遍历完成(返回游标0)
127.0.0.1:6379> ZSCAN big_rank 9900 COUNT 1000
1) "0"          # 游标0 = 全量遍历完成
2) 1) "user9999"
   2) "999"
   3) "user10000"
   4) "1000"

实战场景:筛选 + 分批遍历(业务常用)

遍历 ZSet 键 product_sales(商品 ID + 销量),只匹配以 PRO 开头的商品 ID,每次遍历 50 个哈希桶:

redis 复制代码
# 1. 插入测试数据
127.0.0.1:6379> ZADD product_sales 5000 "PRO1001" 3000 "PRO1002" 8000 "PRO2001" 2000 "TEST3001"
(integer) 4

# 2. 初始遍历(游标0,MATCH PRO*,COUNT=50)
127.0.0.1:6379> ZSCAN product_sales 0 MATCH PRO* COUNT 50
1) "0"
2) 1) "PRO1001"
   2) "5000"
   3) "PRO1002"
   4) "3000"
   5) "PRO2001"
   6) "8000"

hscan示例

linux 复制代码
HSCAN hash键名 游标 [MATCH 字段匹配规则] [COUNT 遍历建议数]

HSCAN 是 Redis 专为 Hash(哈希表) 设计的非阻塞遍历命令,用于渐进式遍历单个 Hash 键内的「字段(field)+ 值(value)」(替代一次性遍历的 HGETALL,避免阻塞主线程),语法如下:
基础场景:遍历小 Hash 键(无筛选 / 指定 COUNT)

redis 复制代码
# 1. 先插入测试数据(HSET 键 字段1 值1 字段2 值2 ...)
127.0.0.1:6379> HSET user:101 name "张三" age 28 gender "男" addr "北京市朝阳区" phone "13800138000" email "zhangsan@xxx.com"
(integer) 6

# 2. 初始遍历(游标0,无MATCH/COUNT,默认COUNT=10)
127.0.0.1:6379> HSCAN user:101 0
1) "0"          # 返回游标0 → 遍历完成(Hash字段少,一次扫完)
2) 1) "name"    # 字段1
   2) "张三"    # 字段1的值
   3) "age"     # 字段2
   4) "28"      # 字段2的值
   5) "gender"  # 字段3
   6) "男"      # 字段3的值
   7) "addr"    # 字段4
   8) "北京市朝阳区" # 字段4的值
   9) "phone"   # 字段5
   10) "13800138000" # 字段5的值
   11) "email"  # 字段6
   12) "zhangsan@xxx.com" # 字段6的值

# 3. 带MATCH筛选(只遍历以a开头的字段)
127.0.0.1:6379> HSCAN user:101 0 MATCH a* COUNT 10
1) "0"
2) 1) "age"
   2) "28"
   3) "addr"
   4) "北京市朝阳区"

# 4. 带COUNT指定遍历力度(建议遍历20个哈希桶)
127.0.0.1:6379> HSCAN user:101 0 COUNT 20
1) "0"
2) 1) "name" 2) "张三" 3) "age" 4) "28" 5) "gender" 6) "男" 7) "addr" 8) "北京市朝阳区" 9) "phone" 10) "13800138000" 11) "email" 12) "zhangsan@xxx.com"

核心场景:遍历大 Hash 键(渐进式游标迭代)

redis 复制代码
# 假设 Hash 键 big_product:1001 有 10000 个字段(属性名+属性值)
# 第一步:初始遍历(游标0,COUNT=1000,建议遍历1000个哈希桶)
127.0.0.1:6379> HSCAN big_product:1001 0 COUNT 1000
1) "5000"       # 返回新游标5000 → 下次用这个游标继续
2) 1) "price"   # 字段1
   2) "999"     # 字段1的值
   3) "stock"   # 字段2
   4) "5000"    # 字段2的值
   ... (约1000个字段+值对,数量不绝对精准)

# 第二步:继续遍历(用游标5000)
127.0.0.1:6379> HSCAN big_product:1001 5000 COUNT 1000
1) "8000"       # 新游标8000
2) 1) "weight"
   2) "1.5kg"
   3) "size"
   4) "XL"
   ...

# 最后一步:遍历完成(返回游标0)
127.0.0.1:6379> HSCAN big_product:1001 9900 COUNT 1000
1) "0"          # 游标0 = 全量遍历完成
2) 1) "brand"
   2) "小米"
   3) "origin"
   4) "中国"

BigKey

多大算大?

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

  1. string类型。value最大512MB但是value≥10KB就是bigkey
  2. list、hash、set和zset。 个数超过5000就是bigkey

为什么 BigKey 危险?(核心危害)

  • 阻塞主线程:
    • 一次性操作 BigKey(如 HGETALL/SMEMBERS/LRANGE 0 -1)会占用 Redis 主线程大量时间,导致其他命令(如 GET/SET)等待,业务超时;
    • 删除 BigKey(如 DEL bigkey)时,Redis 需释放大量内存,可能造成秒级阻塞,引发服务雪崩。
  • 内存碎片 / 集群倾斜:
    • BigKey 会导致 Redis 实例内存占用不均(集群中某节点内存占比 80%,其他仅 20%);
    • 频繁修改 BigKey 易产生内存碎片,降低内存利用率。
  • 网络阻塞:
    • 读取 BigKey 会产生大量网络流量,占用带宽,甚至导致客户端连接超时。

如何产生的?

BigKey 不是 "突然出现" 的,而是业务设计缺陷 + 不合理的使用习惯 + 缺乏监控 共同导致的,以下是最常见的产生场景和底层原因,结合真实案例更易理解:
一、核心原因 1:业务设计阶段的 "懒规划"(最主要)

典型场景 具体行为 最终结果
消息队列设计 用单个 List 存储所有业务的消息,不做拆分 List 积压 10 万 + 元素,成为 BigKey
全量数据存储 用单个 Hash 存储所有用户的属性(如 all_users),字段随用户量增长 Hash 字段数突破 10 万,内存占用超 100MB
大文本存储 用单个 String 存储商品详情页 HTML、图片 Base64 编码、日志全量内容 String 大小从几 KB 涨到几十 MB
无分片的集合存储 用单个 Set/ZSet 存储全平台用户 ID、商品 ID,不按维度分片 Set/ZSet 元素数超 100 万,操作阻塞

真实案例:某社交产品用 user:follow:10001(ZSet)存储单个大 V 的粉丝 ID,粉丝量破千万后,该 ZSet 元素数达 120 万,内存占用 80MB,执行 ZSCAN 单次耗时超 50ms,触发客户端超时。

二、核心原因 2:数据写入时的 "无节制"

业务运行中持续向单键写入数据,但未设置 "过期 / 清理机制",导致数据无限累积。

典型场景 具体行为 最终结果
日志 / 监控数据堆积 用单个 List/String 存储全量系统日志,只写不删 String 大小每天涨 10MB,一周后达 70MB
缓存全量榜单 用 ZSet 存储全站商品销量榜,每天新增数据但不清理历史数据 ZSet 元素数从 1 万涨到 50 万
未限流的接口缓存 把高并发接口的全量返回结果(如包含 1000 条商品的列表)直接缓存为单个 String String 大小达 500KB,高频读取时等同于 BigKey

真实案例:某电商平台缓存 "首页商品列表" 为单个 String(包含 200 个商品详情),大小 800KB,秒杀期间单秒读取 200 次,带宽占用超 1Gbps,引发网络阻塞。

三、核心原因 3:不合理的 Redis 命令使用

误用命令导致单键数据量异常增长,或把 "小键" 间接变成 "BigKey"。

典型行为 问题本质 最终结果
用 LPUSH 向 List 批量写入海量数据(一次 1 万条) 单批次写入量过大,List 瞬间成为 BigKey List 元素数骤增,后续 LRANGE 操作阻塞
用 HSET 循环向单个 Hash 写入字段(无分片) 字段数随循环次数线性增长 Hash 字段数突破阈值,HGETALL 超时
把多个小值拼接成一个 String 存储(如用逗号分隔 1000 个 ID) 人为把多个小数据合并为单键 String 大小超标,拆分 / 读取成本高

四、核心原因 4:缺乏监控和预警机制

未对 Redis 键的大小 / 元素数做监控,导致 BigKey 持续增长直到引发故障。

  • 无监控:不知道哪些键是 BigKey、大小增长趋势如何;
  • 无预警:未设置 "键大小≥50KB / 元素数≥5000" 的告警,错过治理时机;
  • 无治理流程:即使发现 BigKey,也无拆分 / 清理方案,任其恶化。

如何发现?

redis-cli --bigkeys

linux 复制代码
redis-cli --bigkeys  -i 0.1 -a 密码 -p 端口 

是 Redis 官方提供的BigKey 检测工具,核心作用是扫描 Redis 实例中所有键,统计不同类型键的「最大键 / 平均大小」,帮你快速定位 BigKey。

参数 是否必须 含义
--bigkeys 核心参数:启动 BigKey 扫描模式(非阻塞,基于 SCAN 遍历
-i 扫描限速:每执行 100 次 SCAN 指令,休眠 0.1 秒(生产必加!避免 SCAN 密集执行导致 ops 飙升

ops(Operations Per Second):Redis 每秒处理的命令数,是衡量 Redis 负载的核心指标;

假设 Redis 实例有 10 万个键,--bigkeys 每次 SCAN 扫 100 个哈希桶,总共需要执行 1000 次 SCAN指令:

  • 无 -i 0.1:1000 次 SCAN 连续执行,耗时 10 秒 → Redis 每秒要处理 100 次 SCAN 指令,ops 从日常 1000 飙升到 1100,若业务高峰期,可能导致业务命令(GET/SET)延迟增加;
  • 加 -i 0.1:每 100 次 SCAN 休眠 0.1 秒 → 1000 次 SCAN 会休眠 10 次(1000/100=10),总休眠 1 秒,扫描总耗时变成 11 秒 → SCAN 指令被 "匀速" 执行,ops 始终平稳,业务无感知。扫描速度变慢 ↔ Redis 性能(ops)更稳定。本质是 "慢扫换稳定"。

示例

redis 复制代码
# 执行命令(生产环境建议加 -i 0.1)
redis-cli --bigkeys -a 111111 -i 0.1

# 核心输出(关键部分)
# 1. 扫描进度提示
Scanning the entire keyspace to find biggest keys as well as
average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands to reduce server load.

# 2. 各类型 BigKey 结果
[00.00%] Biggest string key: "big_str" (size: 1048576 bytes)  # 最大String(1MB)
[00.00%] Biggest list key: "big_list" (elements: 15000)       # 最大List(1.5万元素)
[00.00%] Biggest hash key: "big_hash" (fields: 20000)         # 最大Hash(2万字段)
[00.00%] Biggest set key: "big_set" (members: 18000)          # 最大Set(1.8万元素)
[00.00%] Biggest zset key: "big_zset" (members: 25000)        # 最大ZSet(2.5万元素)

# 3. 整体统计(核心参考)
-------- summary -------
Sampled 10000 keys in the keyspace!
Total key length in bytes is 123456 (avg len 12.35)

Biggest string key: big_str (1048576 bytes)
Biggest   list key: big_list (15000 items)
Biggest    hash key: big_hash (20000 fields)
Biggest     set key: big_set (18000 members)
Biggest   zset key: big_zset (25000 members)

1) String keys: 5000 (50.00%)
   Average size: 1024 bytes (1.00 KB)  # String平均大小
2) List keys: 1000 (10.00%)
   Average size: 1500 items            # List平均元素数
3) Hash keys: 2000 (20.00%)
   Average size: 2000 fields           # Hash平均字段数
4) Set keys: 1500 (15.00%)
   Average size: 1800 members          # Set平均元素数
5) ZSet keys: 500 (5.00%)
   Average size: 2500 members          # ZSet平均元素数

局限性

  • 只统计 "最大键",不直接标记 "是否为 BigKey"(需结合业务阈值判断);
  • 遍历过程中若有键新增 / 删除,可能漏扫(适合定期巡检,不适合实时检测);

redis-cli --bigkeys -a 111111是快速定位 BigKey 的官方工具,核心输出「各类型最大键 + 平均大小」;

memory usage

MEMORY USAGE 是 Redis 用于精准查询单个键占用内存大小的核心命令,能帮你定位隐藏的 BigKey(比如看似小的键实际占用大量内存),弥补 --bigkeys 只统计 "最大键" 的不足。

linux 复制代码
MEMORY USAGE key [SAMPLES count]

返回值不仅是 "数据本身的大小",还包含 Redis 键的元数据(如类型、过期时间、哈希桶等),更贴近实际内存占用;

参数 必选 含义
key 要查询的 Redis 键名(如 user:101、big_str)
SAMPLES count 仅对 Hash/Set/ZSet 生效:抽样统计元素内存(默认 5 个样本),count 越大越精准(如 SAMPLES 100)。就是从指定key的集合中随机抽指定个数的元素(默认是5个)进行估算整个集合占多大内存。

查询 String 键(最精准)示例

String 类型无抽样,直接返回实际占用内存(字节):

redis 复制代码
# 查询 big_str 键的内存占用
127.0.0.1:6379> MEMORY USAGE big_str
(integer) 1048576  # 结果:1048576 字节 = 1MB

查询 Hash/Set/ZSet 键(支持抽样)示例

集合类型默认抽样统计,样本数越多结果越准(适合大集合):

redis 复制代码
# 查询 big_hash 键的内存占用(默认 5 个样本)
127.0.0.1:6379> MEMORY USAGE big_hash
(integer) 5242880  # 约 5MB

# 精准查询(100 个样本,耗时略增但结果更准)
127.0.0.1:6379> MEMORY USAGE big_hash SAMPLES 100
(integer) 5250000  # 精准值:约 5.25MB

查询 List 键示例

redis 复制代码
127.0.0.1:6379> MEMORY USAGE big_list
(integer) 3145728  # 约 3MB

对比两种方式

工具 优势 劣势
redis-cli --bigkeys 一键扫描全量键,速度快 仅统计 "最大键",无精准内存值
MEMORY USAGE 精准查询单个键内存 需遍历键,批量检测需写脚本

如何删除?

删除非字符串类型的 BigKey 时,要 "分批删" 而非 "一次性删";同时要避免让 BigKey 自动过期(过期会触发隐性的一次性删除,导致 Redis 阻塞),本质是防止删除操作占用 Redis 主线程,引发业务卡顿。

为什么要防止 BigKey 过期时间自动删除?
过期自动删除的 "隐性坑":当给 BigKey 设置过期时间(比如 EXPIRE zset_big 3600),到期后 Redis 会自动执行 DEL zset_big------ 这个 DEL 操作和手动执行一样,会一次性删除所有元素,导致阻塞。
更坑的是:

  • 这个自动 DEL 操作不会出现在 Redis 慢查询日志(慢查询只记录客户端发送的命令);
  • 只能通过 latency 命令(如 latency latest)查到突发的延迟峰值,排查难度大

如何避免BigKey过期自动删除

  • 方案 1:不给 BigKey 设过期时间,改用 "手动渐进式删除"(上面的 SCAN + 分批删);
  • 方案 2:若必须设过期,先把 BigKey 拆成多个小键(比如 200 万 ZSet 拆成 20 个 10 万元素的 ZSet),每个小键设过期,即使自动删除也只阻塞极短时间;
  • 方案 3 :(Redis 4.0+):开启 lazy-free(惰性释放)配置,让 BigKey 删除时的内存释放操作在后台线程执行,不阻塞主线程:

开启惰性释放

redis 复制代码
# 开启惰性删除(永久生效需改 redis.conf)
127.0.0.1:6379> CONFIG SET lazyfree-lazy-expire yes
127.0.0.1:6379> CONFIG SET lazyfree-lazy-del yes

永久生效修改配置文件配置

手动删除大Key
String 类型(特殊:无需分批,DEL 直接删)示例

String 类型即使是 BigKey(如 10MB),DEL 命令耗时也极短(毫秒级),无需渐进式删除,直接删即可:(实在害怕用unlink异步删除)

redis 复制代码
# String BigKey 直接删除(无阻塞风险)
127.0.0.1:6379> DEL big_string
(integer) 1  # 删除成功

List 类型(LPOP/RPOP 分批弹出删除)示例

List 是线性结构,用 LPOP/RPOP 批量弹出元素(每次 100 个),直到 List 为空:

redis 复制代码
# 循环执行以下命令,直到返回空(可写脚本/客户端循环)
127.0.0.1:6379> LPOP big_list 100  # 每次从头部弹出100个元素
# 返回:1) "elem1" 2) "elem2" ... (最多100个)
# 重复执行,直到返回 (nil),表示List已空

# 最后删除空键(可选,List为空后Redis会自动回收,DEL耗时极短)
127.0.0.1:6379> DEL big_list
(integer) 1

代码删除

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;

public class RedisBigKeyUtils {

    /**
     * 渐进式删除List类型BigKey(批量截断,最高效方式)
     * @param host Redis主机
     * @param port Redis端口
     * @param password Redis密码(无则传null)
     * @param bigListKey 要删除的List BigKey
     */
    public void delBigList(String host, int port, String password, String bigListKey) {
        Jedis jedis = null;
        try {
            // 1. 初始化Redis连接
            jedis = new Jedis(host, port);
            if (password != null && !password.trim().isEmpty()) {
                jedis.auth(password);
            }

            // 2. 每次截断100个元素(核心逻辑:ltrim保留 [left, end],设为100则删除前100个)
            int batchSize = 100;
            long remainingLen; // 实时剩余元素数
            while ((remainingLen = jedis.llen(bigListKey)) > 0) {
                // 若剩余元素不足100,取剩余数量作为截断点
                int left = Math.min(batchSize, (int) remainingLen);
                // ltrim(key, left, -1):保留第left个到最后一个元素(等价删除前left个元素)
                // 注:Redis List索引从0开始,ltrim(100, -1) 即删除前100个元素
                jedis.ltrim(bigListKey, left, -1); // -1 表示最后一个元素,避免依赖动态llen
            }

            // 3. 可选:删除空List键(元素删完后Redis自动回收,可省略)
            // Redis4.0+推荐用unlink(惰性删除)替代del,减少阻塞
            if (jedis.exists(bigListKey)) {
                // jedis.del(bigListKey); // 同步删除
                jedis.unlink(bigListKey); // 惰性删除(推荐)
            }

            System.out.println("List BigKey " + bigListKey + " 渐进式删除完成");

        } catch (JedisException e) {
            // 捕获Redis异常,保证程序健壮性
            System.err.println("删除List BigKey失败:" + e.getMessage());
            throw e; // 按需选择是否抛出,让上层感知
        } finally {
            // 4. 关闭连接,避免连接泄露
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    // 测试方法
    public static void main(String[] args) {
        RedisBigKeyUtils utils = new RedisBigKeyUtils();
        utils.delBigList("127.0.0.1", 6379, "your_password", "big_list_key");
    }
}

Set 类型(SSCAN + SREM 分批删除)

用 SSCAN 遍历部分元素,再用 SREM 批量删除:

redis 复制代码
# 步骤1:初始化游标为0
127.0.0.1:6379> SET cursor 0

# 步骤2:循环执行(核心逻辑)
# 2.1 用SSCAN遍历100个元素
127.0.0.1:6379> SSCAN big_set $(GET cursor) COUNT 100
1) "12345"  # 新游标,更新到cursor变量
2) 1) "elem1" 2) "elem2" ... (100个元素)

# 2.2 更新游标
127.0.0.1:6379> SET cursor 12345

# 2.3 批量删除遍历到的元素(元素列表作为SREM参数)
127.0.0.1:6379> SREM big_set elem1 elem2 ... (100个元素)
(integer) 100  # 删除成功的数量

# 步骤3:重复步骤2,直到SSCAN返回游标0且元素列表为空
# 步骤4:删除空键
127.0.0.1:6379> DEL big_set
(integer) 1

代码删除

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;

public class RedisBigKeyUtils {

    /**
     * 渐进式删除Set类型BigKey(分批扫描+批量删除元素)
     * @param host Redis主机
     * @param port Redis端口
     * @param password Redis密码(无则传null)
     * @param bigSetKey 要删除的Set BigKey
     */
    public void delBigSet(String host, int port, String password, String bigSetKey) {
        Jedis jedis = null;
        try {
            // 1. 初始化Redis连接
            jedis = new Jedis(host, port);
            if (password != null && !password.trim().isEmpty()) {
                jedis.auth(password);
            }

            // 2. 配置扫描参数:每次扫描100个元素
            ScanParams scanParams = new ScanParams().count(100);
            String cursor = "0";

            // 3. 循环扫描+批量删除元素
            do {
                ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
                List<String> memberList = scanResult.getResult();
                
                if (memberList != null && !memberList.isEmpty()) {
                    // 核心优化:批量删除元素(一次命令删多个,减少网络开销)
                    String[] members = memberList.toArray(new String[0]);
                    jedis.srem(bigSetKey, members);
                }

                // 更新游标(必须在循环末尾,避免游标错位)
                cursor = scanResult.getStringCursor();
            } while (!"0".equals(cursor)); // 游标为0时终止循环

            // 4. 可选:删除空Set键(元素删完后Redis自动回收,可省略)
            // Redis4.0+推荐用unlink(惰性删除)替代del,避免阻塞
            if (jedis.exists(bigSetKey)) {
                // jedis.del(bigSetKey); // 同步删除
                jedis.unlink(bigSetKey); // 惰性删除(推荐)
            }

            System.out.println("Set BigKey " + bigSetKey + " 渐进式删除完成");

        } catch (JedisException e) {
            // 捕获Redis异常,保证程序健壮性
            System.err.println("删除Set BigKey失败:" + e.getMessage());
            throw e; // 按需选择是否抛出,让上层感知
        } finally {
            // 5. 关闭连接,避免连接泄露
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    // 测试方法
    public static void main(String[] args) {
        RedisBigKeyUtils utils = new RedisBigKeyUtils();
        utils.delBigSet("127.0.0.1", 6379, "your_password", "big_set_key");
    }
}

ZSet 类型(ZSCAN + ZREM 分批删除)

用 ZSCAN 遍历元素(返回「元素 + 分值」),提取元素名后用 ZREM 批量删除:

redis 复制代码
# 步骤1:初始化游标为0
127.0.0.1:6379> SET cursor 0

# 步骤2:循环执行
# 2.1 用ZSCAN遍历100个元素(返回元素+分值成对列表)
127.0.0.1:6379> ZSCAN big_zset $(GET cursor) COUNT 100
1) "67890"  # 新游标
2) 1) "elem1" 2) "100" 3) "elem2" 4) "99" ... (100个元素+分值)

# 2.2 提取元素名(elem1、elem2...,忽略分值),更新游标
127.0.0.1:6379> SET cursor 67890

# 2.3 批量删除元素
127.0.0.1:6379> ZREM big_zset elem1 elem2 ... (100个元素)
(integer) 100

# 步骤3:重复步骤2,直到游标返回0且无元素
# 步骤4:删除空键
127.0.0.1:6379> DEL big_zset
(integer) 1

代码删除

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.Tuple;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.stream.Collectors;

public class RedisBigKeyUtils {

    /**
     * 渐进式删除ZSet类型BigKey(分批扫描+批量删除元素)
     * @param host Redis主机
     * @param port Redis端口
     * @param password Redis密码(无则传null)
     * @param bigZsetKey 要删除的ZSet BigKey
     */
    public void delBigZset(String host, int port, String password, String bigZsetKey) {
        Jedis jedis = null;
        try {
            // 1. 初始化Redis连接
            jedis = new Jedis(host, port);
            if (password != null && !password.trim().isEmpty()) {
                jedis.auth(password);
            }

            // 2. 配置扫描参数:每次扫描100个元素(含分值)
            ScanParams scanParams = new ScanParams().count(100);
            String cursor = "0";

            // 3. 循环扫描+批量删除元素
            do {
                // 修复:传scanParams实例而非类名
                ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
                List<Tuple> tupleList = scanResult.getResult();
                
                if (tupleList != null && !tupleList.isEmpty()) {
                    // 核心优化:提取所有元素名,批量删除(1次命令搞定)
                    String[] elements = tupleList.stream()
                            .map(Tuple::getElement) // 提取ZSet元素名(忽略分值)
                            .toArray(String[]::new);
                    jedis.zrem(bigZsetKey, elements);
                }

                // 修复:游标更新移到循环末尾,避免错位
                cursor = scanResult.getStringCursor();
            } while (!"0".equals(cursor)); // 修复:终止条件为数字0,非字母o

            // 4. 可选:删除空ZSet键(元素删完后Redis自动回收,可省略)
            // Redis4.0+推荐用unlink(惰性删除)替代del,减少阻塞
            if (jedis.exists(bigZsetKey)) {
                // jedis.del(bigZsetKey); // 同步删除
                jedis.unlink(bigZsetKey); // 惰性删除(推荐)
            }

            System.out.println("ZSet BigKey " + bigZsetKey + " 渐进式删除完成");

        } catch (JedisException e) {
            // 捕获Redis异常,保证程序健壮性
            System.err.println("删除ZSet BigKey失败:" + e.getMessage());
            throw e; // 按需选择是否抛出,让上层感知
        } finally {
            // 5. 关闭连接,避免连接泄露
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    // 测试方法
    public static void main(String[] args) {
        RedisBigKeyUtils utils = new RedisBigKeyUtils();
        utils.delBigZset("127.0.0.1", 6379, "your_password", "big_zset_key");
    }
}

Hash 类型(HSCAN + HDEL 分批删除)

用 HSCAN 遍历字段,再用 HDEL 批量删除字段:

redis 复制代码
# 步骤1:初始化游标为0
127.0.0.1:6379> SET cursor 0

# 步骤2:循环执行
# 2.1 用HSCAN遍历100个字段(返回字段+值成对列表)
127.0.0.1:6379> HSCAN big_hash $(GET cursor) COUNT 100
1) "78901"  # 新游标
2) 1) "field1" 2) "val1" 3) "field2" 4) "val2" ... (100个字段+值)

# 2.2 提取字段名(field1、field2...,忽略值),更新游标
127.0.0.1:6379> SET cursor 78901

# 2.3 批量删除字段
127.0.0.1:6379> HDEL big_hash field1 field2 ... (100个字段)
(integer) 100

# 步骤3:重复步骤2,直到游标返回0且无字段
# 步骤4:删除空键
127.0.0.1:6379> DEL big_hash
(integer) 1

代码删除

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.exceptions.JedisException;

import java.util.*;
import java.util.Map.Entry;

public class RedisBigKeyUtils {

    /**
     * 渐进式删除Hash类型BigKey(分批扫描+批量删除字段)
     * @param host Redis主机
     * @param port Redis端口
     * @param password Redis密码(无则传null)
     * @param bigHashKey 要删除的Hash BigKey
     */
    public void delBigHash(String host, int port, String password, String bigHashKey) {
        Jedis jedis = null;
        try {
            // 1. 初始化Redis连接
            jedis = new Jedis(host, port);
            if (password != null && !password.trim().isEmpty()) {
                jedis.auth(password);
            }

            // 2. 配置扫描参数:每次扫描100个字段
            ScanParams scanParams = new ScanParams().count(100);
            String cursor = "0";

            // 3. 循环扫描+批量删除字段
            do {
                ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
                // 获取本次扫描的字段列表
                List<Entry<String, String>> entryList = scanResult.getResult();
                
                if (entryList != null && !entryList.isEmpty()) {
                    // 提取所有字段名,批量删除(核心优化:一次命令删多个字段)
                    String[] fields = entryList.stream()
                            .map(Entry::getKey)
                            .toArray(String[]::new);
                    jedis.hdel(bigHashKey, fields);
                }

                // 更新游标(必须在循环末尾,否则会无限循环)
                cursor = scanResult.getStringCursor();
            } while (!"0".equals(cursor)); // 游标为0时终止(数字0,非字母o)

            // 4. 可选:删除空Hash键(字段删完后,Redis会自动回收,可省略)
            // 若开启惰性释放,建议用unlink替代del
            if (jedis.exists(bigHashKey)) {
                // jedis.del(bigHashKey); // 同步删除
                jedis.unlink(bigHashKey); // 惰性删除(Redis4.0+推荐)
            }

            System.out.println("Hash BigKey " + bigHashKey + " 渐进式删除完成");

        } catch (JedisException e) {
            // 捕获Redis异常,避免程序崩溃
            System.err.println("删除Hash BigKey失败:" + e.getMessage());
            throw e; // 按需选择是否抛出,让上层感知
        } finally {
            // 5. 关闭连接,避免连接泄露
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    // 测试方法
    public static void main(String[] args) {
        RedisBigKeyUtils utils = new RedisBigKeyUtils();
        utils.delBigHash("127.0.0.1", 6379, "your_password", "big_hash_key");
    }
}
相关推荐
~莫子2 小时前
Redis
数据库·redis·缓存
历程里程碑2 小时前
36 Linux线程池实战:日志与策略模式解析
开发语言·数据结构·数据库·c++·算法·leetcode·哈希算法
颜颜yan_2 小时前
从千毫秒到亚毫秒:连接条件下推如何让复杂 SQL 飞起来
数据库·sql
ChaITSimpleLove2 小时前
如何查看系统中 PostgreSQL 数据库的进程(postgres)运行状态?
数据库·postgresql·查看pgsql运行状态·pgsql进程运行状态·postgres 进程·tree 树形结构
ChaITSimpleLove2 小时前
PostgreSQL 部署与运维常用命令详解
运维·数据库·postgresql·部署·命令解析
ChaITSimpleLove2 小时前
PostgreSQL 的 SQL 执行过程详解
数据库·sql·postgresql·词法分析·语法分析·执行过程
小鸡脚来咯2 小时前
SQL表连接
java·开发语言·数据库
Henray20242 小时前
SQL 窗口函数
大数据·数据库·sql
顶点多余2 小时前
Mysql--索引的操作
数据库·mysql