曾经,我需要做出一个有效的解决方案,使我能够在广告视图计数器上存储并添加大量统计数据,并能够按白天查看统计信息。
输入参数和限制:
- 返回集群 li>
- 有100万个实体有要计算的意见
- 增量的时间复杂性-
O(1)
,在SELECT -O(n)
上
要实现的功能:
- 增加视图计数
- 获得上个月的观看
- 有史以来获取视图
我立即发现,可以使用简单的哈希存储键可以非常有效地完成,主要的贴上点是如何有效实现群集。 REDIS可以通过哈希插槽跨簇分发钥匙的能力派上用场,使我可以均匀地将负载分布在整个集群上。
一些理论
redis利用哈希插槽在redis群集中的多个节点上分发键。这种分配机制可扩展性和高可用性。哈希插槽的分配基于每个redis键的密钥名称。
创建了redis键时,将哈希函数应用于其密钥名称以确定哈希插槽。 REDIS采用了2^14
-Slot Hash空间,每个插槽均由0到16383的数字识别。通过使用哈希函数和Modulo操作,REDIS将密钥名称的哈希值映射到特定的哈希插槽中。
使用CRC16算法作为默认哈希函数。该算法可确保在哈希空间中分布良好的键分配。但是,包含Curly Bracs {}
的键引入了一个称为哈希标签的概念。 Hash标签允许Redis可以将相同的哈希标签键分为相同的哈希插槽,而与关键名称的其余部分无关。例如,如果键具有格式{advert:314}:views
,则REDIS仅在计算哈希插槽时考虑Hash tag advert:314
。因此,{advert:314}:bids
或new:{advert:314}:bids
之类的键也将分配给同一哈希插槽。
哈希插槽在Redis群集在节点之间均匀分配键的能力中起着至关重要的作用,从而促进了有效的数据分解和水平缩放。通过考虑关键名称并包含哈希标签,Redis确保将相关密钥存储在同一redis节点上,从而优化了逻辑键的数据访问。
执行
我在此解决方案中使用的结构:
关键名称:{advert:%d}:views
数据结构:hash map[string]int
,string
是YYYYMMDD
格式的日期,而int
是计数器。
让我们看一下实践中的每个功能。
增加视图计数
我使用HINCRBY命令来递增特定日期的计数器。
示例命令:
redis> HINCRBY {advert:314}:views 20230606 1
---------- Response ----------
(integer) 1
获取上个月的观点
要获取上个月的数据,我正在使用HMGET命令,其中包含我想获得统计信息的日期列表。
示例命令:
redis> HMGET {advert:314}:views 20230606 20230605 20230604 20230603 20230602 20230601
---------- Response ----------
1) 1
2) 2
3) (nil)
4) 1
5) 6
请注意,与给定字段相关的值列表按照要求的顺序相同。
有史以来欣赏意见
这个问题比以前的问题要困难得多。
我可以运行2个查询,以从钥匙中获取字段列表并通过它们迭代,但我不喜欢它。因此,我决定通过EVAL命令使用内部LUA脚本,Redis的客户端将更有效,因为它仅适用于1个查询。
示例命令:
redis> EVAL "local totalView = 0
local dayViews = redis.call('hvals', KEYS[1])
for _, dayView in ipairs(dayViews) do
totalView = totalView + tonumber(dayView)
end
return totalView" 1 {advert:314}:views
---------- Response ----------
(integer) 10
作为奖金,我将使用此实现附加一个现成的PHP类,该实现使用Laravel的Illuminate Redis客户库编写:
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Entity;
use Illuminate\Redis\RedisManager;
class ViewStatsRepository
{
private const
DATE_FORMAT = 'Ymd',
VIEW_STATS_DAYS = 30
;
public function __construct(
private readonly RedisManager $redis
) { }
public function getTotalViewsCount(Entity $entity): int
{
$evalScript = <<<LUA
local totalView = 0
local dayViews = redis.call('hvals', KEYS[1])
for _, dayView in ipairs(dayViews) do
totalView = totalView + tonumber(dayView)
end
return totalView
LUA;
return (int) $this->redis->eval($evalScript, 1, $this->viewCountKey($entity));
}
public function getTodayViewsCount(Entity $entity, DateTimeInterface $now): int
{
return $this->getViewsCountByDate($entity, $now);
}
public function getViewsCountByDate(Entity $entity, DateTimeInterface $now): int
{
return (int) $this->redis->hget($this->viewCountKey($entity), $now->format(self::DATE_FORMAT));
}
public function getViewStats(Entity $entity, DateTimeInterface $now): array
{
$keys = [];
for ($i = self::VIEW_STATS_DAYS - 1; $i >= 0; $i--) {
$keys[] = $now->modify("-{$i} days")->format(self::DATE_FORMAT);
}
$stats = array_combine($keys, array_values($this->redis->hMGet($this->viewCountKey($entity), $keys)));
return array_map('intval', $stats);
}
public function incrementViewsCount(Entity $entity, DateTimeInterface $now, int $incrementBy = 1): int
{
return (int) $this->redis->hIncrBy(
$this->viewCountKey($entity),
$now->format(self::DATE_FORMAT),
$incrementBy
);
}
private function viewCountKey(Entity $entity): string
{
return sprintf('{advert:%d}:views', $entity->getId());
}
}
有用的链接以增强您的知识: