终于把统计服务实现了,测试并发布上线。观察了几天 Redis 的内存开销,访问量大约每天 800 万,基本维持在 150 MB 以下,数据库的压力也有明显下降。
仔细复盘这个架构,存在一个缺点:由于 Watcher 从 SET
中 Pop
一个 Key,Pop 是随机的,所以有可能把新加入SET
的 key
给 Pop
出来了,而我们的目标是缓存 5 分钟再持久化。
比如说有篇热门博文,5 分钟之内有上千人阅读了,那么就有可能出现最坏的情况, Watcher 刚好 Pop 了上千次这个 Key,也就需要访问上千次数据库。
因此我们要改进这个架构,让每篇博文的阅读数统计尽可能的缓存 5 分钟,进一步减少数据库的压力。
难点是如何缓存 5 分钟。分解一下这个问题,由于SET
的Pop
是随机的,所以无法实现准确的缓存5分钟。我们要换一个结构ZSET
ZSET
和 SET 有些相似,不同的地方是:ZSET 的每个成员都有一个常用来排序的得分 Score,我们可以把每个缓存的开始时间的秒钟数当成 Score 进行排序。
改进之后的架构如下图所示:
Counter 服务接受到请求之后,根据博文的主键生成一个用于去重的 Key,例如 counter-blog-post-100
,去重标志作为 value,尝试插入 SET 集合,如果插入成功,说明是一个有效统计,生成一个用于计数的 Key,例如statistic-blog-post-100
,尝试 incr 到 String 中,incr 成功之后,生成一个用于计时的 Key,例如 zcounter-blog-post-100
以及对应的 score, 即此刻的时间的 ticks,插入到 zset 中。
后台服务定时任务,按序出栈,计算出 Score 对应的时间,判断这个 Key 是不是已经大于 5 分钟,如果大于则按照 Key 取出对应的统计数,再持久化,如果不大于,则停止出栈,等待下一次任务。
后台任务可以灵活的配置,当高峰时期,就多跑几个任务去出栈持久化。
这样就是实现了缓存 5 分钟的效果。
上线之后,观察 Redis 的资源使用情况,发现内存消耗增加了很多,但是对数据库的压力的减轻更明显了,如此实现了我们的目标。