Spring之藉助Redis設計訪問計數器之擴展篇

logo

以前寫了一篇博文,簡單的介紹了下如何利用Redis配合Spring搭建一個web的訪問計數器,以前的內容比較初級,如今考慮對其進行擴展,新增訪問者記錄java

  • 記錄當前站點的總訪問人數(根據Ip或則設備號)
  • 記錄當前訪問者在總訪問人數中的排名
  • 記錄每一個子頁面的訪問計數,記錄站點的總訪問計數

推薦博文:nginx

I. 數據結構設計

首先根據上面的幾個數據維度進行劃分,首先每一個站點有本身獨立的數據結構,其中訪問者記錄和每一個頁面對應的訪問計數,確定是不同的,下面分別進行說明git

1. 訪問記錄

要求記錄每一個訪問者的IP或者設備號,以此來計算總得訪問人數,以及當前的訪問者在總得訪問人數中的位置github

List數據結構是否可行?web

  • 每次新來一個訪問者,須要與全部的訪問者進行對比,判斷是不是新的訪問者,是則插入列表;不是則查出其對應的位置

若是對redis的數據結構有一點了解,會直到有一個ZSet(有序的集合)正好適合這種場景redis

  • 確保不會插入重複的數據,每一個數據對應的score就是該訪問者的首次訪問排序

具體的結構相似數組

-- ip (score)
127.0.0.1   (1)
127.0.0.2   (2)
127.0.0.3   (3)
...
複製代碼

2. url計數

依然沿用以前的Hash數據結構,每一個應用申請一個APPKEY,做爲hash結構的Key,而後field則爲具體的請求域名bash

具體的結構相似服務器

appKey: // appKey
  blog.hhui.top: 1314  // 站點對應的總訪問數
  blog.hhui.top/index: 1303 // 具體的頁面對應的訪問數
  blog.hhui.top/about: 11 // 具體的頁面對應的訪問數
appKey:
  blog.hhui.top: 1314
  blog.hhui.top/index: 1303
  blog.hhui.top/about: 11
複製代碼

II. 實現

具體的實現其實沒有什麼特別須要注意的地方,簡單說一下幾個關鍵點,一個是Redis的Hash和Zset兩個數據結構的訪問修改方法;一個則是如何獲取訪問者的IP數據結構

1. 獲取客戶端IP

在Spring中如何獲取客戶端IP呢?由於我我的的服務器是走的Nginx進行反向代理,因此須要在Nginx層添加一行配置,避免將客戶端IP吃掉了

在nginx.con的配置中,轉發的地方添加下面的一行

location / {
    proxy_set_header X-real-ip  $remote_addr;
}
複製代碼

而後就能夠在代碼層,經過解析HttpServletRequest參數,獲取真實IP,這段代碼網上比較多,直接拿來使用(我這裏是放在了一個Filter層,在這裏獲取服務端關心的一些參數,供整個請求鏈路使用)

獲取客戶端IP方法

/** * 獲取Ip地址 * @param request * @return */
private static String getIpAdrress(HttpServletRequest request) {
    String Xip = request.getHeader("X-Real-IP");
    String XFor = request.getHeader("X-Forwarded-For");
    if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
        //屢次反向代理後會有多個ip值,第一個ip纔是真實ip
        int index = XFor.indexOf(",");
        if(index != -1){
            return XFor.substring(0,index);
        }else{
            return XFor;
        }
    }
    XFor = Xip;
    if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
        return XFor;
    }
    if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
        XFor = request.getHeader("Proxy-Client-IP");
    }
    if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
        XFor = request.getHeader("WL-Proxy-Client-IP");
    }
    if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
        XFor = request.getHeader("HTTP_CLIENT_IP");
    }
    if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
        XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
    }
    if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
        XFor = request.getRemoteAddr();
    }
    return XFor;
}
複製代碼

2. Redis操做

接下來就是redis數據結果的操做了,關於Spring中如何配置和簡單使用RedisTemplate能夠參考 《180611-Spring之RedisTemplate配置與使用》

下面簡單貼一下核心的Redis操做代碼, 關於Hash的訪問就沒啥好說的,參考上一篇博文便可

/** * 獲取redis中指定value的score * * @param key 惟一key * @param value 存在redis中的實際值(計數組件中value即爲客戶端IP) * @return */
public static Long zScore(String key, String value) {
    return template.execute((RedisCallback<Long>) con -> {
        Double ans = con.zScore(toBytes(key), toBytes(value));
        return ans == null ? 0 : ans.longValue();
    });
}

/** * 表示新增一條記錄 * * @param key * @param value 對應客戶端ip * @param score 對應客戶端訪問的排名 * @return 當set中沒有記錄時,返回true;不然返回false */
public static Boolean zAdd(String key, String value, long score) {
    return template.execute((RedisCallback<Boolean>) con -> con.zAdd(toBytes(key), score, toBytes(value)));
}

/** * 獲取zset中最大的score,即在計數組件中,這個值就是總得訪問人數 * @param key * @return */
public static Long zMaxScore(String key) {
    return template.execute((RedisCallback<Long>) con -> {
        Set<RedisZSetCommands.Tuple> set = con.zRangeWithScores(toBytes(key), -1, -1);
        if (CollectionUtils.isEmpty(set)) {
            return 0L;
        }

        Double score = set.stream().findFirst().get().getScore();
        return score.longValue();
    });
}
複製代碼

主要的redis操做是上面三個方法,那麼怎麼調用的呢?直接看下面的邏輯便可,比較清晰

  • 獲取站點的總訪問人數
  • 嘗試獲取訪問者的排名
  • 若是沒有獲取到排名,表示首次訪問,則須要新插入一條記錄
  • 獲取到排名,則直接返回
public CountDTO visit(String appKey, String url) {
    String visitKey = visitKey(appKey);

    // 首先是獲取站點的總訪問人數
    long visitTotalNum = QuickRedisClient.zMaxScore(visitKey);
    // 獲取訪問者在總訪問人數中的排名,若是爲0,表示該用戶沒有訪問過
    long visitIndex = QuickRedisClient.zScore(visitKey, ReqInfoContext.getReqInfo().getClientIp());
    if (visitIndex == 0) {
        // 不存在(即用戶沒有訪問過),則須要添加一條訪問記錄
        visitTotalNum += 1;
        visitIndex = visitTotalNum;
        QuickRedisClient.zAdd(visitKey, ReqInfoContext.getReqInfo().getClientIp(), visitIndex);
    }
    
    // 構建DO對象
}
複製代碼

看到上面這一段邏輯的實現,若是一點疑問都沒有,那我不得不懷疑是否真的看了這篇博文了,或者說就是單純的看了而已,卻沒有一點的收貨

重點說明,上面的實現有併發問題、併發問題、併發問題,重要的事情說三遍,至於爲何以及該如何解決,歡迎討論

一個實際使用這個計數器的case,就是我的的博客網站了,歡迎點擊查看:

showcase

III. 其餘

0. 相關博文

1. 一灰灰Blog: https://liuyueyi.github.io/hexblog

一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

2. 聲明

盡信書則不如,已上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

QrCode
相關文章
相關標籤/搜索