在Redis中为您的实体的有效计数器存储
#php #laravel #redis

曾经,我需要做出一个有效的解决方案,使我能够在广告视图计数器上存储并添加大量统计数据,并能够按白天查看统计信息。

输入参数和限制:

  • 返回集群
  • 有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}:bidsnew:{advert:314}:bids之类的键也将分配给同一哈希插槽。

哈希插槽在Redis群集在节点之间均匀分配键的能力中起着至关重要的作用,从而促进了有效的数据分解和水平缩放。通过考虑关键名称并包含哈希标签,Redis确保将相关密钥存储在同一redis节点上,从而优化了逻辑键的数据访问。

执行

我在此解决方案中使用的结构:
关键名称:{advert:%d}:views
数据结构:hash map[string]intstringYYYYMMDD格式的日期,而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());
    }
}

有用的链接以增强您的知识: