问题描述
我有一个应用程序,其中后端数据存储是 Redis。这个应用程序(界面)为用户提供了一个必须支持搜索、分页、排序和过滤的表格。
我的 Redis 设计包括使用排序集和标准键值对。例如,考虑一家二手车经销商。假设我有一个集合,toyota
who's members 是与该品牌相关的所有待售汽车的列表。每个成员都是汽车型号和一些与实体汽车相关的唯一标识符的组合。
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",'-','+',1);
foreach( $cars as $car ) {
$results[] = json_decode($redis->get($car),true);
}
// example $cars return:
// [ "corolla:100","corolla:200" ]
// example $results return:
// [
// { id: 100,cost: 15000 },// { id: 200,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 设计/模式,它会支持我想在这个前端表中实现的所有功能(排序、分页、搜索、过滤)?
解决方法
Redis 没有索引的概念,因此您应该像您自己提到的那样构建和维护类似索引的结构:一种很棒且有点简单的方法要做到这一点,为您希望排序和分页的每个索引维护一个 ZSET,每个 ZSET 成员的键都指向最终的对象键(例如 {{1} }) 并且它的分数是您用来相对于其他项目对项目进行排序和分页的值。
完成此设置后,您可以使用 corolla:100
命令(或 Redis 6.2+ 上的 ZRANGEBYSCORE
)及其 ZRANGE
选项快速获取原始 ZSET。
这里是如何定义 ZSET 的丰田汽车按成本排序的类似索引的结构,然后迭代其排序的项目,一页(每页仅包含 2 个项目,为了这个例子)一次:
LIMIT
在性能方面,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
has a time complexity of:
O(log(N)+M) 其中 N 是排序集中的元素数,M 是返回的元素数
因此它非常适合分页和排序作业。
关于搜索:这实际上取决于您希望如何搜索以及您需要搜索哪些实体/字段;如果您对 ZRANGEBYSCORE
(及其同伴 SCAN
和 HSCAN
)提供的模式匹配感到满意,那么我建议为每个拥有并维护一个 ZSET您希望向用户提供并坚持使用 ZSCAN
的可搜索数据集。
另一方面,如果您需要类似全文的搜索体验,那么 RediSearch 模块会在这里真正发挥作用:https://github.com/RediSearch/RediSearch
如果您不想或不能在 Redis 安装中使用外部模块,那么您可能需要遵循 here 中提到的 ZSCAN
/SADD
方法 - 您可以轻松地如果需要,可以转换为 SINTER
/ZADD
方法。
Redisearch 模块非常适合此用例。 https://oss.redislabs.com/redisearch/
你不需要使用集合,因为 Redissearch 直接索引哈希。 Redisearch 是一个二级索引引擎,可让您轻松索引、查询、过滤、排序和分页数据。您不必使用全文搜索功能,但它们可能会有用。