大家好,我是小义。今天来聊聊面试中的高频考点:如何处理redis缓存中的大key? 大 key 其实并不是指 key 的值很大,而是 key 对应的 value 很大,占了很大内存。
了解大Key的成因是解决问题的第一步。大Key的形成可能源于多种因素,包括但不限于:
那具体多大才算大key呢?参考标准大致如下:
大key会带来以下四种危害:
这里介绍一个好用的查找大key的第三方工具,用python语言编写的redis-rdb-tools,可以用来解析 Redis 快照(RDB)文件。要使用该工具得先下载python,具体安装过程可以参考网上的教程,下面介绍几个常用命令:
rdb -c memory /mnt/data/redis/dump.rdb > /mnt/data/redis/memory.csv
rdb --command memory --largest 3 dump.rdb
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
针对大key,肯定是要删除的,那怎么删除才最高效呢?直接用del命令行不行?答案是不行。Redis 官方文档描述到:
1、String 类型的key,DEL 时间复杂度是 O(1),大key除外。
2、List/Hash/Set/ZSet 类型的key,DEL 时间复杂度是 O(M),M 为元素数量,元素越多,耗时越久。
大Key如果一次性执行删除操作,会立即触发大量内存的释放过程。这个过程中,操作系统需要将释放的内存块重新插入空闲内存块链表,以便之后的管理和再分配。由于这个过程是同步进行的,并且可能涉及大量的内存块操作,因此它将占用相当一部分处理时间,并可能造成Redis主线程的阻塞。
这种阻塞会导致Redis无法及时响应其他命令请求,从而引起请求超时,超时的累积可能会导致Redis连接耗尽,进而产生服务异常。
因此删除大key,一定要慎之又慎,可以选择异步删除或批量删除。
Redis从 4.0开始, 可以使用 UNLINK 命令来异步删除大key,删除大Key的语法与DEL命令相同。
UNLINK bigkey
当使用UNLINK删除一个大Key时,Redis不会立即释放关联的内存空间,而是将删除操作放入后台处理队列中。Redis会在处理命令的间隙,逐步执行后台队列中的删除操作,从而不会显著影响服务器的响应性能。
主要是针对Hash、List、Set、Zset,具体操作见下方代码描述
@Component
@Slf4j
public class RedisUtils {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* Hash删除: hscan + hdel
* @param key 大key
* @param match 要匹配的hash的key,支持正则表达式
* @param count 每次扫描的记录数。值越小,扫描次数越过、越耗时。建议设置在1000-10000
*/
public void delBigHash(String key, String match, int count) {
ScanOptions scanOptions = ScanOptions.scanOptions().match(match).count(count).build();
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(key, scanOptions);
while (cursor.hasNext()) {
Map.Entry<Object, Object> next = cursor.next();
redisTemplate.opsForHash().delete(key, next.getKey());
log.info("del:"+ next.getKey());
}
try {
//遍历完成后,游标需要关闭
cursor.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* List删除: trim + del
* @param key
* @param num 每次删除的个数
*/
public void delBigList(String key, int num) {
Long size = redisTemplate.opsForList().size(key);
int counter = 0;
while (counter < size) {
//每次从左侧截掉 num 个
redisTemplate.opsForList().trim(key, 0, num);
counter += num;
log.info("count="+counter);
}
//最终删除key
redisTemplate.delete(key);
}
/**
* Set删除: sscan + srem
*/
public void delBigSet(String key, int count) {
ScanOptions scanOptions = ScanOptions.scanOptions().count(count).build();
Cursor<String> cursor = redisTemplate.opsForSet().scan(key, scanOptions);
while (cursor.hasNext()) {
String value = cursor.next();
redisTemplate.opsForSet().remove(key, value);
log.info("set del:"+ value);
}
try {
//遍历完成后,游标需要关闭
cursor.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* ZSet删除: zscan + zrem
*/
public void delBigZSet(String key, int count) {
ScanOptions scanOptions = ScanOptions.scanOptions().count(count).build();
Cursor<ZSetOperations.TypedTuple<String>> cursor = redisTemplate.opsForZSet().scan(key, scanOptions);
while (cursor.hasNext()) {
ZSetOperations.TypedTuple<String> next = cursor.next();
redisTemplate.opsForZSet().remove(key, next.getValue());
log.info("zset del -> value:"+ next.getValue() + ", score:"+ next.getScore());
}
try {
//遍历完成后,游标需要关闭
cursor.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
在Redis的世界里,大Key问题就像是一颗隐藏的炸弹,随时可能引发性能危机,但通过合理的策略和持续的优化,就可以有效地控制其对系统性能的影响。