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

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

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

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

1 方案設計

1.1 術語說明

在我們的實際實現(xiàn)中,對 PV 和 UV 的定義與標準定義存在一定差異:

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

1.2 統(tǒng)計流程

用戶訪問時,首先獲取目標 IP,然后根據(jù)其訪問情況更新對應的計數(shù):

  • 首次訪問目標資源:總 PV 加 1,總 UV 加 1;當天 PV 加 1,當天 UV 加 1。
  • 非首次訪問,但為當天第一次訪問:總 PV 加 1,總 UV 不變;當天 PV 加 1,當天 UV 加 1。
  • 當天非首次訪問:總 PV 加 1,總 UV 不變;當天 PV 加 1,當天 UV 不變。

在這里插入圖片描述

1.3 數(shù)據(jù)結構

我們使用 Redis 的 hash 來存儲訪問信息,具體需要存儲以下三類信息:

  • 站點的總訪問信息:包括站點的 PV/UV,以及每個 URI 的 PV/UV。
  • 某一天的訪問信息:涵蓋某一天站點的總訪問 PV/UV,以及某一天每個 URI 的 PV/UV。由于計算 UV 時需要存儲用戶是否訪問過某個資源的信息,所以額外添加了存儲單元保存用戶訪問歷史。
  • 用戶的訪問信息:包含用戶訪問站點的總次數(shù),以及訪問每個 URI 的總次數(shù)。用戶每天的訪問信息存儲在每天的訪問信息結構中,因為每天的訪問信息通常不需要持久化保存,比如只存儲最近一個月的情況,可設置 Redis 的有效期為 30 天,到期自動清除。

完整的 hash 定義如下:

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

在這里插入圖片描述

2 實現(xiàn)方式

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

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

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

     String todayKey = globalKey + "_" + day;

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

     RedisClient.PipelineAction pipelineAction = RedisClient.pipelineAction();
     if (globalUserVisitCnt == 1) {
         // 站點新用戶
         // 今日的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));

         // 全局站點的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) {
         // 判斷是今天的首次訪問,更新今天的uv+1
         pipelineAction.add(todayKey, "uv", (connection, key, field) -> connection.hIncrBy(key, field, 1));
         if (RedisClient.hIncr(todayKey, "pv_" + path + "_" + visitIp, 1) == 1) {
             // 判斷是否為今天首次訪問這個資源,若是,則uv+1
             pipelineAction.add(todayKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));
         }

         // 判斷是否是用戶的首次訪問這個path,若是,則全局的path uv計數(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訪問信息
     // 今天的相關信息 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) {
         // 非當天首次訪問,則pv+1; 因為首次訪問時,在前面更新uv時,已經(jīng)計數(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));

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

2.2 Redis 管道封裝

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

/**
 * 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 計數(shù)更新與使用

PV/UV 的更新可以在 Filter 中統(tǒng)一調用,為避免計數(shù)影響實際業(yè)務操作,采用異步更新策略: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("請求參數(shù)構建");
    try {
        stopWatch.start("traceId");
        // 添加全鏈路的traceId
        MdcUtil.addTraceId();
        stopWatch.stop();

        stopWatch.start("請求基本信息");
        // 手動寫入一個session,借助 OnlineUserCountListener 實現(xiàn)在線人數(shù)實時統(tǒng)計
        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站點統(tǒng)計");
        // 更新uv/pv計數(shù)
        AsyncUtil.execute(() -> SpringUtil.getBean(SitemapServiceImpl.class).saveVisitInfo(reqInfo.getClientIp(), reqInfo.getPath()));
        stopWatch.stop();

        stopWatch.start("回寫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("{} -> 請求構建耗時: \n{}", request.getRequestURI(), stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
        }
    }

    return request;
}

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

/**
 * 查詢站點某一天or總的訪問信息
 *
 * @param date 日期,為空時,表示查詢所有的站點信息
 * @param path 訪問路徑,為空時表示查站點信息
 * @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) {
        // 表示查詢對應路徑的訪問信息
        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;
}

前臺使用路徑:

在這里插入圖片描述

3 小結

在這里插入圖片描述

基于 Redis 實現(xiàn) PV/UV 統(tǒng)計主要依靠兩個關鍵知識點:

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

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

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

相關文章

  • 詳解Redis中的雙鏈表結構

    詳解Redis中的雙鏈表結構

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

    解鎖redis鎖的正確姿勢

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

    Redis延遲隊列和分布式延遲隊列的簡答實現(xiàn)

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

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

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

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

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

    Redis序列化反序列化不一致導致String類型值多了雙引號問題

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

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

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

    YII2框架手動安裝Redis擴展的過程

    這篇文章主要介紹了YII2框架手動安裝Redis擴展的過程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-06-06
  • Redis中對大Key進行處理方式

    Redis中對大Key進行處理方式

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

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

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

最新評論

杂多县| 崇礼县| 田东县| 卢龙县| 那曲县| 偏关县| 霸州市| 甘德县| 涟源市| 沂水县| 禹州市| 乌兰浩特市| 太白县| 宜君县| 鹤山市| 长葛市| 平安县| 肃宁县| 益阳市| 永宁县| 米泉市| 鹰潭市| 会昌县| 鹿泉市| 西林县| 和政县| 东丽区| 称多县| 津南区| 赤壁市| 容城县| 田阳县| 分宜县| 湖口县| 桓仁| 阿瓦提县| 辰溪县| 沛县| 安丘市| 都江堰市| 香格里拉县|