我们知道Redis是单工作线程的组件,这意味着在Redis中许多操作都是阻塞的(前面一个命令不执行完,后一个命令只能等待),
在此基础上,如果我们不谨慎操作大key,可能会导致Redis运行缓慢甚至宕机。因此此篇文章将java讲述如何正确地处理大key。
正式开始之前
了解一下要用到的命令
在Redis中,有一个命令系列:SCAN
,本文中将会用到,你可以简单理解为linux中的more
(翻页查看)命令,但也有所不同。对于SCAN
命令的详解你可以查阅官方文档。
本文章基于以下版本
jdk17(jdk8也适用)
spring-boot-starter-data-redis:3.3.2(经测试,2.6.10也同样适用,其他版本自测)
Redis6
RedisConfig配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建redis template对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置key的序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置value的序列化
redisTemplate.setValueSerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(RedisSerializer.string());
return redisTemplate;
}
}
什么是大key?
首先我们来了解一下什么是大Key
虽然官方表示Redis中每种数据类型的最大容量如下:
string:单个字符串的最大限制是512MB。
list:列表可以存储大约40亿个元素(2^32-1)。
set:集合同样可以存储40亿个元素
hash:哈希表可以存储40亿个字段和值。
zset:与集合类似,有序集合也可以存储40亿个元素。
但是实际开发中,如果一个key的大小达到了512MB,那就太恐怖了。
因此,官方建议string类型的key控制在10KB以内,list,set,hash,zset的元素个数控制在5000个以内。而超过这个界限的key,我们都称之为大key。
如何操作
对于string类型
判断大key
我们可以使用MEMORY USAGE
命令,例如:
127.0.0.1:6379> MEMORY USAGE k1
(integer) 56
可以得到看k1这个key所占的大小是56byte
,如果返回的结果大于等于10KB(10240byte
),那它就是一个大key
如何删除?
对于大key,不建议使用的DEL
命令,使用UNLINK
进行异步删除会是一个比较好的选择。
异步删除指的是:Redis先将这个key的隐藏起来,让客户端访问不到,然后自己在后台找个时间把他删除。
redisTemplate.unlink("key");
对于list类型
判断大key
在Redis中使用LLEN
命令,或者在java中使用
Long size = redisTemplate.opsForList().size("list");
如果得到的值大于等于5000,则这个key是大key
如何删除?
我们知道,在Redis对于list的操作中,有一个命令为LTRIM
,它用于切割list,我们可以利用这个命令,将list每次切割一部分,最后完全删除。
在java中,我们这样使用它
参数说明:
key:要删除的key
groupMaxSize:每次要删除的元素个数
public void del(String key, int groupMaxSize) {
if (!StringUtils.hasText(key)) {
// key为空字符串直接结束
return;
}
ListOperations<String, String> opsForList = redisTemplate.opsForList();
// 获取长度
Long size = opsForList.size(key);
if (Objects.isNull(size)||size==0) {
// key不存在,直接删除
return;
}
while (size > (long) groupMaxSize) {
// 通过trim进行切割,每次删除前groupMaxSize个元素
opsForList.trim(key, groupMaxSize, -1);
size -= (long) groupMaxSize;
}
redisTemplate.delete(key);
}
对于set类型
判断大key
在Redis中使用SCARD
命令,或者在java中使用
Long size = redisTemplate.opsForSet().size("set");
如果得到的值大于等于5000,则这个key是大key
如何删除?
我们可以采用SSCAN
命令来分批获取和删除
在java中,我们这样使用它
参数说明:
key:要删除的key
groupMaxSize:每次要删除的元素个数
public void delByScan(String key, int groupMaxSize) {
// 检查键是否为空,如果为空则直接返回,不执行删除操作。
if (!StringUtils.hasText(key)) {
return;
}
// 设置扫描选项,指定每次扫描的元素数量。
ScanOptions scanOptions = ScanOptions.scanOptions().count(groupMaxSize).build();
// 用于存储扫描到的键的列表。
List<Object> keyList = new ArrayList<>();
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
// 使用scan扫描指定的键。
try (Cursor<String> scan = setOperations.scan(key, scanOptions)) {
// 遍历迭代器,将扫描到的键添加到keyList中。
while (scan.hasNext()) {
Object next = scan.next();
keyList.add(next);
// 当list的size等于预设的最大值时,批量删除这些key,并清除list
if (keyList.size() == groupMaxSize) {
setOperations.remove(key, keyList.toArray());
keyList.clear();
}
}
}
// 最后,直接删除原始键。
redisTemplate.delete(key);
}
对于hash类型
判断大key
在Redis中使用HLEN
命令,或者在java中使用
Long size = redisTemplate.opsForHash().size("hash");
如果得到的值大于等于5000,则这个key是大key
如何删除?
跟set一样,Redis一样提供了HSCAN
来让我们分批获取hash表中的元素。
在java中,我们这样使用它
参数说明:
key:要删除的key
groupMaxSize:每次要删除的元素个数
public void delByScan(String key, int groupMaxSize) {
// 检查键是否为空,如果为空则直接返回,不执行删除操作。
if (!StringUtils.hasText(key)) {
return;
}
// 设置扫描选项,指定每次扫描的元素数量。
ScanOptions scanOptions = ScanOptions.scanOptions().count(groupMaxSize).build();
// 用于存储扫描到的键的列表。
List<Object> keyList = new ArrayList<>();
HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
try (Cursor<Map.Entry<Object, Object>> scan = hashOperations.scan(key, scanOptions)) {
// 遍历迭代器,将扫描到的键添加到keyList中。
while (scan.hasNext()) {
Map.Entry<Object, Object> next = scan.next();
keyList.add(next.getKey());
// 当list的size等于预设的最大值时,批量删除这些key,并清除list
if (keyList.size() == groupMaxSize) {
hashOperations.delete(key, keyList.toArray());
keyList.clear();
}
}
// 最后,直接删除原始键。
redisTemplate.delete(key);
}
}
对于zset类型
判断大key
在Redis中使用ZCARD
命令,或者在java中使用
Long size = redisTemplate.opsForZSet().size("zset");
如果得到的值大于等于5000,则这个key是大key
如何删除?
使用ZSCAN
命令来分批获取和删除
在java中,我们这样使用它
参数说明:
key:要删除的key
groupMaxSize:每次要删除的元素个数
public void delByScan(String key, int groupMaxSize) {
// 检查键是否为空,如果为空则直接返回,不执行删除操作。
if (!StringUtils.hasText(key)) {
return;
}
// 设置扫描选项,指定每次扫描的元素数量。
ScanOptions scanOptions = ScanOptions.scanOptions().count(groupMaxSize).build();
// 用于存储扫描到的键的列表。
List<Object> keyList = new ArrayList<>();
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
try (Cursor<ZSetOperations.TypedTuple<String>> scan = zSetOperations.scan(key, scanOptions)) {
// 遍历扫描结果,将每个键添加到keyList中,并检查是否达到groupMaxSize的大小。
while (scan.hasNext()) {
ZSetOperations.TypedTuple<String> next = scan.next();
// 这里的value相当于唯一的标识
Object value = next.getValue();
keyList.add(value);
if (keyList.size() == groupMaxSize) {
zSetOperations.remove(key, keyList.toArray());
keyList.clear();
}
}
}
// 最后,直接删除原始键。
redisTemplate.delete(key);
}
最后
我将代码都整合在了基于Springboot3和java17的简单框架|hsxazj的博客 (xiake-half.com)的Redis工具类中,非常欢迎你提出建议,一起学习。