Redis计数器:数字的秘密

文章目录

      • Redis计数器
        • incr 指令
        • 用户计数统计
        • 用户统计信息查询
        • 缓存一致性
      • 小结

技术派项目源码地址 :

  • Gitee :技术派 - https://gitee.com/itwanger/paicoding
  • Github :技术派 - https://github.com/itwanger/paicoding

用户的相关统计信息

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

文章的相关统计信息

  • 文章点赞数,阅读数,收藏数,评论数

image.png

Redis计数器

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

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

  • 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
  • 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
  • 本操作的值限制在 64 位(bit)有符号数字表示之内。

接下来看下技术派的封装实现

/*** 自增** @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));
}
用户计数统计

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

  • key: user_statistic_${userId}

  • field:

    • followCount: 关注数
    • fansCount: 粉丝数
    • articleCount: 已发布文章数
    • praiseCount: 文章点赞数
    • readCount: 文章被阅读数
    • collectionCount: 文章被收藏数
  • 计数器的核心就在于满足计数条件之后,实现的计数+1/-1

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

  • 用户收藏了一个文章,若按照正常的设计,就是再收藏这里,调用计数器执行+1操作

  • 上面的这样实现有问题么?当然没有问题,但是不够优雅

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

  • 技术派选择消息机制来应对这种场景(扩展一下,为什么大一点的项目,会设计自己的消息总线呢?一个重要的目的就是各自业务逻辑内聚,向外只抛出自己的状态/业务变更消息,实现解耦)

  • 技术派写了如下监听器 :

/*** 用户操作行为,增加对应的积分** @param msgEvent*/
@EventListener(classes = NotifyMsgEvent.class)
@Async
public 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:}
}

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

/*** 发布文章,更新对应的文章计数** @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);}
}
用户统计信息查询
  • 前面实现了用户的相关计数统计,查询用户的统计信息则相对更简单了,直接hgetall即可
@Override
public UserStatisticInfoDTO queryUserStatisticInfo(Long userId) {Map<String, Integer> ans = RedisClient.hGetAll(CountConstants.USER_STATISTIC_INFO + userId, Integer.class);UserStatisticInfoDTO info = new UserStatisticInfoDTO();// 关注数info.setFollowCount(ans.getOrDefault(CountConstants.FOLLOW_COUNT, 0));// 文章数info.setArticleCount(ans.getOrDefault(CountConstants.ARTICLE_COUNT, 0));// 点赞数info.setPraiseCount(ans.getOrDefault(CountConstants.PRAISE_COUNT, 0));// 收藏数info.setCollectionCount(ans.getOrDefault(CountConstants.COLLECTION_COUNT, 0));// 阅读量info.setReadCount(ans.getOrDefault(CountConstants.READ_COUNT, 0));// 粉丝数info.setFansCount(ans.getOrDefault(CountConstants.FANS_COUNT, 0));return info;
}
缓存一致性
  • 通常我们会做一个校对/定时同步任务来保证缓存与实际数据中的一致性

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

/*** 每天4:15分执行定时任务,全量刷新用户的统计信息*/
@Scheduled(cron = "0 15 4 * * ?")
public void autoRefreshAllUserStatisticInfo() {Long now = System.currentTimeMillis();log.info("开始自动刷新用户统计信息");Long userId = 0L;// 批量处理的用户数,每次处理 20 个用户int batchSize = 20;while (true) {List<Long> userIds = userDao.scanUserId(userId, batchSize);userIds.forEach(this::refreshUserStatisticInfo);// 如果用户数小于 batchSize,说明已经处理完了,退出循环if (userIds.size() < batchSize) {userId = userIds.get(userIds.size() - 1);break;} else {userId = userIds.get(batchSize - 1);}}log.info("结束自动刷新用户统计信息,共耗时: {}ms, maxUserId: {}", System.currentTimeMillis() - now, userId);
}/*** 更新用户的统计信息** @param userId*/
@Override
public void refreshUserStatisticInfo(Long userId) {// 用户的文章点赞数,收藏数,阅读计数ArticleFootCountDTO count = userFootDao.countArticleByUserId(userId);if (count == null) {count = new ArticleFootCountDTO();}// 获取关注数Long followCount = userRelationDao.queryUserFollowCount(userId);// 粉丝数Long fansCount = userRelationDao.queryUserFansCount(userId);// 查询用户发布的文章数Integer articleNum = articleDao.countArticleByUser(userId);String key = CountConstants.USER_STATISTIC_INFO + userId;RedisClient.hMSet(key, MapUtils.create(CountConstants.PRAISE_COUNT, count.getPraiseCount(),CountConstants.COLLECTION_COUNT, count.getCollectionCount(),CountConstants.READ_COUNT, count.getReadCount(),CountConstants.FANS_COUNT, fansCount,CountConstants.FOLLOW_COUNT, followCount,CountConstants.ARTICLE_COUNT, articleNum));
}
  • 文章统计信息每天全量同步

image.png

public void refreshArticleStatisticInfo(Long articleId) {ArticleFootCountDTO res = userFootDao.countArticleByArticleId(articleId);if (res == null) {res = new ArticleFootCountDTO();} else {res.setCommentCount(commentReadService.queryCommentCount(articleId));}RedisClient.hMSet(CountConstants.ARTICLE_STATISTIC_INFO + articleId,MapUtils.create(CountConstants.COLLECTION_COUNT, res.getCollectionCount(),CountConstants.PRAISE_COUNT, res.getPraiseCount(),CountConstants.READ_COUNT, res.getReadCount(),CountConstants.COMMENT_COUNT, res.getCommentCount()));
}

小结

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

  2. 技术派的源码中,对于用户/文章的相关统计,同时给出了基于db计数 + redis计数两套方案

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

  4. 当我们项目发展起来之后,借助redis直接存储最终的结果,在展示层直接获取即可,性能更强,满足各位的高并发的遐想,缺点则是数据的一致性保障难度更高**

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

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

相关文章

go设计模式——单例模式

概念 单例是一种创建型设计模式&#xff0c;它确保一个类在整个程序运行期间只有一个实例&#xff0c;并提供一个全局访问点来使用该实例。虽然单例模式在某些情况下非常有用&#xff0c;例如管理全局配置、日志记录或资源共享&#xff0c;但它也带来了与全局变量相似的问题。…

redis面试(二十三)写锁释放

先加了写锁&#xff0c;后面再次加写锁或者读锁 anyLock: { “mode”: “write”, “UUID_01:threadId_01:write”: 2, “UUID_01:threadId_01”: 1 } 写锁的释放lua脚本在这里 RedissonWriteLock.unlockInnerAsync() 比如说现在的参数是这 KEYS[1] anyLock KEYS[2] redi…

SQL手工注入漏洞测试(MongoDB数据库)靶场通关攻略

构造数据回显 });return ({title:1,content:2 成功回显1,2&#xff0c;接下来我们开始尝试查询数据库 });return({title:tojson(db),content:2 得到之后我们就可以继续查询他的表名了 });return({title:tojson(db.getCollectionNames()),content:2 最后我们就可以爆出他表里的数…

宝塔面板配置FTP服务并安装内网穿透实现无公网IP远程连接

文章目录 前言1. Linux安装Cpolar2. 创建FTP公网地址3. 宝塔FTP服务设置4. FTP服务远程连接小结 5. 固定FTP公网地址6. 固定FTP地址连接 前言 本文主要介绍宝塔FTP文件传输服务如何搭配内网穿透工具&#xff0c;实现随时随地远程连接局域网环境搭建的宝塔FTP文件服务并进行文件…

ssrf实现.SSH未创建写shell

一、介绍SSRF漏洞 SSRF (Server-Side Request Forgery,服务器端请求伪造)是一种由攻击者构造请求&#xff0c;由服务端发起请求的安全漏洞。一般情况下&#xff0c;SSRF攻击的目标是外网无法访问的内部系统(正因为请求是由服务端发起的&#xff0c;所以服务端能请求到与自身相…

C语言基础——函数详解

目录 函数的概述 1 函数的概念 2 函数的意义 函数的定义和使用 1 函数的定义 2 函数的调用 2.1 在同一文件中函数定义后函数调用 2.2 在同一文件中函数定义前函数调用 2.3 调用其它文件中定义的函数 2.3.1 在函数调用文件中进行声明 2.3.2 在头文件中进行函数的声明 函…

图片工具箱:一键批量加水印,守护创意,提升效率!

前言 你是否曾在处理海量图片时&#xff0c;被繁琐的步骤和漫长的等待时间折磨得苦不堪言&#xff1f;是否梦想过拥有一款神器&#xff0c;能让你的图片处理工作变得轻松愉快&#xff0c;从此告别加班的烦恼&#xff0c;迎接升职加薪的曙光&#xff1f;那么&#xff0c;让我向…

有限差分学习笔记

有限差分介绍 ​ 在数学中&#xff0c;有限差分法&#xff08;finite-difference methods&#xff0c;简称FDM&#xff09;&#xff0c;是一种微分方程数值方法&#xff0c;是通过有限差分来近似导数&#xff0c;从而寻求微分方程的近似解。 由泰勒展开式的推导 显式方…

Web应用加密数据传输方案

目录 概述 最初的方案 改进后的方案 秘钥的过期时间 概述 介于公司最近发布了一个面向C端用户的Web系统&#xff0c;为防止前端调用后端的API接口时&#xff0c;数据传输的内容轻易的被黑客获取&#xff0c;而设计的一个前后端数据加密传输方案 最初的方案 在最开始&#xf…

2 种方式申请免费 SSL 证书,阿里云 Certbot

如何使用免费的 SSL 证书&#xff0c;有时在项目中需要使用免费的 SSL 证书&#xff0c;Aliyun 提供免费证书&#xff0c;三个月有效期&#xff0c;可以直接在aliyun 申请&#xff0c;搜索 SSL 证书&#xff0c;选择测试证书。 Aliyun 证书需要每三月来来换一次&#xff0c;页…

<数据集>车内视角行人识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;6470张 标注数量(xml文件个数)&#xff1a;6470 标注数量(txt文件个数)&#xff1a;6470 标注类别数&#xff1a;1 标注类别名称&#xff1a;[pedestrian] 序号类别名称图片数框数1pedestrian647029587 使用标注…

奔驰S迈巴赫S480升级动态按摩座椅效果怎么样

在迈巴赫 S480 的尊崇之旅中&#xff0c;舒适从未有尽头。现在&#xff0c;为您呈现前排动态按摩座椅的升级&#xff0c;将舒适体验提升至全新境界。 迈巴赫 S480 已然是舒适的代名词&#xff0c;但前排动态按摩座椅的升级&#xff0c;将为您带来前所未有的放松与享受。 当您…

网络UDP报文详细解析

目录 一、简介二、详细介绍三、其他相关链接1、TCP报文段的详细图总结2、TCP三次握手和四次挥手详解3、socket通信原理及相关函数详细总结4、网络包IP首部详细解析 一、简介 本文主要介绍UDP报文格式。 二、详细介绍 UDP是一种无连接、不可靠的用户数据报协议&#xff0c;其…

【注解】反序列化时匹配多个 JSON 属性名 @JsonAlias 详解

JsonAlias 注解是 Jackson 提供的一个功能强大的注解&#xff0c;允许一个字段在反序列化时匹配多个 JSON 属性名。它适用于在处理多种输入数据格式时&#xff0c;或当 JSON 数据的键名可能变化时。 一、JsonAlias 的作用 多种别名&#xff1a;JsonAlias 允许你为一个字段定义…

MySQL在Windows和Ubuntu上的安装与远程连接配置

MySQL是一个广泛使用的开源关系数据库管理系统&#xff0c;适用于各种操作系统。本文将详细介绍如何在Windows和Ubuntu系统上安装MySQL&#xff0c;并配置远程连接。 1. 在Windows上安装MySQL 1.1 下载MySQL安装包 首先&#xff0c;访问MySQL官方网站&#xff08;https://de…

UEFI 01记: 开发环境 在 ubuntu22 中搭建 edk2 开发环境并运行简单示例

https://uefi.org 1&#xff0c;预备环境 $ sudo apt install uuid-dev $ sudo apt install nasm $ sudo apt install bison flex $ sudo apt install build-essential $ sudo apt-get install x11proto-xext-dev $ sudo apt-get install libx11-dev $ sudo apt-get install l…

[数据集][目标检测]管道漏水泄漏破损检测数据集VOC+YOLO格式2614张4类

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2614 标注数量(xml文件个数)&#xff1a;2614 标注数量(txt文件个数)&#xff1a;2614 标注…

[windows][apache]Apache代理安装

下载apache服务软件和VC_redist安装包 https://www.apachelounge.com/download/ https://www.microsoft.com/zh-CN/download/details.aspx?id48145 解压文件&#xff0c;修改httpd.conf文件 37行出修改文件的解压目录 60行修改监听端口 安装服务 进入apache的目录&#xf…

美团mtgsig 1.2算法分析

声明 本文以教学为基准、本文提供的可操作性不得用于任何商业用途和违法违规场景。 本人对任何原因在使用本人中提供的代码和策略时可能对用户自己或他人造成的任何形式的损失和伤害不承担责任。 如有侵权,请联系我进行删除。 这里只是我分析的分析过程,以及一些重要点的记录…

帮助我们从曲线图中获取数据的软件分享——GetData Graph Digitizer

在科技论文写作和数据分析过程中&#xff0c;我们常常需要将自己的数据与前人的研究成果进行对比。然而&#xff0c;有时我们只能从别人的论文中获得一张包含坐标轴的曲线图&#xff0c;而无法直接获取原始数据。在这种情况下&#xff0c;GetData Graph Digitizer 软件就显得尤…