之前寫了一篇博文,簡單的介紹了下如何利用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,就是個人的部落格網站了,歡迎點選檢視:
- 小灰灰blog: blog.hhui.top/
- 小灰灰blog: liuyueyi.github.io/hexblog/
III. 其他
0. 相關博文
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840