前言
在經歷了,緩存、限流、布隆穿透等等一系列加強功能,十萬博客基本算是成型,網站上線以后也加入了百度統計來見證十萬+ 的整個過程。
但是百度統計並不能對每篇博文進行詳細的瀏覽量統計,如果做一些熱點博文排行、48小時排行之類統計,還需要引入瀏覽量統計功能。
設計
通常情況下,我們只需要每次請求瀏覽量+1,但是這樣真的好嗎?或者更直白的講,真實瀏覽數准確嗎?
UPDATE blog SET views = views+1 WHERE id=?
參考了多個社區博客的設計,因為並不十分清楚其后端實現過程,只能從前端得出以下結論。
-
慕課網手記:無論是用戶登錄模式還是用戶狀態,每次刷新頁面瀏覽數都會 +1。
-
51CTO博客:無論是用戶登錄模式還是用戶狀態,每次刷新頁面瀏覽數都會 +1。
-
簡書:用戶登錄模式下,無論如何刷新瀏覽數都不會新增,但是游客狀態下每次刷新瀏覽數都會+1。
-
博客園:無論是用戶登錄模式還是用戶狀態,每次刷新頁面瀏覽數都不變,即使隔天訪問,也不變,沒細測。
-
微信公眾號:只能是用戶登錄狀態,每次刷新瀏覽數基本不變,有時候會出現由多變少的情況,不知道大家有沒有發現。
-
CSDN博客:無論是用戶登錄模式還是用戶狀態,每次刷新頁面瀏覽數都不變,但是隔天訪問,瀏覽數會+1,沒細測。
基於以上社區的數據,直接 Pass 掉前兩位,總結了以下幾種方案,都是基於緩存標識實現。
-
如果游客或者登錄用戶訪問,按照 IP + 文章 ID 維度增加瀏覽數,那局域網中怎么算?
-
如果是游客訪問,按照 IP + 瀏覽器SessionId + 文章 ID 維度增加瀏覽數,可能解決局域網問題,那么關閉瀏覽器,重新打開又怎么算?
-
如果是登錄用戶,用戶ID + 文章 ID 維度增加瀏覽數,那么游客在登錄后算不算一個瀏覽數,或者是用戶換個 IP 登錄算不算 ?
所以說,怎么算都不准確,瀏覽數本身就是一個不需要太精確的功能,不要想太多,直接使用 IP + 文章ID 維度即可。
方案
方案一
得到 GET 請求,在限流之后,緩存之前,判斷緩存中是否存在 IP+ 文章ID是否存在 Key。
如果存在,說明之前瀏覽過,就什么也不做。如果沒有,就加上這個 Key,根據業務設置緩存失效時間,然后更新數據庫瀏覽量+1,下面是代碼實現:
//獲取 Key
String key = IPUtils.getIpAddr()+":blog:"+id;
//判斷是否存在
boolean flag = redisUtil.hasKey(key);
if(!flag){
//設置緩存標識並更新數據庫
redisUtil.set(key,"true",36000);
String nativeSql = "UPDATE blog SET views = views+1 WHERE id=?";
dynamicQuery.nativeExecuteUpdate(nativeSql,new Object[]{id});
}
方案二
這樣基本能保證真實的博文瀏覽量,你以為就這么結束了嗎?我們做的可是一個高並發的博客,直接落庫,顯得不是逼格太 Low 了!
為了進一步提升性能力,來做下一步優化,判斷不存在之后,先不急於更新數據庫,先在 Redis 里給這篇文章的瀏覽量+1,Key 為 viewCount:articleId,value 為緩存的瀏覽量。然后設置一個定時任務,定時更新 Redis 緩存數據到數據庫。
這樣,是不是逼格一下子提升了好幾個檔次!!!下面來介紹一款更有逼格的第三方計數工具。
方案三
一款高並發計數神器 Redis HyperLogLog,她是用來做基數統計的算法,優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、並且是很小的。
在 Redis 里面,每個 HyperLogLog 鍵只需要花費 12 KB 內存,就可以計算接近 2^64 個不同元素的基數。這和計算基數時,元素越多耗費內存就越多的集合形成鮮明對比。
什么是基數?比如數據集 {1, 3, 5, 7, 5, 7, 8}, 那么這個數據集的基數集為 {1, 3, 5 ,7, 8}, 基數(不重復元素)為5。
為了校驗准確性,博主特意測試了一下,分別測試了,20000 和 100000 的數據量,基本上用了 12KB。
在測試之前 info 查詢一下:
used_memory_human:910.14K
測試之后,可以說基本差不多:
used_memory_human:922.27K
下面我們通過代碼來實現,引入 redis starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
這里,我們只需要兩個API即可:
/**
* 計數
* https://blog.52itstyle.vip
* @param key
* @param value
*/
public void add(String key, Object... value) {
redisTemplate.opsForHyperLogLog().add(key,valu);
}
/**
* 獲取總數
* https://blog.52itstyle.vip
* @param key
*/
public Long size(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
然后寫個AOP:
@Around("ServiceAspect()")
public Object around(ProceedingJoinPoint joinPoint) {
Object[] object = joinPoint.getArgs();
Object blogId = object[0];
Object obj = null;
try {
String value = IPUtils.getIpAddr();
String key = "viewCount:" + blogId;
// key 為 文章ID,Value 為請求IP地址
redisUtil.add(key,value);
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
博文請求:
/**
* 博文
* https://blog.52itstyle.vip
*/
@RequestMapping("{id}.shtml")
public String page(@PathVariable("id") Long id, ModelMap model) {
try{
Blog blog = blogService.getById(id);
String key = "viewCount:"+id;
Long views = redisUtil.size(key);
//直接從緩存中獲取並與之前的數量相加
blog.setViews(views+blog.getViews());
model.addAttribute("blog",blog);
} catch (Throwable e) {
return "error/404";
}
return "article";
}
業務代碼:
/**
* https://blog.52itstyle.vip
* 執行順序
* 1)限流
* 2)布隆
* 3)計數
* 4) 緩存
* @param id
* @return
*/
@Override
@ServiceLimit(limitType= ServiceLimit.LimitType.IP)
@BloomLimit
@HyperLogLimit
@Cacheable(cacheNames ="blog")
public Blog getById(Long id) {
String nativeSql = "SELECT * FROM blog WHERE id=?";
return dynamicQuery.nativeQuerySingleResult(Blog.class,nativeSql,new Object[]{id});
}
最后,寫個定時任務,夜間入庫:
@Scheduled(cron = "0 30 23 * * ?")
public void createHyperLog() {
logger.info("計數落庫開始");
String nativeSql = "SELECT id FROM blog";
List<Object> list = dynamicQuery.query(nativeSql,new Object[]{});
list.forEach(blogId ->{
String key = "viewCount:"+blogId;
Long views = redisUtil.size(key);
if(views>0){
String updateSql = "UPDATE blog SET views=views+? WHERE id=?";
dynamicQuery.nativeExecuteUpdate(updateSql,new Object[]{views,blogId});
redisUtil.del(key);
}
});
logger.info("計數落庫結束");
}
小結
擼完計數功能,作為一個個人博客基本上差不多了已經,前后端框架、連接池、限流、緩存、計數、動靜分離,HTTPS安全認證、百度收錄等等,后面會追加后台管理,模板、插件等等一系列功能,有興趣的小伙伴可以一起參與進來啊啊啊啊啊啊......
案例
源碼:https://gitee.com/52itstyle/spring-boot-blog
列表:https://blog.52itstyle.top/index
博文:https://blog.52itstyle.top/51.html