Redis设计模式:支持数据分页、排序、过滤、搜索

Redis Design Pattern: Supporting data pagination, sorting, filtering and searching

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

我的 Redis 设计包括使用排序集和标准 key:value 对。例如,考虑一家二手车经销商。假设我有一组,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
}

理解了这种基本的数据关系,我发现自己处于这样一种场景中,我想提供在这个前端 table 中对数据进行排序的能力,通过对象中包含的一些 属性每个键。比方说,汽车的颜色。当然,为了做到这一点,我需要比较所有的对象。

我的困境是如何在考虑分页的同时实现它。实际上,我的布景不是汽车,它们可以轻松容纳数千名成员。但是数据关系是同一个概念。我不想为了确定这种排序而获取所有键,因为它违背了分页的目的。

我要澄清一下,我并不是在我的 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 调用时获取数千条记录,然后遍历它们,比 acceptable.

花费的时间更长

我还要注意,在搜索时,我正在利用 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 design/pattern 更合适,它会支持我想在此前端 table 中实现的所有功能(排序、分页、搜索、过滤)?

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

在性能方面,ZRANGEBYSCORE has a time complexity of:

O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned

因此它将是分页和排序作业的理想选择。

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

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

如果您不想或不能在您的 Redis 安装中使用外部模块,那么您可能需要遵循 here 提到的 SADD/SINTER 方法 - 您如果您需要,可以很容易地转化为 ZADD/ZINTER 方法。

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

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