1. 程式人生 > >從SpringBoot構建十萬博文聊聊高併發文章瀏覽量設計

從SpringBoot構建十萬博文聊聊高併發文章瀏覽量設計

前言

在經歷了,快取、限流、布隆穿透等等一系列加強功能,十萬部落格基本算是成型,網站上線以後也加入了百度統計來見證十萬+ 的整個過程。

但是百度統計並不能對每篇博文進行詳細的瀏覽量統計,如果做一些熱點博文排行、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即可:

/**
 * 計數
 * @param key
 * @param value
 */
public void add(String key, Object... value) {
    redisTemplate.opsForHyperLogLog().add(key,valu);
}
/**
  * 獲取總數
  * @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;
}

博文請求:

/**
  * 博文
  */
@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";
}

業務程式碼:

/**
  * 執行順序
  * 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

參考

Redis HyperLogLog

神奇的HyperLogLog演算法