Redis实现计数统计

介绍

计数器大量应用于互联网上大大小小的项目,你可以在很多场景都能找到计数器的应用范畴,单纯以技术派项目为例,也有相当多的地方会有计数相关的诉求,比如

文章带赞数

收藏数

评论数

用户粉丝数

......

技术派中有两种查询计数相关的方案,一个是基于db中的操作记录进行实施,一种是基于redis的incr特性来实现计数器

下面来看一下,redis的计数器是怎样用于技术派的技术场景的

计数的业务场景

首先我们看一下技术派中使用到的计数器的场景,主要有两大类(业务计数+pv/uv),三个细分领域(用户、文章、站点)

用户的相关统计信息

        文章数,文章总阅读数,粉丝数,关注作者数,文章被收藏数、被点赞数量

    站点的pv/uv等统计信息

        网站的总pv/uv,某一天的pv/uv

        某个uri的pv/uv

注意上面的几个场景,这里主要介绍redis计数器的使用

那用户与文章的相关统计将是我们的重点,因为这两个的业务属性很相似,因此我们选择一个重点,以用户统计来实现。

redis计数器

redis计数器,主要是借助原生的incr指令来实现原子的+1-1操作,更棒的是不仅redis的string数据结构支持incr,hash、zset数据结构同样也是支持incr的

1.incr指令

Redis incr命令将key中存储的数字值增值一。

        如果key不存在,那么key的值会先被初始化为0,然后在执行INCR操作。

        如果值包含错误类型,或者字符串类型的值不能表示为数字,那么返回一个错误。

        本操作的值限制在64位有符号数字表示之内。

接下来看项目封装实现

    /*** 自增** @param key* @param filed* @param cnt* @return*/public static Long hIncr(String key, String filed, Integer cnt) {return template.execute((RedisCallback<Long>) con -> con.hIncrBy(keyBytes(key), valBytes(filed), cnt));}

2.用户计数统计

我们将用户的相关计数,每个用户对应一个hash数据结构

        key: user_statistic_${userId}

        filed: 

                follCount: 关注数

                fansCount: 粉丝数

                articleCount: 已发布文章数

                praiseCount: 文章点赞数

                readCount: 文章被阅读数

                collectionCount: 文章被收藏数

计数器的核心就在于满足条件之后,实现的计数 + 1 / -1

通常的业务场景中,此类计数不太建议直接与业务代码强耦合,举个例子

用户收藏了一篇文章,若按照正常的设计,就是在收藏这里,带哦用计数器执行 + 1 操作 

上面这样实现有问题吗? 

        显然是没有额问题的,但是不够好,不够优雅。

比如现在技术派的场景中,点赞之后,除了计数器更新之外,还有前面用户说到的用户活跃度更新,若所有的逻辑都放在业务中,会导致业务的耦合较重

技术派选择消息机制来应对这种场景(大一点的项目会设计自己额的消息总线,为了让各自的业务逻辑内聚,向外抛出自己额的状态/业务变更消息,实现解耦)

对映的,计数实现逻辑在。src/main/java/com/github/paicoding/forum/service/statistics/listener/UserStatisticEventListener.java

package com.github.paicoding.forum.service.statistics.listener;import com.github.paicoding.forum.api.model.enums.ArticleEventEnum;
import com.github.paicoding.forum.api.model.event.ArticleMsgEvent;
import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent;
import com.github.paicoding.forum.core.cache.RedisClient;
import com.github.paicoding.forum.service.article.repository.dao.ArticleDao;
import com.github.paicoding.forum.service.article.repository.entity.ArticleDO;
import com.github.paicoding.forum.service.comment.repository.entity.CommentDO;
import com.github.paicoding.forum.service.user.repository.entity.UserFootDO;
import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO;
import com.github.paicoding.forum.service.statistics.constants.CountConstants;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;import javax.annotation.Resource;/*** 用户活跃相关的消息监听器** @author YiHui* @date 2023/8/19*/
@Component
public class UserStatisticEventListener {@Resourceprivate ArticleDao articleDao;/*** 用户操作行为,增加对应的积分*这段代码是一个使用Spring框架的事件监听器注解。* 它使用了@EventListener注解来指定要监听的事件类型为NotifyMsgEvent.class,并且使用了@Async注解来表示该方法是异步执行的。** 当NotifyMsgEvent事件被发布时,该事件监听器方法将被自动调用。由于使用了@Async注解,* 该方法将在单独的线程中异步执行,不会阻塞主线程。* @param msgEvent*/@EventListener(classes = NotifyMsgEvent.class)@Asyncpublic void notifyMsgListener(NotifyMsgEvent msgEvent) {switch (msgEvent.getNotifyType()) {//评论/回复case COMMENT:case REPLY:CommentDO comment = (CommentDO) msgEvent.getContent();RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + comment.getArticleId(), CountConstants.COMMENT_COUNT, 1);break;//删除评论/回复case DELETE_COMMENT:case DELETE_REPLY:comment = (CommentDO) msgEvent.getContent();RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + comment.getArticleId(), CountConstants.COMMENT_COUNT, -1);break;//收藏case COLLECT:UserFootDO foot = (UserFootDO) msgEvent.getContent();RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.COLLECTION_COUNT, 1);RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.COLLECTION_COUNT, 1);break;//取消收藏case CANCEL_COLLECT:foot = (UserFootDO) msgEvent.getContent();RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.COLLECTION_COUNT, -1);RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.COLLECTION_COUNT, -1);break;//点赞case PRAISE:foot = (UserFootDO) msgEvent.getContent();RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.PRAISE_COUNT, 1);RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.PRAISE_COUNT, 1);break;//取消点赞case CANCEL_PRAISE:foot = (UserFootDO) msgEvent.getContent();RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.PRAISE_COUNT, -1);RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.PRAISE_COUNT, -1);break;case FOLLOW:UserRelationDO relation = (UserRelationDO) msgEvent.getContent();// 主用户粉丝数 + 1RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getUserId(), CountConstants.FANS_COUNT, 1);// 粉丝的关注数 + 1RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getFollowUserId(), CountConstants.FOLLOW_COUNT, 1);break;case CANCEL_FOLLOW:relation = (UserRelationDO) msgEvent.getContent();// 主用户粉丝数 + 1RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getUserId(), CountConstants.FANS_COUNT, -1);// 粉丝的关注数 + 1RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getFollowUserId(), CountConstants.FOLLOW_COUNT, -1);break;default:}}/*** 发布文章,更新对应的文章计数** @param event*/@Async@EventListener(ArticleMsgEvent.class)public void publishArticleListener(ArticleMsgEvent<ArticleDO> event) {ArticleEventEnum type = event.getType();if (type == ArticleEventEnum.ONLINE || type == ArticleEventEnum.OFFLINE || type == ArticleEventEnum.DELETE) {Long userId = event.getContent().getUserId();int count = articleDao.countArticleByUser(userId);RedisClient.hSet(CountConstants.USER_STATISTIC_INFO + userId, CountConstants.ARTICLE_COUNT, count);}}
}

上面直接基于当下技术派抛出的各种消息事件,来实现用户/文章对应计数变更

不一样的地方则在于用户的文章数统计,因为消息发布时,并没有告知这个文章是 从 未上线状态到发布, 发布到下线/删除 ,因此无法进行+1 -1。我们直接采用的是全量的更新策略。

注:

全量更新策略指的是**在数据同步或更新过程中,每次都对整个数据集进行处理,而不是只更新发生变化的部分**。

这种策略的优点包括:

- **简单直观**:由于不需要考虑数据的增量变化,因此实现起来相对简单,易于理解和操作。
- **数据一致性**:每次全量更新可以确保目标系统中的数据与源系统保持完全一致,避免了因部分更新而导致的数据不一致问题。

然而,全量更新策略也存在一些缺点:

- **资源消耗大**:当数据量庞大或者更新频率较高时,全量更新可能会占用大量的网络带宽和存储资源,导致效率低下。
- **系统压力大**:频繁的全量更新可能会给系统带来较大的处理压力,尤其是在数据量持续增长的情况下,可能会超出系统的处理能力。

此外,在某些情况下,全量更新策略可能不是最佳选择。例如,在数据仓库中,如果源数据库的数据量非常大,而且只有少量数据发生变更,使用全量更新策略就不如增量更新策略高效。增量更新策略只针对发生变化的数据进行处理,这样可以大大减少数据处理的工作量和系统资源的消耗。

总的来说,全量更新策略适用于数据量较小或更新频率较低的场景,而在数据量大且更新频繁的环境中,可能需要考虑其他更高效的数据更新策略。在实际应用中,应根据具体的业务需求和系统条件来选择合适的更新策略。

3.用户统计信息查询

前面实现了用户的相关统计数,查询用户的统计信息则相对简单了,直接hgetall即可。

4.缓存一致性

基本上到上面,一个完整的计数服务就已经成型了,但是我们在实际的生产服务中,再自信的人也不保证它没问题100分。

通常我们会做一个校对/定时同步任务来保证缓存与实际数据中的一致性

技术派中选择简单的定时同步方案来实现

        用户统计信息每天全量同步

                

        文章统计信息每天全量同步

5.小结

基于redis的incr ,很容易就可以实现计数相关的需求支撑,但是为啥我们要用redis来实现一个计数器呢?直接用数据库的原始数据进行统计有什么问题吗?

通常而言,项目初期,或者项目本身非常简单,访问量低,只希望快速上线支撑业务时,使用db进行统计即可,优势时简单,叙述,不容易出问题;缺点则是每次都是实时统计性能差,扩展性不强。

当我们项目发展起来,借助redis直接存储最终结果。再展示层直接俄获取即可,性能更强,满足高并发,缺点是数据的一致性保障难度高。先选择一个实现代价小的,再重构哈啊哈哈。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/535295.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

收藏贴!6个谈薪小技巧,助你拿到满意薪资

Salesforce的就业市场一直在迅猛发展&#xff0c;对Salesforce专业人士的需求持续不断&#xff0c;对优秀人才的需求更大。 本篇文章总结了6个谈薪小技巧&#xff0c;可以帮助SF从业者、求职者拿到满意的薪资。 01 了解市场价格 首先&#xff0c;需要了解当前就业市场的情况…

WPF监控平台(科技大屏)[一]

跟着B站的视频敲了一个略微复杂的WPF界面,链接如下.在这里我详细的写一份博客进行设计总结. 系统介绍和配置及主窗口设计_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Wy421Y7QD?p1&vd_source4796b18a2e4c1ec8a310391a5644b6da 成果展示 实现过程 总体来说,我的…

使用 pg_profile 在 Postgres 中生成性能分析报告

前言&#xff1a; postgres数据库中拥有大量的辅助插件用于帮助DBA更好的分析数据库性能或整个集群&#xff0c;包括索引、I/O、CPU和内存等&#xff0c;pg_profile是基于PostgreSQL标准统计信息视图的诊断工具&#xff0c;它类似于Oracle AWR架构&#xff0c;和Oracle一样&am…

UL1642标准_锂聚合物电池亚马逊测试报告

UL1642标准_锂聚合物电池亚马逊测试报告 什么是锂聚合物电池UL1642标准&#xff1f; UL1642 认证要求涵盖旨在用于技术人员可更换或用户可更换应用的锂离子电池。UL1642 认证要求是为了避免锂离子电池在产品中工作时发生火灾或爆炸的风险。 锂聚合物电池 UL是Underwriters L…

《ElementPlus 与 ElementUI 差异集合》icon 图标使用(包含:el-button,el-input和el-dropdown 差异对比)

安装 注意 ElementPlus 的 Icon 图标 要额外安装插件 element-plus/icons-vue. npm install element-plus/icons-vue注册 全局注册 定义一个文件 element-icon.js &#xff0c;注意代码第 6 行。加上了前缀 ElIcon &#xff0c;避免组件命名重复&#xff0c;且易于理解为 e…

人工智能迷惑行为大赏!

目录 人工智能迷惑行为大赏 一&#xff1a;人工智能的“幽默”瞬间 1. 图像识别出现AI的极限 2. 小批量梯度下降优化器 3. 智能聊天机器人的冰雹问题 4. 大语言模型-3经典语录 二&#xff1a;技术原理探究 1. 深度学习 2. 机器学习 3. 自然语言处理 4. 计算机视觉 三…

Sui与数据平台ZettaBlock达成合作,为其公测提供数据

Sui一向以闪电般的速度、无限水平扩展著称&#xff0c;现已迅速成为DeFi活动的重要场所。近期&#xff0c;数据平台ZettaBlock宣布在其开创性的Web3数据平台发布中&#xff0c;选择Sui作为基础集成合作伙伴之一。在ZettaBlock的开放测试版发布之际&#xff0c;构建者和开发者将…

高精度三维扫描测量服务3d扫描仪抄数工业级精密激光扫描建模设计

在工业设计与制造领域&#xff0c;工业3D扫描仪的应用日益广泛&#xff0c;其“抄数设计”的功能更是备受瞩目。抄数设计&#xff0c;简单来说&#xff0c;就是通过3D扫描仪对实物进行精确测量&#xff0c;快速获取其三维数据&#xff0c;并基于这些数据进行设计、分析和优化。…

免费视频背景素材下载

找免费视频素材、背景就上这6个网站&#xff0c;高质量&#xff0c;无版权可商用。 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYwNDUx 菜鸟图库虽然是个设计素材网站&#xff0c;但除了设计类素材之外还有很多视频、音频、办公类等素材&#xff0c;视频素材就有上…

【Power Apps】响应式布局与布局容器

做响应式布局之前要先把这里关掉呦。 这里我可能要先简单说一下什么是响应式布局&#xff0c;说白了就是咱们做出来的应用的界面可以根据当前窗口的大小来自适应地调整内部组件的大小、位置等属性&#xff0c;这样我们只需要做一套页面&#xff0c;就可以既在桌面端使用&#x…

【20240309】WORD宏设置批量修改全部表格格式

WORD宏设置批量修改全部表格格式 引言1. 设置表格文字样式2. 设置表格边框样式3. 设置所有表格边框样式为075pt4. 删除行参考 引言 这两周已经彻底变为office工程师了&#xff0c;更准确一点应该是Word工程师&#xff0c;一篇文档动不动就成百上千页&#xff0c;表格图片也是上…

SAR ADC学习笔记(5)

高精度比较器 一、基于开环运放的比较器 OP开环应用时不需要考虑频率特性(相位裕度那些) &#xff0c;不存在稳定性问题。 单级运放的时域响应 多级运放 二、Latch比较器 Latch比较器的速度 Latch比较器的噪声优化 三、高速高精度比较器 消除失调电压OFFSET OOS IOS