优化 BigKey 的查询与删除:Redis 大 Key 治理实战
什么是 BigKey
BigKey(大键)是指单个 key 对应的值占用大量内存或包含大量元素。BigKey 是 Redis 性能问题的常见元凶。
BigKey 的判断标准
| 类型 | BigKey 判断标准 | 说明 |
|---|---|---|
| String | value > 10KB | 大字符串 |
| Hash | field > 10000 | 哈希字段过多 |
| List | 元素 > 10000 | 列表过长 |
| Set | 成员 > 10000 | 集合成员过多 |
| Sorted Set | 成员 > 10000 | 有序集合元素过多 |
| Stream | 条目 > 10000 | 消息流过长 |
注意:这个阈值不是绝对的,需要根据业务场景判断。对于某些应用,100 万的列表元素也是”大 key”。
BigKey 的危害
1. 阻塞 Redis
# 获取 100 万成员的 Set
SMEMBERS huge_set # O(N) 操作,可能阻塞数秒
HGETALL huge_hash # 返回 100 万个字段,占用大量内存
2. 网络拥塞
# 一次性获取 10MB 的大字符串
data = r.get('huge_string') # 10MB 数据传输,占用带宽
3. 删除阻塞
# 删除大集合
r.delete('huge_set') # O(N) 删除,阻塞直到释放完所有内存
4. 主从延迟
BigKey 在主从复制中传输耗时较长,可能导致从库同步延迟。
如何发现 BigKey
使用 redis-cli 的 –bigkeys 扫描
redis-cli --bigkeys
# 输出示例
-------- summary -------
Sampled 100000 keys in the keyspace!
Total key length in bytes is 2450675 (avg len 24.51)
Biggest string found 'bighash:5' has 52425 bytes
Biggest list found 'biglist' has 150000 items
Biggest set found 'bigset' has 20000 members
Biggest hash found 'bighash:1' has 32000 fields
Biggest zset found 'bigzset' has 25000 members
13 strings with 100000 bytes (00.01% of keys, avg size 7692.23)
1 lists with 150000 items (00.00% of keys, avg size 150000.00)
使用 MEMORY USAGE 命令
# 查看单个 key 的内存占用
MEMORY USAGE user:1001
# (integer) 2560 -- 占用 2560 字节
MEMORY USAGE huge_set
# (integer) 12582912 -- 占用 12MB
使用 SCAN + MEMORY USAGE 扫描
def find_big_keys(redis, threshold_bytes=1024*1024):
"""找出超过阈值的大 key"""
big_keys = []
cursor = 0
while True:
cursor, keys = redis.scan(cursor, count=1000)
for key in keys:
try:
size = redis.memory_usage(key)
if size and size > threshold_bytes:
big_keys.append((key, size))
except:
pass
if cursor == 0:
break
return sorted(big_keys, key=lambda x: x[1], reverse=True)
使用 Redis 慢查询日志
SLOWLOG GET 10
# 如果看到 SMEMBERS、HGETALL、LRANGE 等命令且耗时较长,大概率是 BigKey
删除 BigKey 的正确姿势
方案一:UNLINK(推荐)
# 异步删除,Redis 在后台回收内存
UNLINK huge_set huge_hash # 立即返回,后台删除
UNLINK 与 DEL 的区别:
# DEL:同步删除,阻塞 Redis
r.delete('big_list') # 可能阻塞几秒
# UNLINK:异步删除,快速返回
r.unlink('big_list') # 立即返回,后台线程回收内存
方案二:分批删除
对于大 List、Hash、Set、Sorted Set:
# 分批删除大 List
def trim_big_list(redis, key, batch_size=100):
while redis.llen(key) > 0:
redis.ltrim(key, batch_size, -1) # 保留最后 batch_size 个
time.sleep(0.01) # 给其他操作留出时间
# 分批删除大 Hash
def delete_big_hash(redis, key, batch_size=100):
while True:
fields = redis.hkeys(key, batch_size)
if not fields:
break
redis.hdel(key, *fields)
# 分批删除大 Set
def delete_big_set(redis, key, batch_size=1000):
while redis.scard(key) > 0:
members = redis.spop(key, batch_size) # 弹出 batch_size 个成员
方案三:过期 + 异步删除
# 设置过期时间,到期后异步删除
EXPIRE huge_key 3600 # 1 小时后过期
预防 BigKey
1. 数据分片
# 将大 Hash 拆分成多个小 Hash
# ❌ 一个 Hash 存全部
r.hset('user:1:friends', mapping={f"u{uid}": uid for uid in range(100000)})
# ✅ 分片存储
SHARD_SIZE = 1000
def get_friends_key(uid, page):
return f"user:{uid}:friends:{page // SHARD_SIZE}"
for i in range(0, 100000, SHARD_SIZE):
shard = {f"u{j}": j for j in range(i, min(i+SHARD_SIZE, 100000))}
r.hset(f"user:1:friends:{i//SHARD_SIZE}", mapping=shard)
2. 限制数据结构大小
# 用 LTRIM 限制 List 长度
redis.lpush('recent_visitors', user_id)
redis.ltrim('recent_visitors', 0, 100) # 只保留最近 100 条
3. 定时扫描告警
def check_big_keys(redis, alert_threshold=10*1024*1024):
"""定时扫描 BigKey 并告警"""
cursor = 0
while True:
cursor, keys = redis.scan(cursor, count=1000)
for key in keys:
size = redis.memory_usage(key) or 0
if size > alert_threshold:
alert(f"发现 BigKey: {key} 占用 {size/1024/1024:.2f}MB")
if cursor == 0:
break
4. 优化 BigKey 查询
# ✅ 分批查询替代全量查询
def get_hash_fields(redis, key, field_pattern="*", batch_size=100):
cursor = 0
result = {}
while True:
cursor, fields = redis.hscan(key, cursor, match=field_pattern, count=batch_size)
result.update(fields)
if cursor == 0:
break
return result
面试要点
- BigKey 的三大危害:阻塞 Redis、网络拥塞、删除延迟
- 发现 BigKey 的工具:redis-cli –bigkeys、MEMORY USAGE、SCAN
- 删除 BigKey 首选 UNLINK(异步删除)
- 预防比治理更重要:数据分片、结构限制、定时扫描
- Lazy Freeing(惰性释放)是处理大 key 删除的重要机制
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容