【问题标题】:Redis Design Pattern: Supporting data pagination, sorting, filtering and searchingRedis设计模式:支持数据分页、排序、过滤和搜索
【发布时间】:2021-07-30 00:06:51
【问题描述】:

我有一个后端数据存储为 Redis 的应用程序。该应用程序(界面)为用户提供了一个必须支持搜索、分页、排序和过滤的表格。

我的 Redis 设计包括使用排序集和标准键:值对。例如,考虑一家二手车经销商。假设我有一个集合,toyota 的成员是与该品牌相关的所有待售汽车的列表。每个成员都是汽车型号和与实体汽车相关的一些唯一标识符的组合。

toyota
  - corolla:100
  - corolla:200
  - corolla:300
  - sienna:100
  - sienna:200

该集合的每个成员都有一个单独的键,例如toyota:corolla:100,其中的值是一个包含有关该特定汽车的各种信息的对象:

{
  id: 100,
  brand: "Toyota",
  model: "Corolla",
  color: "red",
  cost: 15000
}

了解这种基本数据关系后,我发现自己处于一个场景中,我希望通过每个键的对象中包含的某些属性来提供对这个前端表中的数据进行排序的能力。例如,汽车的颜色。当然,为此,我需要比较所有对象。

我的困境是如何在考虑分页的同时实现这一点。实际上,我的集合不是汽车,它们很容易包含数千名成员。但是数据关系是同一个概念。我不想为了确定这种排序而必须获取所有键,因为它违背了分页的目的。

我会澄清,我不是在我的 API 层中人为地对结果进行分页。我通过利用zrangebylex(提供一些基本排序)以及限制偏移量直接限制redis结果来实现分页。

$results = [];
$cars = $redis->zRangeByLex("toyota", '-', '+', 0, 1);

foreach( $cars as $car ) {
    $results[] = json_decode($redis->get($car), true);
}

// example $cars return:
// [ "corolla:100", "corolla:200" ]

// example $results return:
// [
//   { id: 100, brand: "Toyota", model: "Corolla", color: "red", cost: 15000 },
//   { id: 200, brand: "Toyota", model: "Corolla", color: "blue", cost: 14000 },
// ]

我想避免人为地对结果进行分页,因为在每次 API 调用中获取数千条记录,然后遍历它们,花费的时间比可接受的要长。

我还要注意,在搜索方面,我正在使用 zscan 来搜索集合——这并不理想,因为这意味着我受到每个集合中成员值的限制。

$search = "corolla"; # user search term
$cars = []; # result container

$it = NULL; # iterator
$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

while($matches = $redis->zScan('toyota', $it, "*{$search}*")) {
    foreach($matches as $key => $score) {
        $cars[] = $key;
    }
}

// example $cars return:
// [ "corolla:100", "corolla:200", "corolla:300" ]

虽然我可以在 SQL 环境中重新设计此应用程序并相对轻松地实现所有这些功能,但我更感兴趣的是使用 Redis 来完成这项工作。什么是更合适的 Redis 设计/模式,它将支持我想在这个前端表中实现的所有功能(排序、分页、搜索、过滤)?

【问题讨论】:

    标签: php redis phpredis


    【解决方案1】:

    Redis 没有 indexes 的概念,因此您应该像您自己提到的那样构建 和维护类似索引的结构:一种很棒且有点简单的方法为此,需要为您希望排序和分页的每个索引维护一个 ZSET,每个 ZSET 成员的键都指向最终对象键(例如 corolla:100)它的分数是您用来对项目相对于其他项目进行排序和分页的值。

    有了这个设置,您可以使用ZRANGEBYSCORE 命令(或Redis 6.2+ 上的ZRANGE)及其LIMIT 选项来快速获取原始ZSET的分页子集.

    这里是一个如何定义 ZSET 的丰田汽车按成本排序的类索引结构,然后遍历其排序的项目,一页(每页只包含 2 个项目,为了这个例子)一次:

    ZADD cars-by-cost:toyota 15000 corolla:100
    ZADD cars-by-cost:toyota 12000 corolla:200
    ZADD cars-by-cost:toyota 16000 corolla:300
    ZADD cars-by-cost:toyota 13000 sienna:100
    ZADD cars-by-cost:toyota 15000 sienna:200
    
    ZRANGEBYSCORE cars-by-cost:toyota -inf +inf LIMIT 0 2
    ZRANGEBYSCORE cars-by-cost:toyota -inf +inf LIMIT 2 2
    ZRANGEBYSCORE cars-by-cost:toyota -inf +inf LIMIT 4 2
    

    在性能方面,ZRANGEBYSCOREhas a time complexity of

    O(log(N)+M) 其中 N 是排序集中的元素个数,M 是返回的元素个数

    因此它非常适合分页和排序作业。

    关于搜索:这实际上取决于您希望如何搜索以及您需要搜索哪些实体/字段;如果您对SCAN(及其伙伴HSCANZSCAN)提供的模式匹配感到满意,那么我建议为您希望的每个可搜索数据集保留并维护一个ZSET提供给您的用户并坚持使用ZSCAN

    另一方面,如果您需要类似全文的搜索体验,那么 RediSearch 模块将在这里大放异彩:https://github.com/RediSearch/RediSearch

    如果您不想或不能在您的 Redis 安装中使用外部模块,那么您可能需要遵循 here 提到的 SADD/SINTER 方法 - 您可以轻松地将其转换为 ZADD /ZINTER 方法,如果你需要的话。

    【讨论】:

    • 感谢您抽出宝贵时间提供您的见解和反馈。正如我所担心的,利用 Redis 的核心功能真的没有优雅的方法。我很欣赏关于 RediSearch 的提示,我不知道这个模块存在。在我的场景中,我正在使用 Azure 的 Redis 服务,如果不扩展到 Enterprise 版本,则此模块不可用,我无法证明这一点。我可能最终会实现一个混合模型,我同时使用 Redis 和 SQL。干杯伙伴。
    【解决方案2】:

    Redisearch 模块非常适合此用例。 https://oss.redislabs.com/redisearch/

    您不需要使用集合,因为 Redisearch 直接索引哈希。 Redisearch 是一个二级索引引擎,可让您轻松索引、查询、过滤、排序和分页数据。您不必使用全文搜索功能,但它们可能会派上用场。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-18
      • 2022-01-07
      • 1970-01-01
      • 1970-01-01
      • 2020-01-18
      • 2010-11-25
      • 2017-08-11
      • 2011-01-21
      相关资源
      最近更新 更多