记得刚接触 Redis 那会儿,我最喜欢用的就是 KEYS 命令。简单粗暴,一个 KEYS * 就能列出所有键,简直不要太方便。直到有一次在生产环境执行了这个命令,整个 Redis 实例直接卡死,我才意识到问题的严重性。
KEYS 命令的工作原理就像是在图书馆里找书,但不是通过目录检索,而是把书架上的书一本本拿下来检查。当你的 Redis 实例中有上百万个 key 时,这个操作会阻塞整个 Redis 服务,导致其他所有请求都被迫等待。我亲身经历过因为一个 KEYS 命令导致线上服务大面积超时的惨痛教训。
SCAN 命令的出现完美解决了这个问题。它就像是一个聪明的图书管理员,每次只检查一小部分书架,记下进度后就把控制权交还给 Redis。这样其他命令就能穿插执行,不会造成服务卡顿。SCAN 的基本用法是这样的:
bash复制SCAN 0 MATCH user:* COUNT 100
这个命令会从游标 0 开始,查找所有以 "user:" 开头的 key,每次大约扫描 100 个槽位。返回结果包含两部分:新的游标值和匹配的 key 列表。当游标再次返回 0 时,表示遍历完成。
SCAN 命令之所以不会阻塞 Redis,是因为它采用了分批次扫描的策略。Redis 内部的所有 key 都存储在一个哈希表中,SCAN 通过游标在这个哈希表中分步前进。每次调用 SCAN 时,它只会处理有限数量的哈希桶(hash bucket),然后就把执行权交还给 Redis。
这里有个很重要的细节:COUNT 参数并不是精确控制返回结果数量的,它只是建议 SCAN 每次扫描多少个哈希桶。我做过测试,设置 COUNT 100 时,返回结果可能在 0 到几百个 key 之间波动。这是因为:
SCAN 命令有个特点可能会让新手困惑:它可能会返回重复的 key。这是因为在遍历过程中,如果发生了哈希表的扩容或缩容(rehashing),就可能导致某些 key 被重复扫描。我建议在客户端对结果进行去重处理。
另一个需要注意的问题是,在 SCAN 过程中如果有 key 被修改或删除,这些变更可能反映在结果中,也可能不反映。这就好比你在清点库存时,有人正在往货架上补货 - 你无法保证看到的是完全一致的快照。
假设我们要找出所有以 "session:" 开头的 key,可以这样操作:
python复制import redis
r = redis.Redis(host='localhost', port=6379)
cursor = 0
all_keys = []
while True:
cursor, keys = r.scan(cursor, match='session:*', count=500)
all_keys.extend(keys)
if cursor == 0:
break
print(f"Found {len(all_keys)} session keys")
这个脚本会分批扫描所有 key,直到游标返回 0。我在生产环境经常用类似的脚本来统计特定模式的 key 数量,完全不会影响线上服务。
大 Key 就像是 Redis 里的"巨无霸",它们会带来一系列问题:
我曾经遇到过一个实际案例:一个 Hash 类型的 Key 存储了百万级字段,每次读取都会导致 Redis 短暂卡顿。后来我们用 SCAN 配合 HSCAN 逐步优化,才解决了这个问题。
Redis 官方提供了一个非常方便的工具来检测大 Key:
bash复制redis-cli --bigkeys
这个命令会自动扫描整个 Redis 数据库,统计各种数据类型中最大的 Key。输出结果类似这样:
code复制# 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 (not usually needed).
[00.00%] Biggest string found so far 'user:1001:profile' with 1024 bytes
[12.34%] Biggest hash found so far 'product:1234' with 15 fields
...
如果担心影响线上性能,可以加上 -i 参数:
bash复制redis-cli --bigkeys -i 0.1
这样每扫描 100 个 key 就会暂停 0.1 秒,大大降低对生产环境的影响。
对于更复杂的需求,我们可以自己编写扫描脚本。比如要找出所有大小超过 1MB 的 Key:
python复制import redis
from itertools import count
r = redis.Redis(host='localhost', port=6379)
def get_key_size(key):
key_type = r.type(key)
if key_type == b'string':
return r.memory_usage(key)
elif key_type == b'hash':
return r.hlen(key)
elif key_type == b'list':
return r.llen(key)
elif key_type == b'set':
return r.scard(key)
elif key_type == b'zset':
return r.zcard(key)
return 0
cursor = 0
big_keys = []
for _ in count():
cursor, keys = r.scan(cursor, count=500)
for key in keys:
size = get_key_size(key)
if size > 1048576: # 1MB
big_keys.append((key, size))
if cursor == 0:
break
for key, size in sorted(big_keys, key=lambda x: x[1], reverse=True):
print(f"{key.decode()}: {size}")
这个脚本会找出所有大于 1MB 的 Key,并按大小排序输出。在实际项目中,我们可以根据需求调整阈值和输出格式。
即使 SCAN 命令已经很温和,但在极端情况下仍可能对生产环境造成影响。我建议:
找到大 Key 后,我们需要制定优化方案。常见的处理方式包括:
为了预防大 Key 问题再次发生,建议建立长期监控机制:
我曾经为一个电商项目设计过这样的监控方案,通过定时任务每天凌晨扫描 Redis 大 Key,结果自动发送到监控平台。当发现异常大 Key 时,会立即通知相关团队处理,有效预防了多次潜在的性能问题。