最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Redis 實(shí)現(xiàn)網(wǎng)站PV/UV數(shù)據(jù)統(tǒng)計(jì)

 更新時(shí)間:2025年04月22日 09:26:47   作者:·云揚(yáng)·  
PV和UV是兩個(gè)重要的指標(biāo),本文主要介紹了基于Redis 實(shí)現(xiàn)網(wǎng)站PV/UV數(shù)據(jù)統(tǒng)計(jì),具有一定的參考價(jià)值,感興趣的可以了解一下

在網(wǎng)站的數(shù)據(jù)分析中,PV(Page View,頁(yè)面瀏覽量)和 UV(Unique Visitor,獨(dú)立訪客數(shù))是兩個(gè)重要的指標(biāo),幾乎每個(gè)網(wǎng)站都需要對(duì)其進(jìn)行統(tǒng)計(jì)。市面上有很多成熟的統(tǒng)計(jì)產(chǎn)品,例如百度的站點(diǎn)統(tǒng)計(jì)功能,而本文將介紹如何借助 Redis 的計(jì)數(shù)器功能,實(shí)現(xiàn)一套屬于自己的站點(diǎn)統(tǒng)計(jì)服務(wù)。

1 方案設(shè)計(jì)

1.1 術(shù)語(yǔ)說(shuō)明

在我們的實(shí)際實(shí)現(xiàn)中,對(duì) PV 和 UV 的定義與標(biāo)準(zhǔn)定義存在一定差異:

  • PV(Page View):指的是每個(gè)頁(yè)面的訪問(wèn)次數(shù)。在本服務(wù)中,PV 是總量概念,一個(gè)獨(dú)立的 IP 每訪問(wèn)一次 URL,對(duì)應(yīng)的訪問(wèn)計(jì)數(shù)就加 1。我們希望按自然日統(tǒng)計(jì)每個(gè) URL 的訪問(wèn)計(jì)數(shù),同時(shí)也能統(tǒng)計(jì)總的訪問(wèn)計(jì)數(shù),以此判斷哪些頁(yè)面更受讀者喜愛(ài)。
  • UV(Unique Visitor):用于統(tǒng)計(jì) URI 的訪問(wèn) IP 數(shù),同樣按照自然日和總數(shù)進(jìn)行區(qū)分。

1.2 統(tǒng)計(jì)流程

用戶訪問(wèn)時(shí),首先獲取目標(biāo) IP,然后根據(jù)其訪問(wèn)情況更新對(duì)應(yīng)的計(jì)數(shù):

  • 首次訪問(wèn)目標(biāo)資源:總 PV 加 1,總 UV 加 1;當(dāng)天 PV 加 1,當(dāng)天 UV 加 1。
  • 非首次訪問(wèn),但為當(dāng)天第一次訪問(wèn):總 PV 加 1,總 UV 不變;當(dāng)天 PV 加 1,當(dāng)天 UV 加 1。
  • 當(dāng)天非首次訪問(wèn):總 PV 加 1,總 UV 不變;當(dāng)天 PV 加 1,當(dāng)天 UV 不變。

在這里插入圖片描述

1.3 數(shù)據(jù)結(jié)構(gòu)

我們使用 Redis 的 hash 來(lái)存儲(chǔ)訪問(wèn)信息,具體需要存儲(chǔ)以下三類(lèi)信息:

  • 站點(diǎn)的總訪問(wèn)信息:包括站點(diǎn)的 PV/UV,以及每個(gè) URI 的 PV/UV。
  • 某一天的訪問(wèn)信息:涵蓋某一天站點(diǎn)的總訪問(wèn) PV/UV,以及某一天每個(gè) URI 的 PV/UV。由于計(jì)算 UV 時(shí)需要存儲(chǔ)用戶是否訪問(wèn)過(guò)某個(gè)資源的信息,所以額外添加了存儲(chǔ)單元保存用戶訪問(wèn)歷史。
  • 用戶的訪問(wèn)信息:包含用戶訪問(wèn)站點(diǎn)的總次數(shù),以及訪問(wèn)每個(gè) URI 的總次數(shù)。用戶每天的訪問(wèn)信息存儲(chǔ)在每天的訪問(wèn)信息結(jié)構(gòu)中,因?yàn)槊刻斓脑L問(wèn)信息通常不需要持久化保存,比如只存儲(chǔ)最近一個(gè)月的情況,可設(shè)置 Redis 的有效期為 30 天,到期自動(dòng)清除。

完整的 hash 定義如下:

  • 站點(diǎn)總統(tǒng)計(jì) hash
    • key:visit_info
    • field:
      • pv:站點(diǎn)的總 PV
      • uv:站點(diǎn)的總 UV
      • pv_path:站點(diǎn)某個(gè)資源的總訪問(wèn) PV
      • uv_path:站點(diǎn)某個(gè)資源的總訪問(wèn) UV
  • 每天統(tǒng)計(jì) hash
    • key:visit_info_20230822(每日記錄,一天一條記錄)
    • field:
      • pv:12(field = 月日_pv,PV 的計(jì)數(shù))
      • uv:5(field = 月日_uv,UV 的計(jì)數(shù))
      • pv_path:2(資源的當(dāng)前訪問(wèn)計(jì)數(shù))
      • uv_path:資源的當(dāng)天訪問(wèn) UV
      • pv_ip:用戶當(dāng)天的訪問(wèn)次數(shù)
      • pv_path_ip:用戶對(duì)資源的當(dāng)天訪問(wèn)次數(shù)
  • 用戶訪問(wèn)統(tǒng)計(jì)
    • key:visit_info_ip
    • field:
      • pv:用戶訪問(wèn)的站點(diǎn)總次數(shù)
      • path_pv:用戶訪問(wèn)的路徑總次數(shù)

在這里插入圖片描述

2 實(shí)現(xiàn)方式

2.1 統(tǒng)計(jì)計(jì)數(shù)

核心計(jì)數(shù)的實(shí)現(xiàn)路徑為 com.github.paicoding.forum.service.sitemap.service.SitemapServiceImpl#saveVisitInfo。其原理是:用戶站點(diǎn)總 PV 加 1,若返回的最新計(jì)數(shù)是 1,表示是站點(diǎn)的新用戶,所有 UV 加 1;今日 PV 加 1,若返回的最新計(jì)數(shù)是 1,表示當(dāng)前用戶今日首次訪問(wèn),進(jìn)入的 UV 加 1 。

 /**
  * 保存站點(diǎn)數(shù)據(jù)模型
  * <p>
  * 站點(diǎn)統(tǒng)計(jì)hash:
  * - visit_info:
  * ---- pv: 站點(diǎn)的總pv
  * ---- uv: 站點(diǎn)的總uv
  * ---- pv_path: 站點(diǎn)某個(gè)資源的總訪問(wèn)pv
  * ---- uv_path: 站點(diǎn)某個(gè)資源的總訪問(wèn)uv
  * - visit_info_ip:
  * ---- pv: 用戶訪問(wèn)的站點(diǎn)總次數(shù)
  * ---- path_pv: 用戶訪問(wèn)的路徑總次數(shù)
  * - visit_info_20230822每日記錄, 一天一條記錄
  * ---- pv: 12  # field = 月日_pv, pv的計(jì)數(shù)
  * ---- uv: 5   # field = 月日_uv, uv的計(jì)數(shù)
  * ---- pv_path: 2 # 資源的當(dāng)前訪問(wèn)計(jì)數(shù)
  * ---- uv_path: # 資源的當(dāng)天訪問(wèn)uv
  * ---- pv_ip: # 用戶當(dāng)天的訪問(wèn)次數(shù)
  * ---- pv_path_ip: # 用戶對(duì)資源的當(dāng)天訪問(wèn)次數(shù)
  *
  * @param visitIp 訪問(wèn)者ip
  * @param path    訪問(wèn)的資源路徑
  */
 @Override
 public void saveVisitInfo(String visitIp, String path) {
     String globalKey = SitemapConstants.SITE_VISIT_KEY;
     String day = SitemapConstants.day(LocalDate.now());

     String todayKey = globalKey + "_" + day;

     // 用戶的全局訪問(wèn)計(jì)數(shù)+1
     Long globalUserVisitCnt = RedisClient.hIncr(globalKey + "_" + visitIp, "pv", 1);
     // 用戶的當(dāng)日訪問(wèn)計(jì)數(shù)+1
     Long todayUserVisitCnt = RedisClient.hIncr(todayKey, "pv_" + visitIp, 1);

     RedisClient.PipelineAction pipelineAction = RedisClient.pipelineAction();
     if (globalUserVisitCnt == 1) {
         // 站點(diǎn)新用戶
         // 今日的uv + 1
         pipelineAction.add(todayKey, "uv"
                 , (connection, key, field) -> {
                     connection.hIncrBy(key, field, 1);
                 });
         pipelineAction.add(todayKey, "uv_" + path
                 , (connection, key, field) -> connection.hIncrBy(key, field, 1));

         // 全局站點(diǎn)的uv
         pipelineAction.add(globalKey, "uv", (connection, key, field) -> connection.hIncrBy(key, field, 1));
         pipelineAction.add(globalKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));
     } else if (todayUserVisitCnt == 1) {
         // 判斷是今天的首次訪問(wèn),更新今天的uv+1
         pipelineAction.add(todayKey, "uv", (connection, key, field) -> connection.hIncrBy(key, field, 1));
         if (RedisClient.hIncr(todayKey, "pv_" + path + "_" + visitIp, 1) == 1) {
             // 判斷是否為今天首次訪問(wèn)這個(gè)資源,若是,則uv+1
             pipelineAction.add(todayKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));
         }

         // 判斷是否是用戶的首次訪問(wèn)這個(gè)path,若是,則全局的path uv計(jì)數(shù)需要+1
         if (RedisClient.hIncr(globalKey + "_" + visitIp, "pv_" + path, 1) == 1) {
             pipelineAction.add(globalKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));
         }
     }


     // 更新pv 以及 用戶的path訪問(wèn)信息
     // 今天的相關(guān)信息 pv
     pipelineAction.add(todayKey, "pv", (connection, key, field) -> connection.hIncrBy(key, field, 1));
     pipelineAction.add(todayKey, "pv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));
     if (todayUserVisitCnt > 1) {
         // 非當(dāng)天首次訪問(wèn),則pv+1; 因?yàn)槭状卧L問(wèn)時(shí),在前面更新uv時(shí),已經(jīng)計(jì)數(shù)+1了
         pipelineAction.add(todayKey, "pv_" + path + "_" + visitIp, (connection, key, field) -> connection.hIncrBy(key, field, 1));
     }


     // 全局的 PV
     pipelineAction.add(globalKey, "pv", (connection, key, field) -> connection.hIncrBy(key, field, 1));
     pipelineAction.add(globalKey, "pv" + "_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));

     // 保存訪問(wèn)信息
     pipelineAction.execute();
     if (log.isDebugEnabled()) {
         log.info("用戶訪問(wèn)信息更新完成! 當(dāng)前用戶總訪問(wèn): {},今日訪問(wèn): {}", globalUserVisitCnt, todayUserVisitCnt);
     }
 }

2.2 Redis 管道封裝

Redis 管道技術(shù)允許在服務(wù)端未響應(yīng)時(shí),客戶端繼續(xù)向服務(wù)端發(fā)送請(qǐng)求,并最終一次性讀取所有服務(wù)端的響應(yīng),從而實(shí)現(xiàn)批量操作。通過(guò)對(duì) Redis pipeline 使用姿勢(shì)的封裝,簡(jiǎn)化了調(diào)用過(guò)程,例如 com.github.paicoding.forum.core.cache.RedisClient.PipelineAction 中的相關(guān)代碼:

/**
 * redis 管道執(zhí)行的封裝鏈路
 */
public static class PipelineAction {
    private List<Runnable> run = new ArrayList<>();

    private RedisConnection connection;

    public PipelineAction add(String key, BiConsumer<RedisConnection, byte[]> conn) {
        run.add(() -> conn.accept(connection, RedisClient.keyBytes(key)));
        return this;
    }

    public PipelineAction add(String key, String field, ThreeConsumer<RedisConnection, byte[], byte[]> conn) {
        run.add(() -> conn.accept(connection, RedisClient.keyBytes(key), valBytes(field)));
        return this;
    }

    public void execute() {
        template.executePipelined((RedisCallback<Object>) connection -> {
            PipelineAction.this.connection = connection;
            run.forEach(Runnable::run);
            return null;
        });
    }
}

@FunctionalInterface
public interface ThreeConsumer<T, U, P> {
    void accept(T t, U u, P p);
}

2.3 計(jì)數(shù)更新與使用

PV/UV 的更新可以在 Filter 中統(tǒng)一調(diào)用,為避免計(jì)數(shù)影響實(shí)際業(yè)務(wù)操作,采用異步更新策略:com.github.paicoding.forum.web.hook.filter.ReqRecordFilter#initReqInfo

private HttpServletRequest initReqInfo(HttpServletRequest request, HttpServletResponse response) {
    if (isStaticURI(request)) {
        // 靜態(tài)資源直接放行
        return request;
    }

    StopWatch stopWatch = new StopWatch("請(qǐng)求參數(shù)構(gòu)建");
    try {
        stopWatch.start("traceId");
        // 添加全鏈路的traceId
        MdcUtil.addTraceId();
        stopWatch.stop();

        stopWatch.start("請(qǐng)求基本信息");
        // 手動(dòng)寫(xiě)入一個(gè)session,借助 OnlineUserCountListener 實(shí)現(xiàn)在線人數(shù)實(shí)時(shí)統(tǒng)計(jì)
        request.getSession().setAttribute("latestVisit", System.currentTimeMillis());

        ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();
        reqInfo.setHost(request.getHeader("host"));
        reqInfo.setPath(request.getPathInfo());
        if (reqInfo.getPath() == null) {
            String url = request.getRequestURI();
            int index = url.indexOf("?");
            if (index > 0) {
                url = url.substring(0, index);
            }
            reqInfo.setPath(url);
        }
        reqInfo.setReferer(request.getHeader("referer"));
        reqInfo.setClientIp(IpUtil.getClientIp(request));
        reqInfo.setUserAgent(request.getHeader("User-Agent"));
        reqInfo.setDeviceId(getOrInitDeviceId(request, response));

        request = this.wrapperRequest(request, reqInfo);
        stopWatch.stop();

        stopWatch.start("登錄用戶信息");
        // 初始化登錄信息
        globalInitService.initLoginUser(reqInfo);
        stopWatch.stop();

        ReqInfoContext.addReqInfo(reqInfo);
        stopWatch.start("pv/uv站點(diǎn)統(tǒng)計(jì)");
        // 更新uv/pv計(jì)數(shù)
        AsyncUtil.execute(() -> SpringUtil.getBean(SitemapServiceImpl.class).saveVisitInfo(reqInfo.getClientIp(), reqInfo.getPath()));
        stopWatch.stop();

        stopWatch.start("回寫(xiě)traceId");
        // 返回頭中記錄traceId
        response.setHeader(GLOBAL_TRACE_ID_HEADER, Optional.ofNullable(MdcUtil.getTraceId()).orElse(""));
        stopWatch.stop();
    } catch (Exception e) {
        log.error("init reqInfo error!", e);
    } finally {
        if (!EnvUtil.isPro()) {
            log.info("{} -> 請(qǐng)求構(gòu)建耗時(shí): \n{}", request.getRequestURI(), stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
        }
    }

    return request;
}

目前站點(diǎn)的統(tǒng)計(jì)信息在前臺(tái)只顯示全局站點(diǎn)的統(tǒng)計(jì)情況,使用時(shí)直接從 hash 中獲取對(duì)應(yīng)的計(jì)數(shù)即可:com.github.paicoding.forum.service.sitemap.service.impl.SitemapServiceImpl#querySiteVisitInfo。

/**
 * 查詢站點(diǎn)某一天or總的訪問(wèn)信息
 *
 * @param date 日期,為空時(shí),表示查詢所有的站點(diǎn)信息
 * @param path 訪問(wèn)路徑,為空時(shí)表示查站點(diǎn)信息
 * @return
 */
@Override
public SiteCntVo querySiteVisitInfo(LocalDate date, String path) {
    String globalKey = SitemapConstants.SITE_VISIT_KEY;
    String day = null, todayKey = globalKey;
    if (date != null) {
        day = SitemapConstants.day(date);
        todayKey = globalKey + "_" + day;
    }

    String pvField = "pv", uvField = "uv";
    if (path != null) {
        // 表示查詢對(duì)應(yīng)路徑的訪問(wèn)信息
        pvField += "_" + path;
        uvField += "_" + path;
    }

    Map<String, Integer> map = RedisClient.hMGet(todayKey, Arrays.asList(pvField, uvField), Integer.class);
    SiteCntVo siteInfo = new SiteCntVo();
    siteInfo.setDay(day);
    siteInfo.setPv(map.getOrDefault(pvField, 0));
    siteInfo.setUv(map.getOrDefault(uvField, 0));
    return siteInfo;
}

前臺(tái)使用路徑:

在這里插入圖片描述

3 小結(jié)

在這里插入圖片描述

基于 Redis 實(shí)現(xiàn) PV/UV 統(tǒng)計(jì)主要依靠?jī)蓚€(gè)關(guān)鍵知識(shí)點(diǎn):

  • hash: incr:利用 Redis 的 hash 結(jié)構(gòu)結(jié)合 incr 命令實(shí)現(xiàn)原子計(jì)數(shù)。
  • pipeline:通過(guò)管道方式實(shí)現(xiàn)批量操作,提高操作效率。

最后提出一個(gè)思考問(wèn)題:當(dāng)站點(diǎn)訪問(wèn)量劇增,一天達(dá)到幾百萬(wàn)的訪問(wèn)量時(shí),通過(guò)記錄 IP 來(lái)實(shí)現(xiàn) UV 計(jì)數(shù)會(huì)導(dǎo)致用戶訪問(wèn)記錄存儲(chǔ)開(kāi)銷(xiāo)巨大,此時(shí)可以考慮使用 Redis 中的 HyperLoglog 來(lái)解決這一問(wèn)題,它利用數(shù)學(xué)上的概率統(tǒng)計(jì)分布原理,能在空間復(fù)雜度較低的情況下實(shí)現(xiàn)近似的計(jì)數(shù)統(tǒng)計(jì)。

到此這篇關(guān)于基于Redis 實(shí)現(xiàn)網(wǎng)站PV/UV數(shù)據(jù)統(tǒng)計(jì)的文章就介紹到這了,更多相關(guān)Redis  PV/UV數(shù)據(jù)統(tǒng)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳解Redis中的雙鏈表結(jié)構(gòu)

    詳解Redis中的雙鏈表結(jié)構(gòu)

    這篇文章主要介紹了Redis中的雙鏈表結(jié)構(gòu),包括listNode結(jié)構(gòu)的API,需要的朋友可以參考下
    2015-08-08
  • 解鎖redis鎖的正確姿勢(shì)

    解鎖redis鎖的正確姿勢(shì)

    這篇文章主要為大家詳細(xì)介紹了解鎖redis鎖的正確姿勢(shì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-03-03
  • Redis延遲隊(duì)列和分布式延遲隊(duì)列的簡(jiǎn)答實(shí)現(xiàn)

    Redis延遲隊(duì)列和分布式延遲隊(duì)列的簡(jiǎn)答實(shí)現(xiàn)

    在我們的工作中,很多地方使用延遲隊(duì)列,比如訂單到期沒(méi)有付款取消訂單,制訂一個(gè)提醒的任務(wù)等都需要延遲隊(duì)列,那么我們需要實(shí)現(xiàn)延遲隊(duì)列,本文就來(lái)介紹一下如何實(shí)現(xiàn),感興趣的可以了解一下
    2021-05-05
  • Redis KEYS查詢大批量數(shù)據(jù)替代方案

    Redis KEYS查詢大批量數(shù)據(jù)替代方案

    在使用 Redis 時(shí),KEYS 命令雖然簡(jiǎn)單直接,但其全表掃描的特性在處理大規(guī)模數(shù)據(jù)時(shí)會(huì)導(dǎo)致性能問(wèn)題,甚至可能阻塞 Redis 服務(wù),本文將介紹SCAN命令、有序集合、哈希表和RediSearch模塊四種替代 KEYS 的高效方案,需要的朋友可以參考下
    2024-12-12
  • Redis實(shí)現(xiàn)編碼生成規(guī)則方式

    Redis實(shí)現(xiàn)編碼生成規(guī)則方式

    在自動(dòng)生成編碼時(shí)應(yīng)采用“MD+年月日+4位序列號(hào)”的規(guī)則,如“MD202310130001”,為避免使用隨機(jī)序列號(hào)導(dǎo)致的重復(fù)編碼,建議使用從0開(kāi)始的自增序列號(hào),此外,使用Redis的incrBy功能實(shí)現(xiàn)序列號(hào)自增,可以有效提高效率和降低實(shí)現(xiàn)難度
    2023-01-01
  • Redis序列化反序列化不一致導(dǎo)致String類(lèi)型值多了雙引號(hào)問(wèn)題

    Redis序列化反序列化不一致導(dǎo)致String類(lèi)型值多了雙引號(hào)問(wèn)題

    這篇文章主要介紹了Redis序列化反序列化不一致導(dǎo)致String類(lèi)型值多了雙引號(hào)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • 使用注解實(shí)現(xiàn)Redis緩存功能

    使用注解實(shí)現(xiàn)Redis緩存功能

    這篇文章主要為大家詳細(xì)介紹了使用注解實(shí)現(xiàn)Redis緩存功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-07-07
  • YII2框架手動(dòng)安裝Redis擴(kuò)展的過(guò)程

    YII2框架手動(dòng)安裝Redis擴(kuò)展的過(guò)程

    這篇文章主要介紹了YII2框架手動(dòng)安裝Redis擴(kuò)展的過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-06-06
  • Redis中對(duì)大Key進(jìn)行處理方式

    Redis中對(duì)大Key進(jìn)行處理方式

    這篇文章主要介紹了Redis中對(duì)大Key進(jìn)行處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-06-06
  • 詳解Redis?鍵和字符串常用命令

    詳解Redis?鍵和字符串常用命令

    字符串是?Redis?最基本的數(shù)據(jù)結(jié)構(gòu),它將以一個(gè)鍵?和一個(gè)值?儲(chǔ)存在?Redis?內(nèi)部,本文重點(diǎn)給大家介紹Redis鍵和字符串常用命令,感興趣的朋友一起看看吧
    2022-02-02

最新評(píng)論

磴口县| 新昌县| 鹤庆县| 靖宇县| 连山| 远安县| 新乐市| 许昌市| 且末县| 天门市| 邢台县| 邹城市| 山阳县| 哈尔滨市| 彩票| 左云县| 安达市| 孝感市| 乐至县| 吉木萨尔县| 渑池县| 万州区| 小金县| 开平市| 阿合奇县| 龙南县| 临武县| 松潘县| 宜章县| 绥阳县| 高州市| 田阳县| 田林县| 上犹县| 杭锦后旗| 伊金霍洛旗| 萍乡市| 太和县| 邯郸市| 石景山区| 迭部县|