项目场景:
提示:这里简述项目相关背景:
例如:项目场景:查平台的支付方式
问题描述
用来redis的scan模糊匹配的方式,查询key,在redis的key大于百万级的key的时候,会导致查询非常缓慢
package com.hznt.yeahgo.cms.portal.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* @author wangwenping
* @description redis helper
* @date 2021/5/14
*/
@Slf4j
public class RedisHelper {
/**
* scan 实现
*
* @param redisTemplate
* @param pattern 表达式
* @param consumer 对迭代到的key进行操作
*/
public static void scan(RedisTemplate redisTemplate, String pattern, Consumer<byte[]> consumer) {
//pattern="*"+pattern+"*";
//String finalPattern = pattern;
redisTemplate.execute((RedisConnection connection) -> {
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(1000000).match(pattern).build())) {
cursor.forEachRemaining(consumer);
return null;
} catch (IOException e) {
log.error("redis scan 发生异常 {}", e);
throw new RuntimeException(e);
}
});
}
/**
* 获取符合条件的key
*
* @param pattern 表达式
* @return
*/
public static List<String> keys(RedisTemplate redisTemplate, String pattern) {
List<String> keys = new ArrayList<>();
scan(redisTemplate, pattern, item -> {
//符合条件的key
String key = new String(item, StandardCharsets.UTF_8);
keys.add(key);
});
return keys;
}
}
`` String resourceKey = cmsResourceByResourceKeyRequest.getResourceKey();
String patternAll = CacheKey.getResourceKey(resourceKey, "*" + cmsResourceByResourceKeyRequest.getPlatform() + "*", "0.0.0");
List<String> keys = RedisHelper.keys(redisTemplate, patternAll);
# 原因分析:
方法1:使用KEYS [pattern]:查找所有符合给定模式pattern的key
使用keys [pattern]指令可以找到所有符合pattern条件的key,但是keys会一次性返回所有符合条件的key,所以会造成redis的卡顿,假设redis此时正在生产环境下,使用该命令就会造成隐患,另外如果一次性返回所有key,对内存的消耗在某些条件下也是巨大的。
例:
keys test* //返回所有以test为前缀的key
方法2:使用SCAN cursor [MATCH pattern] [COUNT count]
cursor:游标
MATCH pattern:查询key的条件
count:返回的条数
SCAN是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。SCAN以0作为游标,开始一次新的迭代,直到命令返回游标0完成一次遍历。此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回0个元素,但只要游标不是0,程序都不会认为SCAN命令结束,但是返回的元素数量大概率符合count参数。另外,SCAN支持模糊查询。
例:
SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key
用游标和模糊匹配找key的方式,在key非常多的时候,内存消耗也会很严重,导致查询慢
---
# 解决方案:
>把这些相关的key,用String的方式或者hash或者listd的形式存储,用java代码来去查找匹配
/**
* 平台放最后
* @param key
* @return
*/
public static String getLastKeys(String key,String appVersion,String platform){
String resourceKey = new StringBuilder(ResourceConstant.toClientSourceKey).append(":")
.append(key)
.append(":").append("appVersion").append(":").append(appVersion).append(":").append("platform").append(":").append(platform).toString();
return resourceKey;
}//获取版本号和平台对应的key iOS&Android&MiniProgram&H5&FlutterAndroid&FultterIos 2.0.0的形式
String ishasKey = CacheKey.getLastKeys(resourceKey, appVersion, platform);
log.info("要去截取的key"+ishasKey);
String lastPlatform = ishasKey.substring(ishasKey.lastIndexOf(":") + 1);
log.info("截取的后面的{}",lastPlatform);
//截取前面的
String beforePlateform = ishasKey.substring(0, ishasKey.lastIndexOf(":"));
log.info("截取的前面的{}",beforePlateform);
List<String> resourceResponseList = JSON.parseArray(cmsData, String.class);
//拿出全部的key
if (!CollectionUtils.isEmpty(resourceResponseList)) {
log.info("redis资源位key的数量为{}",resourceResponseList.size());
for (String key : resourceResponseList) {
//将平台切割出来
String lastPlatform1 = key.substring(key.lastIndexOf(":") + 1);
//截取前面的
String beforePlateform1 = key.substring(0, key.lastIndexOf(":"));
if (beforePlateform1.equals(beforePlateform) && lastPlatform1.contains(lastPlatform)) {
finalKey = beforePlateform1 +":"+ lastPlatform1;
break;
}
}
log.info("要查的最终匹配的key{}",finalKey);
}
return finalKey;