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)景,主要有兩大類(lèi)(業(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ò)誤類(lèi)型,或者字符串類(lèi)型的值不能表示為數(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)景中,此類(lèi)計(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)的事件類(lèi)型為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存儲(chǔ)數(shù)據(jù)類(lèi)型及存取值方法
這篇文章主要介紹了淺談Redis存儲(chǔ)數(shù)據(jù)類(lèi)型及存取值方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
 CentOS 7下安裝 redis 3.0.6并配置集群的過(guò)程詳解
這篇文章主要給大家介紹了CentOS 7下安裝 redis 3.0.6并配置集群的過(guò)程,文中通過(guò)示例代碼和詳細(xì)的步驟介紹的很相信,對(duì)大家具有一定的參考價(jià)值,有需要的朋友們下面來(lái)一起看看吧。2017-01-01
 Redis 數(shù)據(jù)庫(kù)忘記密碼找回或重置的解決方法
對(duì)于 Redis 數(shù)據(jù)庫(kù),如果忘記了密碼,可以通過(guò)密碼重置來(lái)找回密碼,今天通過(guò)本文給大家分享Redis 數(shù)據(jù)庫(kù)忘記密碼找回或重置的解決方法,感興趣的朋友一起看看吧2024-01-01
 一文了解發(fā)現(xiàn)并解決Redis熱key與大key問(wèn)題
熱key是服務(wù)端的常見(jiàn)問(wèn)題,本文主要介紹Redis熱key與大key問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05
 關(guān)于Redis解決Session共享問(wèn)題
這篇文章主要介紹了Redis解決Session共享問(wèn)題,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
 redis.clients.jedis.exceptions.JedisBusyException無(wú)法處理異常的解決方法
redis.clients.jedis.exceptions.JedisBusyException異常通常不是 Jedis客戶端直接拋出的標(biāo)準(zhǔn)異常,本文就來(lái)介紹一下異常的解決方法,感興趣的可以了解一下2024-05-05

