高效的多键模式redis扫描

jvidinwx  于 2021-06-08  发布在  Redis
关注(0)|答案(2)|浏览(352)

我正在尝试使用 SCAN 对我的数据进行操作,我不确定我的方向是否正确。
我正在使用aws elasticache(redis 5.0.6)。
密钥设计::::
例子:
13434:guacamole:dip:墨西哥
34244:gazpacho:soup:西班牙
42344:paella:dish:西班牙
23444:hotdog:streetfood:美国
78687:c码ustardpie:dessert:葡萄牙
75453:churritos:dessert:西班牙
如果我想使用复杂的多选过滤器(例如返回匹配来自两个不同国家的五种配方类型的所有键)增强查询,那么 SCAN glob样式的匹配模式无法处理,对于生产场景来说,常见的方法是什么?
假设我将通过对所有场交替模式和多场滤波器进行笛卡尔积来计算所有可能的模式:
[[鳄梨酱,意大利浓汤],[汤,菜,甜点],[葡萄牙]]

  • :guacamole:soup:葡萄牙
  • :guacamole:dish:葡萄牙
  • :guacamole:dessert:葡萄牙
  • :gazpacho:soup:葡萄牙
  • :gazpacho:dish:葡萄牙
  • :gazpacho:dessert:葡萄牙

我应该使用什么机制在redis中实现这种模式匹配?
做多次 SCAN 对每个可扫描的模式进行排序并合并结果?
lua脚本在扫描键时对每个模式使用改进的模式匹配,并在一个脚本中获得所有匹配的键 SCAN ?
一种建立在排序集之上的索引,支持快速查找与单个字段匹配的键,并解决同一字段中的匹配交替问题 ZUNIONSTORE 求不同域的交集 ZINTERSTORE ?
<配方名称>::=>键1,键2,键n
:<配方类型>:=>键1、键2、键n
::<原产国>=>键1、键2、键n
一个建立在排序集之上的索引,支持快速查找匹配所有维度组合的键,因此避免了联合和中间词,但浪费了更多的存储空间并扩展了索引键空间?
<配方名称>::=>键1,键2,键n
<配方名称>:<配方类型>:=>键1、键2、键n
<配方名称>::<原产国>=>键1、键2、键n
:<配方类型>:=>键1、键2、键n
:<配方类型>:<原产国>=>键1、键2、键n
::<原产国>=>键1、键2、键n
利用再搜索(虽然对于我的用例来说是不可能的,但请参阅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):

zour9fqk

zour9fqk1#

我会投票给选项3,但我可能会开始使用重新搜索。
你也看过再搜索吗?此模块允许您创建辅助索引并执行复杂查询和全文搜索。
这可能会简化您的开发。
我邀请你来看看这个项目和开始。
一旦安装,您将能够通过以下命令实现:

HSET recipe:13434 name "Guacamole" type "Dip" country "Mexico" 

HSET recipe:34244 name "Gazpacho" type "Soup" country "Spain"

HSET recipe:42344 name "Paella"  type "Dish" country "Spain"

HSET recipe:23444 name "Hot Dog"  type "StreetFood" country "USA"

HSET recipe:78687  name "Custard Pie"  type  "Dessert" country "Portugal"

HSET recipe:75453  name "Churritos" type "Dessert" country "Spain"

FT.CREATE idx:recipe ON HASH PREFIX 1 recipe: SCHEMA name TEXT SORTABLE type TAG SORTABLE country TAG SORTABLE

FT.SEARCH idx:recipe "@type:{Dessert}"

FT.SEARCH idx:recipe "@type:{Dessert} @country:{Spain}" RETURN 1 name

FT.AGGREGATE idx:recipe "*" GROUPBY 1 @type REDUCE COUNT 0 as nb_of_recipe

我不是在这里详细解释所有的命令,因为你可以在教程中找到解释,但这里有一些基本知识:
使用散列存储食谱
创建一个重新搜索索引,并为要查询的字段编制索引
运行查询,例如:
要获得所有西班牙沙漠: FT.SEARCH idx:recipe "@type:{Dessert} @country:{Spain}" RETURN 1 name 要按类型计算配方数: FT.AGGREGATE idx:recipe "*" GROUPBY 1 @type REDUCE COUNT 0 as nb_of_recipe

sg2wtvxw

sg2wtvxw2#

最后,我使用了一个简单的策略,在创建键时更新每个字段的每个辅助索引:

protected static void setKeyAndUpdateIndexes(Jedis jedis, String key, String value, int idxDimSize) {
    String[] key_arr = key.split(":");
    Pipeline pipeline = jedis.pipelined();

    pipeline.set(key, value);
    for (int y = 0; y < key_arr.length; y++)
        pipeline.zadd(
                "idx:" +
                    StringUtils.repeat(":", y) +
                    key_arr[y] +
                    StringUtils.repeat(":", idxDimSize-y),
                java.time.Instant.now().getEpochSecond(),
                key);

    pipeline.sync();
}

找到与模式匹配的多个键(包括交替模式和多字段过滤器)的搜索策略如下所示:

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();
}

相关问题