Redis——BigKey
BigKey
1 多大算 BigKey?
阿里云 Redis 开发规范:
string
类型的数据控制在 10KB 以内,hash, list, set, zset
元素数量不要超过 5000。- 非字符串的 BigKey,不要使用
del
删除,而是使用hsacn, sscan, zscan
方式 渐进式删除。同时,要防止 BigKey 过期时自动删除,因为自动删除会使用del
指令。
2. BigKey 有什么危害?
- 如果没有配置 Redis 非阻塞删除,则在过期自动删除 BigKey 时会导致 Redis 主进程阻塞。
- 如果在分片集群中,则会造成某个节点被频繁访问,单点流量过高的问题。
- 如果有主从复制,则可能延长主从不一致的时间。
- 在查询 BigKey 时,可能导致网络设备带宽耗尽,从而超时。
3. BigKey 是如何产生的?
BigKey 一般不是突然就产生的,而是 逐步积累 的。比如粉丝列表、统计报表。
如果使用 Redis 不当,也可能会造成 BigKey,例如:
- 把大文件直接存入 Redis。
- 把对象的键作为 hash 数据结构内部的键,从而往一个 hash 数据结构中存储大量对象。
4. 如何发现 BigKey?
redis-cli --bigkeys
:在 shell 中执行这个命令后,可以分析出最大的数据。memory useage xxx
:在 redis-cli 里执行这个命令后,可以获取xxx
所占的字节数。
5. 如何删除 BigKey?
5.1 string 类型
一般用 del
,保险起见,可以使用 unlink
。
5.2 hash 类型
使用 hscan + hdel
,每次删除 100 个键值对,使用 redisTemplate
来操作是这样的:
final int BATCH_SIZE = 100;
ScanOptions options = ScanOptions.scanOptions().count(BATCH_SIZE).build();
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(KEY, options);
while (cursor.hasNext()) {Object[] keys = new Object[BATCH_SIZE];int elementCount = 0;while (cursor.hasNext() && elementCount < BATCH_SIZE) {Map.Entry<Object, Object> entry = cursor.next();keys[elementCount++] = entry.getKey();}redisTemplate.opsForHash().delete(KEY, keys);
}
cursor.close();
redisTemplate.delete(KEY);
5.3 list 类型
使用 ltrim
,每次删除 100 个元素,使用 redisTemplate
来操作是这样的:
final int BATCH_SIZE = 100;
long size = Optional.ofNullable(redisTemplate.opsForList().size(KEY)).orElse(0L);
long end = size - 1;
while (end > 0) {end -= BATCH_SIZE;redisTemplate.opsForList().trim(KEY, 0, end);
}
redisTemplate.delete(KEY);
注:这些操作不是原子的,按理应该写一个 lua 脚本来保证操作的原子性,但是 lua 脚本在 Redis 中执行时会阻塞其他操作,还不如直接使用 del
指令。一般情况下,删除这个 list 时需要确保没有人访问它,所以不是原子的也可以。
5.4 set 类型
使用 sscan + srem
,每次删除 100 个元素,使用 redisTemplate
来操作是这样的:
final int BATCH_SIZE = 100;
ScanOptions scanOptions = ScanOptions.scanOptions().count(BATCH_SIZE).build();
Cursor<Object> cursor = redisTemplate.opsForSet().scan(KEY, scanOptions);
while (cursor.hasNext()) {Object[] keys = new Object[BATCH_SIZE];int elementCount = 0;while (cursor.hasNext() && elementCount < BATCH_SIZE) {keys[elementCount++] = cursor.next();}redisTemplate.opsForSet().remove(KEY, keys);
}
cursor.close();
redisTemplate.delete(KEY);
5.5 zset 类型
使用 zscan + zrem
,每次删除 100 个元素,使用 redisTemplate
来操作是这样的:
final int BATCH_SIZE = 100;
ScanOptions scanOptions = ScanOptions.scanOptions().count(BATCH_SIZE).build();
Cursor<TypedTuple<Object>> cursor = redisTemplate.opsForZSet().scan(KEY, scanOptions);
while (cursor.hasNext()) {Object[] keys = new Object[BATCH_SIZE];int elementCount = 0;while (cursor.hasNext() && elementCount < BATCH_SIZE) {TypedTuple<Object> typedTuple = cursor.next();keys[elementCount++] = typedTuple.getValue();}redisTemplate.opsForZSet().remove(KEY, keys);
}
cursor.close();
redisTemplate.delete(KEY);
6. BigKey 优化
Redis 默认的过期删除策略用的删除指令是 del
,这个指令是 阻塞 的,所以在删除 BigKey 时会造成 Redis 卡顿。此外,Redis 还提供了一个指令 unlink
,这个指令是 非阻塞 的,可以通过配置来让 Redis 使用 unlink
删除过期的数据,配置如下:
# 当 Redis 服务器主动删除对象(如 过期键、内存淘汰)时,是否使用异步线程执行实际内存释放
lazyfree-lazy-server-del yes
# 当执行 FLUSHDB 或 FLUSHALL 命令清空数据库时,是否使用异步线程执行实际内存释放
lazyfree-lazy-flush yes
# 当用户通过 DEL 命令主动删除键时,是否使用异步线程执行实际内存释放
lazyfree-lazy-user-del yes
7. 总结
本文对 BigKey 的定义做了诠释,介绍了 BigKey 的危害,提到了 BigKey 的产生原因,主要讲解了如何发现和删除 BigKey,除此之外,还介绍了可以通过配置文件让 Redis 在删除数据时使用非阻塞的方式。