欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于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,頁面瀏覽量)和 UV(Unique Visitor,獨(dú)立訪客數(shù))是兩個(gè)重要的指標(biāo),幾乎每個(gè)網(wǎng)站都需要對其進(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ù)語說明

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

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

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

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

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

在這里插入圖片描述

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

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

  • 站點(diǎn)的總訪問信息:包括站點(diǎn)的 PV/UV,以及每個(gè) URI 的 PV/UV。
  • 某一天的訪問信息:涵蓋某一天站點(diǎn)的總訪問 PV/UV,以及某一天每個(gè) URI 的 PV/UV。由于計(jì)算 UV 時(shí)需要存儲(chǔ)用戶是否訪問過某個(gè)資源的信息,所以額外添加了存儲(chǔ)單元保存用戶訪問歷史。
  • 用戶的訪問信息:包含用戶訪問站點(diǎn)的總次數(shù),以及訪問每個(gè) URI 的總次數(shù)。用戶每天的訪問信息存儲(chǔ)在每天的訪問信息結(jié)構(gòu)中,因?yàn)槊刻斓脑L問信息通常不需要持久化保存,比如只存儲(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è)資源的總訪問 PV
      • uv_path:站點(diǎn)某個(gè)資源的總訪問 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)前訪問計(jì)數(shù))
      • uv_path:資源的當(dāng)天訪問 UV
      • pv_ip:用戶當(dāng)天的訪問次數(shù)
      • pv_path_ip:用戶對資源的當(dāng)天訪問次數(shù)
  • 用戶訪問統(tǒng)計(jì)
    • key:visit_info_ip
    • field:
      • pv:用戶訪問的站點(diǎn)總次數(shù)
      • path_pv:用戶訪問的路徑總次數(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)前用戶今日首次訪問,進(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è)資源的總訪問pv
  * ---- uv_path: 站點(diǎn)某個(gè)資源的總訪問uv
  * - visit_info_ip:
  * ---- pv: 用戶訪問的站點(diǎn)總次數(shù)
  * ---- path_pv: 用戶訪問的路徑總次數(shù)
  * - visit_info_20230822每日記錄, 一天一條記錄
  * ---- pv: 12  # field = 月日_pv, pv的計(jì)數(shù)
  * ---- uv: 5   # field = 月日_uv, uv的計(jì)數(shù)
  * ---- pv_path: 2 # 資源的當(dāng)前訪問計(jì)數(shù)
  * ---- uv_path: # 資源的當(dāng)天訪問uv
  * ---- pv_ip: # 用戶當(dāng)天的訪問次數(shù)
  * ---- pv_path_ip: # 用戶對資源的當(dāng)天訪問次數(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;

     // 用戶的全局訪問計(jì)數(shù)+1
     Long globalUserVisitCnt = RedisClient.hIncr(globalKey + "_" + visitIp, "pv", 1);
     // 用戶的當(dāng)日訪問計(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) {
         // 判斷是今天的首次訪問,更新今天的uv+1
         pipelineAction.add(todayKey, "uv", (connection, key, field) -> connection.hIncrBy(key, field, 1));
         if (RedisClient.hIncr(todayKey, "pv_" + path + "_" + visitIp, 1) == 1) {
             // 判斷是否為今天首次訪問這個(gè)資源,若是,則uv+1
             pipelineAction.add(todayKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1));
         }

         // 判斷是否是用戶的首次訪問這個(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訪問信息
     // 今天的相關(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)天首次訪問,則pv+1; 因?yàn)槭状卧L問時(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));

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

2.2 Redis 管道封裝

Redis 管道技術(shù)允許在服務(wù)端未響應(yīng)時(shí),客戶端繼續(xù)向服務(wù)端發(fā)送請求,并最終一次性讀取所有服務(wù)端的響應(yīng),從而實(shí)現(xiàn)批量操作。通過對 Redis pipeline 使用姿勢的封裝,簡化了調(diào)用過程,例如 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("請求參數(shù)構(gòu)建");
    try {
        stopWatch.start("traceId");
        // 添加全鏈路的traceId
        MdcUtil.addTraceId();
        stopWatch.stop();

        stopWatch.start("請求基本信息");
        // 手動(dòng)寫入一個(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("回寫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("{} -> 請求構(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 中獲取對應(yīng)的計(jì)數(shù)即可:com.github.paicoding.forum.service.sitemap.service.impl.SitemapServiceImpl#querySiteVisitInfo。

/**
 * 查詢站點(diǎn)某一天or總的訪問信息
 *
 * @param date 日期,為空時(shí),表示查詢所有的站點(diǎn)信息
 * @param path 訪問路徑,為空時(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) {
        // 表示查詢對應(yīng)路徑的訪問信息
        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ì)主要依靠兩個(gè)關(guān)鍵知識(shí)點(diǎn):

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

最后提出一個(gè)思考問題:當(dāng)站點(diǎn)訪問量劇增,一天達(dá)到幾百萬的訪問量時(shí),通過記錄 IP 來實(shí)現(xiàn) UV 計(jì)數(shù)會(huì)導(dǎo)致用戶訪問記錄存儲(chǔ)開銷巨大,此時(shí)可以考慮使用 Redis 中的 HyperLoglog 來解決這一問題,它利用數(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis中五種數(shù)據(jù)類型簡單操作

    Redis中五種數(shù)據(jù)類型簡單操作

    這篇文章主要介紹了Redis中五種數(shù)據(jù)類型簡單操作的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • RedisTemplate中boundHashOps的使用小結(jié)

    RedisTemplate中boundHashOps的使用小結(jié)

    redisTemplate.boundHashOps(key)?是 RedisTemplate 類的一個(gè)方法,本文主要介紹了RedisTemplate中boundHashOps的使用小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-04-04
  • Redis內(nèi)存回收策略

    Redis內(nèi)存回收策略

    這篇文章主要介紹了Redis內(nèi)存回收策略,需要的朋友可以參考下
    2007-02-02
  • Redis cluster集群的介紹

    Redis cluster集群的介紹

    今天小編就為大家分享一篇關(guān)于Redis cluster集群的介紹,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • 利用Redis實(shí)現(xiàn)訂單30分鐘自動(dòng)取消

    利用Redis實(shí)現(xiàn)訂單30分鐘自動(dòng)取消

    本文主要介紹了利用Redis實(shí)現(xiàn)訂單30分鐘自動(dòng)取消,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Redis的六種底層數(shù)據(jù)結(jié)構(gòu)(小結(jié))

    Redis的六種底層數(shù)據(jù)結(jié)構(gòu)(小結(jié))

    本文主要介紹了Redis的六種底層數(shù)據(jù)結(jié)構(gòu),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • redis常用命令小結(jié)

    redis常用命令小結(jié)

    這篇文章主要介紹了redis的一些常用命令,需要的朋友可以參考下
    2014-06-06
  • Redis實(shí)現(xiàn)多人多聊天室功能

    Redis實(shí)現(xiàn)多人多聊天室功能

    這篇文章主要為大家詳細(xì)介紹了Redis實(shí)現(xiàn)多人多聊天室功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • 淺談Redis哨兵模式的使用

    淺談Redis哨兵模式的使用

    這篇文章主要介紹了淺談Redis哨兵模式的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • Redis中l(wèi)ua腳本實(shí)現(xiàn)及其應(yīng)用場景

    Redis中l(wèi)ua腳本實(shí)現(xiàn)及其應(yīng)用場景

    本文主要介紹了Redis中l(wèi)ua腳本實(shí)現(xiàn)及其應(yīng)用場景,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04

最新評論