文章摘要: 其中訪問者記錄和每個頁面對應的訪問計數* @param score 對應客戶端訪問的排名
之前寫了一篇博文,簡單的介紹了下如何利用Redis配合Spring搭建一個web的訪問計數器,之前的內容比較初級,現在考慮對其進行擴充套件,新增訪問者記錄
- 記錄當前站點的總訪問人數(根據Ip或則裝置號)
- 記錄當前訪問者在總訪問人數中的排名
- 記錄每個子頁面的訪問計數,記錄站點的總訪問計數
推薦博文:
- 180626-Spring之藉助Redis設計一個簡單訪問計數器
- 180611-Spring之RedisTemplate配置與使用
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) 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 ) con -> con.zAdd(toBytes(key), score, toBytes(value))); } /** * 獲取zset中最大的score,即在計陣列件中,這個值就是總得訪問人數 * @param key * @return */ public static Long zMaxScore(String key) { return template.execute((RedisCallback ) con -> { Set 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,就是個人的部落格網站了,歡迎點選檢視:
- 小灰灰blog:blog.hhui.top/
- 小灰灰blog: liuyueyi.github.io/hexblog/
III. 其他
0. 相關博文
- 180626-Spring之藉助Redis設計一個簡單訪問計數器
- 180611-Spring之RedisTemplate配置與使用
- Redis實現分散式鎖相關注意事項
1.一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址:小灰灰Blog
- QQ: 一灰灰/3302797840