【问题标题】:Efficient Redis SCAN of multiple key patterns多种关键模式的高效 Redis SCAN
【发布时间】: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 中实现这种模式匹配?

  1. 按顺序对每个可扫描模式执行多个SCAN 并合并结果?
  2. LUA 脚本在扫描键时对每个模式使用改进的模式匹配,并在单个 SCAN 中获取所有匹配键?
  3. 建立在排序集之上的索引,支持快速查找与单个字段匹配的键,并使用 ZUNIONSTORE 解决同一字段中的匹配交替,并使用 ZINTERSTORE 解决不同字段的交集?

:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN

  1. 建立在排序集之上的索引支持快速查找匹配所有维度组合的键,从而避免联合和交集,但浪费更多存储空间并扩展我的索引键空间占用空间?

:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN
:: => key1, key2, keyN

  1. 利用 RedisSearch? (虽然对于我的用例来说不可能,但请参阅 Tug Grall 答案,这似乎是一个非常好的解决方案。)
  2. 其他?

我已经实现了 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


【解决方案1】:

我会投票给选项 3,但我可能会开始使用 RediSearch

你也看过 RediSearch 吗?该模块允许您创建二级索引并进行复杂的查询和全文搜索。

这可能会简化您的开发。

我邀请您查看projectGetting Started

安装后,您将能够使用以下命令实现它:


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

我不会在这里详细解释所有命令,因为您可以在教程中找到解释,但这里是基础知识:

  • 使用哈希存储配方
  • 创建一个 RediSearch 索引并索引您要查询的字段
  • 运行查询,例如:
    • 获取所有西班牙沙漠: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

【讨论】:

  • 我正在对所有选项进行原型设计,如果我能找到一种使用 AWS ElastiCache (Redis) 部署它的方法,我会尝试使用 RediSearch 选项来实现。
  • RediSearch 在 Elasticache 上不可用,如果您想在 AWS 上使用,您要么必须使用 Redis Cloud:redislabs.com/try-redis-modules-for-free,要么安装在 EC2 上。 (抱歉,在我的第一个答案中,我错过了 Elasticache 部分,重点是示例代码)
【解决方案2】:

我最终使用了一个简单的策略来在创建键时更新每个字段的每个二级索引:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-03
    • 1970-01-01
    • 2020-08-29
    • 1970-01-01
    • 2021-07-25
    • 1970-01-01
    • 2014-11-14
    相关资源
    最近更新 更多