Redis如何實(shí)現(xiàn)計(jì)數(shù)統(tǒng)計(jì)
介紹
計(jì)數(shù)器大量應(yīng)用于互聯(lián)網(wǎng)上大大小小的項(xiàng)目,你可以在很多場(chǎng)景都能找到計(jì)數(shù)器的應(yīng)用范疇,單純以技術(shù)派項(xiàng)目為例,也有相當(dāng)多的地方會(huì)有計(jì)數(shù)相關(guān)的訴求,比如
- 文章帶贊數(shù)
- 收藏?cái)?shù)
- 評(píng)論數(shù)
- 用戶粉絲數(shù)
- ......
技術(shù)派中有兩種查詢計(jì)數(shù)相關(guān)的方案,一個(gè)是基于db中的操作記錄進(jìn)行實(shí)施,一種是基于redis的incr特性來(lái)實(shí)現(xiàn)計(jì)數(shù)器
下面來(lái)看一下,redis的計(jì)數(shù)器是怎樣用于技術(shù)派的技術(shù)場(chǎng)景的
計(jì)數(shù)的業(yè)務(wù)場(chǎng)景
首先我們看一下技術(shù)派中使用到的計(jì)數(shù)器的場(chǎng)景,主要有兩大類(業(yè)務(wù)計(jì)數(shù)+pv/uv),三個(gè)細(xì)分領(lǐng)域(用戶、文章、站點(diǎn))
用戶的相關(guān)統(tǒng)計(jì)信息
- 文章數(shù),文章總閱讀數(shù),粉絲數(shù),關(guān)注作者數(shù),文章被收藏?cái)?shù)、被點(diǎn)贊數(shù)量

站點(diǎn)的pv/uv等統(tǒng)計(jì)信息
- 網(wǎng)站的總pv/uv,某一天的pv/uv
- 某個(gè)uri的pv/uv

注意上面的幾個(gè)場(chǎng)景,這里主要介紹redis計(jì)數(shù)器的使用
那用戶與文章的相關(guān)統(tǒng)計(jì)將是我們的重點(diǎn),因?yàn)檫@兩個(gè)的業(yè)務(wù)屬性很相似,因此我們選擇一個(gè)重點(diǎn),以用戶統(tǒng)計(jì)來(lái)實(shí)現(xiàn)。
redis計(jì)數(shù)器
redis計(jì)數(shù)器,主要是借助原生的incr指令來(lái)實(shí)現(xiàn)原子的+1-1操作,更棒的是不僅redis的string數(shù)據(jù)結(jié)構(gòu)支持incr,hash、zset數(shù)據(jù)結(jié)構(gòu)同樣也是支持incr的
1.incr指令
Redis incr命令將key中存儲(chǔ)的數(shù)字值增值一。
- 如果key不存在,那么key的值會(huì)先被初始化為0,然后在執(zhí)行INCR操作。
- 如果值包含錯(cuò)誤類型,或者字符串類型的值不能表示為數(shù)字,那么返回一個(gè)錯(cuò)誤。
- 本操作的值限制在64位有符號(hào)數(shù)字表示之內(nèi)。
接下來(lái)看項(xiàng)目封裝實(shí)現(xiàn)
/**
* 自增
*
* @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.用戶計(jì)數(shù)統(tǒng)計(jì)
我們將用戶的相關(guān)計(jì)數(shù),每個(gè)用戶對(duì)應(yīng)一個(gè)hash數(shù)據(jù)結(jié)構(gòu)
key: user_statistic_${userId}
filed:
- follCount: 關(guān)注數(shù)
- fansCount: 粉絲數(shù)
- articleCount: 已發(fā)布文章數(shù)
- praiseCount: 文章點(diǎn)贊數(shù)
- readCount: 文章被閱讀數(shù)
- collectionCount: 文章被收藏?cái)?shù)
計(jì)數(shù)器的核心就在于滿足條件之后,實(shí)現(xiàn)的計(jì)數(shù) + 1 / -1
通常的業(yè)務(wù)場(chǎng)景中,此類計(jì)數(shù)不太建議直接與業(yè)務(wù)代碼強(qiáng)耦合,舉個(gè)例子
用戶收藏了一篇文章,若按照正常的設(shè)計(jì),就是在收藏這里,帶哦用計(jì)數(shù)器執(zhí)行 + 1 操作
上面這樣實(shí)現(xiàn)有問(wèn)題嗎?
顯然是沒(méi)有額問(wèn)題的,但是不夠好,不夠優(yōu)雅。
比如現(xiàn)在技術(shù)派的場(chǎng)景中,點(diǎn)贊之后,除了計(jì)數(shù)器更新之外,還有前面用戶說(shuō)到的用戶活躍度更新,若所有的邏輯都放在業(yè)務(wù)中,會(huì)導(dǎo)致業(yè)務(wù)的耦合較重
技術(shù)派選擇消息機(jī)制來(lái)應(yīng)對(duì)這種場(chǎng)景(大一點(diǎn)的項(xiàng)目會(huì)設(shè)計(jì)自己額的消息總線,為了讓各自的業(yè)務(wù)邏輯內(nèi)聚,向外拋出自己額的狀態(tài)/業(yè)務(wù)變更消息,實(shí)現(xiàn)解耦)
對(duì)映的,計(jì)數(shù)實(shí)現(xiàn)邏輯在。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;
/**
* 用戶活躍相關(guān)的消息監(jiān)聽(tīng)器
*
* @author YiHui
* @date 2023/8/19
*/
@Component
public class UserStatisticEventListener {
@Resource
private ArticleDao articleDao;
/**
* 用戶操作行為,增加對(duì)應(yīng)的積分
*這段代碼是一個(gè)使用Spring框架的事件監(jiān)聽(tīng)器注解。
* 它使用了@EventListener注解來(lái)指定要監(jiān)聽(tīng)的事件類型為NotifyMsgEvent.class,并且使用了@Async注解來(lái)表示該方法是異步執(zhí)行的。
*
* 當(dāng)NotifyMsgEvent事件被發(fā)布時(shí),該事件監(jiān)聽(tīng)器方法將被自動(dòng)調(diào)用。由于使用了@Async注解,
* 該方法將在單獨(dú)的線程中異步執(zhí)行,不會(huì)阻塞主線程。
* @param msgEvent
*/
@EventListener(classes = NotifyMsgEvent.class)
@Async
public void notifyMsgListener(NotifyMsgEvent msgEvent) {
switch (msgEvent.getNotifyType()) {
//評(píng)論/回復(fù)
case COMMENT:
case REPLY:
CommentDO comment = (CommentDO) msgEvent.getContent();
RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + comment.getArticleId(), CountConstants.COMMENT_COUNT, 1);
break;
//刪除評(píng)論/回復(fù)
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;
//點(diǎn)贊
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;
//取消點(diǎn)贊
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();
// 主用戶粉絲數(shù) + 1
RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getUserId(), CountConstants.FANS_COUNT, 1);
// 粉絲的關(guān)注數(shù) + 1
RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getFollowUserId(), CountConstants.FOLLOW_COUNT, 1);
break;
case CANCEL_FOLLOW:
relation = (UserRelationDO) msgEvent.getContent();
// 主用戶粉絲數(shù) + 1
RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getUserId(), CountConstants.FANS_COUNT, -1);
// 粉絲的關(guān)注數(shù) + 1
RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getFollowUserId(), CountConstants.FOLLOW_COUNT, -1);
break;
default:
}
}
/**
* 發(fā)布文章,更新對(duì)應(yīng)的文章計(jì)數(shù)
*
* @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);
}
}
}上面直接基于當(dāng)下技術(shù)派拋出的各種消息事件,來(lái)實(shí)現(xiàn)用戶/文章對(duì)應(yīng)計(jì)數(shù)變更
不一樣的地方則在于用戶的文章數(shù)統(tǒng)計(jì),因?yàn)橄l(fā)布時(shí),并沒(méi)有告知這個(gè)文章是 從 未上線狀態(tài)到發(fā)布, 發(fā)布到下線/刪除 ,因此無(wú)法進(jìn)行+1 -1。我們直接采用的是全量的更新策略。
注:
全量更新策略指的是**在數(shù)據(jù)同步或更新過(guò)程中,每次都對(duì)整個(gè)數(shù)據(jù)集進(jìn)行處理,而不是只更新發(fā)生變化的部分**。
這種策略的優(yōu)點(diǎn)包括:
- **簡(jiǎn)單直觀**:由于不需要考慮數(shù)據(jù)的增量變化,因此實(shí)現(xiàn)起來(lái)相對(duì)簡(jiǎn)單,易于理解和操作。
- **數(shù)據(jù)一致性**:每次全量更新可以確保目標(biāo)系統(tǒng)中的數(shù)據(jù)與源系統(tǒng)保持完全一致,避免了因部分更新而導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。然而,全量更新策略也存在一些缺點(diǎn):
- **資源消耗大**:當(dāng)數(shù)據(jù)量龐大或者更新頻率較高時(shí),全量更新可能會(huì)占用大量的網(wǎng)絡(luò)帶寬和存儲(chǔ)資源,導(dǎo)致效率低下。
- **系統(tǒng)壓力大**:頻繁的全量更新可能會(huì)給系統(tǒng)帶來(lái)較大的處理壓力,尤其是在數(shù)據(jù)量持續(xù)增長(zhǎng)的情況下,可能會(huì)超出系統(tǒng)的處理能力。此外,在某些情況下,全量更新策略可能不是最佳選擇。例如,在數(shù)據(jù)倉(cāng)庫(kù)中,如果源數(shù)據(jù)庫(kù)的數(shù)據(jù)量非常大,而且只有少量數(shù)據(jù)發(fā)生變更,使用全量更新策略就不如增量更新策略高效。增量更新策略只針對(duì)發(fā)生變化的數(shù)據(jù)進(jìn)行處理,這樣可以大大減少數(shù)據(jù)處理的工作量和系統(tǒng)資源的消耗。
總的來(lái)說(shuō),全量更新策略適用于數(shù)據(jù)量較小或更新頻率較低的場(chǎng)景,而在數(shù)據(jù)量大且更新頻繁的環(huán)境中,可能需要考慮其他更高效的數(shù)據(jù)更新策略。在實(shí)際應(yīng)用中,應(yīng)根據(jù)具體的業(yè)務(wù)需求和系統(tǒng)條件來(lái)選擇合適的更新策略。
3.用戶統(tǒng)計(jì)信息查詢
前面實(shí)現(xiàn)了用戶的相關(guān)統(tǒng)計(jì)數(shù),查詢用戶的統(tǒng)計(jì)信息則相對(duì)簡(jiǎn)單了,直接hgetall即可。

4.緩存一致性
基本上到上面,一個(gè)完整的計(jì)數(shù)服務(wù)就已經(jīng)成型了,但是我們?cè)趯?shí)際的生產(chǎn)服務(wù)中,再自信的人也不保證它沒(méi)問(wèn)題100分。
通常我們會(huì)做一個(gè)校對(duì)/定時(shí)同步任務(wù)來(lái)保證緩存與實(shí)際數(shù)據(jù)中的一致性
技術(shù)派中選擇簡(jiǎn)單的定時(shí)同步方案來(lái)實(shí)現(xiàn)
- 用戶統(tǒng)計(jì)信息每天全量同步

- 文章統(tǒng)計(jì)信息每天全量同步

總結(jié)
基于redis的incr ,很容易就可以實(shí)現(xiàn)計(jì)數(shù)相關(guān)的需求支撐,但是為啥我們要用redis來(lái)實(shí)現(xiàn)一個(gè)計(jì)數(shù)器呢?直接用數(shù)據(jù)庫(kù)的原始數(shù)據(jù)進(jìn)行統(tǒng)計(jì)有什么問(wèn)題嗎?
通常而言,項(xiàng)目初期,或者項(xiàng)目本身非常簡(jiǎn)單,訪問(wèn)量低,只希望快速上線支撐業(yè)務(wù)時(shí),使用db進(jìn)行統(tǒng)計(jì)即可,優(yōu)勢(shì)時(shí)簡(jiǎn)單,敘述,不容易出問(wèn)題;缺點(diǎn)則是每次都是實(shí)時(shí)統(tǒng)計(jì)性能差,擴(kuò)展性不強(qiáng)。
當(dāng)我們項(xiàng)目發(fā)展起來(lái),借助redis直接存儲(chǔ)最終結(jié)果。再展示層直接俄獲取即可,性能更強(qiáng),滿足高并發(fā),缺點(diǎn)是數(shù)據(jù)的一致性保障難度高。先選擇一個(gè)實(shí)現(xiàn)代價(jià)小的,再重構(gòu)哈啊哈哈。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用高斯Redis實(shí)現(xiàn)二級(jí)索引的方法
本文介紹了如何通過(guò)高斯Redis搭建二級(jí)索引,二級(jí)索引在電商、圖(hexastore)、游戲等領(lǐng)域具有廣泛的應(yīng)用場(chǎng)景,高斯redis現(xiàn)網(wǎng)亦有很多類似應(yīng)用,需要的朋友跟隨小編一起看看吧2022-07-07
基于Redis實(shí)現(xiàn)雙加密Token的示例代碼
在現(xiàn)代分布式系統(tǒng)中,Token管理是身份驗(yàn)證和授權(quán)的核心部分,本文將深入分析一個(gè)基于Redis的Token管理實(shí)現(xiàn),探討其設(shè)計(jì)思路、關(guān)鍵代碼邏輯以及實(shí)現(xiàn)細(xì)節(jié),通過(guò)對(duì)源碼的逐層剖析,幫助讀者更好地理解Token管理的實(shí)現(xiàn)原理,需要的朋友可以參考下2025-01-01
Redis跨主機(jī)連接超時(shí)問(wèn)題的解決方案
在微服務(wù)架構(gòu)中,服務(wù)間通信的穩(wěn)定性是系統(tǒng)可用性的重要保障,我們?cè)诮谝淮尉€上排查中,遇到了一個(gè) Redis 跨主機(jī)連接頻繁超時(shí)的問(wèn)題,所以本文給大家分享一下Redis跨主機(jī)連接超時(shí)問(wèn)題的解決方案,需要的朋友可以參考下2025-09-09
Redis事務(wù)機(jī)制與Springboot項(xiàng)目中的使用方式
Redis事務(wù)機(jī)制允許將多個(gè)命令打包在一起,作為一個(gè)原子操作來(lái)執(zhí)行,開(kāi)啟事務(wù)使用MULTI命令,執(zhí)行事務(wù)使用EXEC命令,取消事務(wù)使用DISCARD命令,監(jiān)視一個(gè)或多個(gè)鍵使用WATCH命令,Redis事務(wù)的核心思想是將多個(gè)命令放入一個(gè)隊(duì)列中2025-03-03
使用JMeter插件Redis Data Set如何實(shí)現(xiàn)高性能數(shù)據(jù)驅(qū)動(dòng)測(cè)試
RedisDataSet插件是JMeter的一個(gè)插件,可以實(shí)現(xiàn)從Redis中動(dòng)態(tài)加載數(shù)據(jù),并將其用作測(cè)試參數(shù),本文詳細(xì)介紹如何在JMeter中使用RedisDataSet插件,幫助你實(shí)現(xiàn)高效的數(shù)據(jù)驅(qū)動(dòng)測(cè)試2025-01-01
Redis數(shù)據(jù)庫(kù)中實(shí)現(xiàn)分布式鎖的方法
這篇文章主要介紹了Redis數(shù)據(jù)庫(kù)中實(shí)現(xiàn)分布式鎖的方法,Redis是一個(gè)高性能的主存式數(shù)據(jù)庫(kù),需要的朋友可以參考下2015-06-06

