【资料图】
Redis 是一种流行的内存键值数据库,支持各种数据类型和结构,主要用于缓存短期数据和执行大规模实时操作。
如果您对 PHP 示例感兴趣,请跳到本文底部(锚点)。
先决条件
redis — 你需要在你的机器上安装 redis。
排序集
Redis 支持排序集——一组唯一字符串(称为集成员)与一个称为分数的数值相关联,这是一个示例集:
top-dinosaurs└── Brachiosaurus└── Velociraptor└── T-Rex└── Canada_Goose
现在让我们为我们集合中的每个成员添加一个分数top-dinosaurs,例如,一个残酷分数:
top-dinosaurs└── Brachiosaurus 1└── Velociraptor 3└── T-Rex 5└── Canada_Goose 10
让我们将上面的示例加载到 redis 存储中。在您最喜欢的终端中,运行redis-cli以连接到您已经运行的 redis 服务器,然后执行以下操作:
# 我们将每个成员及其分数添加到我们的集合中127.0.0.1:6379>zadd top-dinosaurs 1 Brachiosaurus(integer) 1127.0.0.1:6379>zadd top-dinosaurs 3 Velociraptor(integer) 1127.0.0.1:6379>zadd top-dinosaurs 5 T-Rex(integer) 1127.0.0.1:6379>zadd top-dinosaurs 10 Canada_Goose(integer) 1# verify that we have all our set members present127.0.0.1:6379>zrange top-dinosaurs 0 -1 withscores1) "Brachiosaurus"2) "1"3) "Velociraptor"4) "3"5) "T-Rex"6) "5"7) "Canada_Goose"8) "10"
现在从上面的例子来看,我们所做的是明确地为成员指定一个分数,如果成员不存在,这实际上是创建成员,否则覆盖其现有分数。
对于趋势应用程序来说不是很实用,是吗?
不,我们将使用的是ZINCRBYAPI 来增加成员在集合中的分数,从而使我们能够提高其在集合中的存在:
# increment score127.0.0.1:6379>zincrby top-dinosaurs 1 Canada_Goose"11"# read the set127.0.0.1:6379>zrange top-dinosaurs 0 -1 withscores1) "Brachiosaurus"2) "1"3) "Velociraptor"4) "3"5) "T-Rex"6) "5"7) "Canada_Goose"8) "11"
正如您在集合预览中看到的那样,集合预览zrange向我们展示了根据分数按升序排序的值。在我们的例子中我们需要的是一个反转的范围,它首先显示最高分,为此,我们将使用zrevrangebyscore:
127.0.0.1:6379>zrevrangebyscore top-dinosaurs +inf -inf withscores 1) "Canada_Goose" 2) "11" 3) "T-Rex" 4) "5" 5) "Velociraptor" 6) "3" 7) "Test" 8) "1" 9) "Brachiosaurus"10) "1"
zrevrangebyscore采用以下参数:
设置名称 ( top-dinosaurs)最高分(+inf将使其无限)最小分数(-inf以相同的方式工作,尽管一个简单的对我们有用,因为我们希望我们的分数是无符号的并且大于)withscores— 当然,我们希望分数与集合成员一起返回。
到目前为止,我们已经学会了创建和预览集合、向集合添加成员以及增加同一集合内成员的分数。现在让我们回到我们的案例。
使用 Redis 排序集检测趋势内容
假设我们正在创建一个类似于 Twitter 的众包内容网站。用户将能够向我们的数据库提交各种内容类型,包括文本。我们可以将这些文本输入分解为单词列表,从而提高每个主题的趋势分数。这是一个例子:
# user inputHappy #Caturday! Hope you"re all #feline good. # topics extracted, based on hashtags usedCaturday: 1feline: 1
创建帖子后,我们将1逐点提升上述 2 个主题:
127.0.0.1:6379>zincrby hot-topics 1 Caturday"1"127.0.0.1:6379>zincrby hot-topics 1 feline"1"
这为我们创建了一个集合:hot-topics。我们可以继续增加同一组主题,为我们的热门话题创建一个成熟的商店。
到目前为止,我们正在摇摆不定。
只有一个问题——我们没有数据清理计划。我们创建的集合将永远存在,这意味着它永远不会过期:
127.0.0.1:6379>ttl hot-topics(integer) -1
不用担心,您也可以过期集合,就像键一样,并使用相同的EXPIRE命令:
# 这将使设置在 1 小时后过期127.0.0.1:6379>expire hot-topics 3600(integer) 1# read the TTL (time-to-live) again127.0.0.1:6379>ttl hot-topics(integer) 3584
所以,计划是只调用EXPIRE一次命令,否则,我们只会继续延长集合的 TTL。在您的应用程序中,只需验证 TTL 是否为-1,然后您才能expire拨打电话:
if redis.ttl(key) == -1: redis.expire(key, 3600)
提高效率
正如我们目前所见,我们只创建了一个集合并为其分配了特定的 TTL(在我们的示例中为一个小时)。这意味着我们将继续向集合中添加内容,直到 TTL 时钟重置并且集合被删除为止。那时,我们只剩下热门话题了。
问题是您不能独立于整个集合使 redis 集合成员过期,因此一个小时前添加的主题将过期,而不会留下集合中最近添加的主题。
似乎最有效的解决方案是组合集合。
我们将遵循与上述相同的方法,但是,我们将创建一定数量的集合而不是一个集合。因此,1 小时的时间窗口将导致每分钟一组(允许我们最多坚持 60 组),或每 2 分钟一组(最多 30 组),或每 5 分钟,等等。
这是一个解释上述场景的示例——使用当前时间戳为我们的集合添加后缀是有意义的:
# this will create 60 sets every hourhot-topics-{hour}:{minute}# this will create 13 sets every hourhot-topics-{hour}-{round(minute/5)}
该解决方案的工作方式是我们避免创建许多集合,这会在稍后组合集合时导致大量读取操作。
随着每个集合自行过期,我们不必担心进行任何数据清理,我们将始终在内存数据库中显示最新的集合,我们的工作是将它们组合起来。
组合集
组合我们最新的集合是一项简单的任务:
查询内存存储中存在的最新集合列表获取每个集合的顶级成员,并将它们添加到全局字典中
要查询尚未过期的集合,我们可以使用keys带有模式的 API,因此理想情况下结果不会与您可能拥有的其他键重叠:
此示例使用扩展Redis提供的类php-redis,您可以安装它,或稍微重构代码以使用另一个客户端,例如predis/predis.
connect("0.0.0.0", 6379);register_shutdown_function([$redis, "close"]);}return $redis;}function boost_topic(string $setId, string $member, int $increment_by=1) : void{// create a set for 5 minutes, with a TTL of 1 hour// reason: cannot expire set members without expiring the whole set$setId .= ":" . date("H.") . round(date("i")/5);// increment set member scoreget_redis()->zincrby($setId, $increment_by, $member);if ( get_redis()->ttl($setId) == -1 ) // is this a new set? if so, set TTL to 60min to we don"t keep stale dataget_redis()->expire($setId, 3600);}function get_top_topics(string $setId, int $limit=10) : array{$items = [];// query all sets created in the past 30minif ( $sets = get_redis()->keys("{$setId}:*") ) {$meta = [ "withscores" =>true, "limit" =>[0, $limit] ];// begin transactionget_redis()->multi();foreach ( $sets as $set ) {// get top N (=$limit) winners of each set and merge them togetherget_redis()->zrevrangebyscore($set, "+inf", "-inf", $meta);}// commit transaction$result = get_redis()->exec();foreach ( $sets as $i =>$set ) {if ( $list = array_map("intval", $result[$i] ?? []) ) {foreach ( $list as $k=>$v ) {$items[$k] = ($items[$k] ?? 0) + $v;}}}arsort($items);// return top winners$items = array_slice($items, 0, $limit);}return $items;}// boost a topic"s scoreboost_topic("hot-topics", "Caturday");boost_topic("hot-topics", "Feline");boost_topic("hot-topics", "Thursday");// boost a topic"s score by 2 pointsboost_topic("hot-topics", "Feline", 2);// get a list of top 2 trendsget_top_topics("hot-topics", 2); // [ "Feline" =>3, "Caturday" =>1 ]