Spring之藉助Redis設計訪問計數器之擴充套件篇

一灰灰發表於2018-07-13

logo

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

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

推薦博文:

I. 資料結構設計

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

1. 訪問記錄

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

List資料結構是否可行?

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

如果對redis的資料結構有一點了解,會直到有一個ZSet(有序的集合)正好適合這種場景

  • 確保不會插入重複的資料,每個資料對應的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則為具體的請求域名

具體的結構類似

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

相關文章