【发布时间】:2021-01-12 07:32:59
【问题描述】:
我正在尝试对我的数据使用SCAN 操作来支持一些多选查询和过滤操作,但我不确定我是否朝着正确的方向前进。
我正在使用 AWS ElastiCache (Redis 5.0.6)。
密钥设计:
示例:
13434:鳄梨酱:蘸酱:墨西哥
34244:西班牙凉菜汤:汤:西班牙
42344:西班牙海鲜饭:菜:西班牙
23444:HotDog:StreetFood:美国
78687:CustardPie:甜点:葡萄牙
75453:Churritos:甜点:西班牙
如果我想使用 SCAN glob 样式匹配模式无法处理的复杂多选过滤器(例如返回匹配来自两个不同国家的五种配方类型的所有键)来支持查询,常见的方法是什么为生产场景而努力?
假设我将通过对所有场交替模式和多场过滤器进行笛卡尔积来计算所有可能的模式:
[[鳄梨酱、西班牙凉菜汤]、[汤、菜、甜点]、[葡萄牙]]
*:鳄梨酱:汤:葡萄牙
*:鳄梨酱:菜:葡萄牙
*:鳄梨酱:甜点:葡萄牙
*:西班牙凉菜汤:汤:葡萄牙
*:西班牙凉菜汤:菜:葡萄牙
*:西班牙凉菜汤:甜点:葡萄牙
我应该使用什么机制在 Redis 中实现这种模式匹配?
- 按顺序对每个可扫描模式执行多个
SCAN并合并结果? - LUA 脚本在扫描键时对每个模式使用改进的模式匹配,并在单个
SCAN中获取所有匹配键? - 建立在排序集之上的索引,支持快速查找与单个字段匹配的键,并使用
ZUNIONSTORE解决同一字段中的匹配交替,并使用ZINTERSTORE解决不同字段的交集?
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
- 建立在排序集之上的索引支持快速查找匹配所有维度组合的键,从而避免联合和交集,但浪费更多存储空间并扩展我的索引键空间占用空间?
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
- 利用 RedisSearch? (虽然对于我的用例来说不可能,但请参阅 Tug Grall 答案,这似乎是一个非常好的解决方案。)
- 其他?
我已经实现了 1) 并且性能很糟糕。
private static HashSet<String> redisScan(Jedis jedis, String pattern, int scanLimitSize) {
ScanParams params = new ScanParams().count(scanLimitSize).match(pattern);
ScanResult<String> scanResult;
List<String> keys;
String nextCursor = "0";
HashSet<String> allMatchedKeys = new HashSet<>();
do {
scanResult = jedis.scan(nextCursor, params);
keys = scanResult.getResult();
allMatchedKeys.addAll(keys);
nextCursor = scanResult.getCursor();
} while (!nextCursor.equals("0"));
return allMatchedKeys;
}
private static HashSet<String> redisMultiScan(Jedis jedis, ArrayList<String> patternList, int scanLimitSize) {
HashSet<String> mergedHashSet = new HashSet<>();
for (String pattern : patternList)
mergedHashSet.addAll(redisScan(jedis, pattern, scanLimitSize));
return mergedHashSet;
}
对于 2) 我创建了一个 Lua 脚本来帮助服务器端 SCAN,性能并不出色,但比 1) 快得多,即使考虑到 Lua 不支持交替匹配模式和我必须通过模式列表循环每个键以进行验证:
local function MatchAny( str, pats )
for pat in string.gmatch(pats, '([^|]+)') do
local w = string.match( str, pat )
if w then return w end
end
end
-- ARGV[1]: Scan Count
-- ARGV[2]: Scan Match Glob-Pattern
-- ARGV[3]: Patterns
local cur = 0
local rep = {}
local tmp
repeat
tmp = redis.call("SCAN", cur, "MATCH", ARGV[2], "count", ARGV[1])
cur = tonumber(tmp[1])
if tmp[2] then
for k, v in pairs(tmp[2]) do
local fi = MatchAny(v, ARGV[3])
if (fi) then
rep[#rep+1] = v
end
end
end
until cur == 0
return rep
以这种方式调用:
private static ArrayList<String> redisLuaMultiScan(Jedis jedis, String luaSha, List<String> KEYS, List<String> ARGV) {
Object response = jedis.evalsha(luaSha, KEYS, ARGV);
if(response instanceof List<?>)
return (ArrayList<String>) response;
else
return new ArrayList<>();
}
对于 3) 我已经使用排序集为 3 个字段中的每一个更新了二级索引,并使用单个字段上的交替匹配模式和多字段匹配模式实现了查询,如下所示:
private static Set<String> redisIndexedMultiPatternQuery(Jedis jedis, ArrayList<ArrayList<String>> patternList) {
ArrayList<String> unionedSets = new ArrayList<>();
String keyName;
Pipeline pipeline = jedis.pipelined();
for (ArrayList<String> subPatternList : patternList) {
if (subPatternList.isEmpty()) continue;
keyName = "un:" + RandomStringUtils.random(KEY_CHAR_COUNT, true, true);
pipeline.zunionstore(keyName, subPatternList.toArray(new String[0]));
unionedSets.add(keyName);
}
String[] unionArray = unionedSets.toArray(new String[0]);
keyName = "in:" + RandomStringUtils.random(KEY_CHAR_COUNT, true, true);
pipeline.zinterstore(keyName, unionArray);
Response<Set<String>> response = pipeline.zrange(keyName, 0, -1);
pipeline.del(unionArray);
pipeline.del(keyName);
pipeline.sync();
return response.get();
}
我的压力测试用例的结果在请求延迟方面明显偏向于 3):
【问题讨论】:
-
第三种方案,即建立二级索引,是更好的选择。
标签: indexing redis lua jedis amazon-elasticache