需求:實現某個介面每天呼叫了多少次,每個使用者只記錄一次。
(例如,統計刷題模組,練題模組,模擬面試模組每天訪問量,利於後續針對功能訪問量做出其他最佳化設計。貼子的瀏覽量)先分析幾種不同的方案:方案一:使用Hash雜湊結構
實現方法:當使用者訪問網站時,我們可以使用使用者的ID作為標識(若使用者未登入,則生成一個隨機標識)。透過Redis的HSET命令,以URI和日期拼接作為key,使用者ID或隨機標識作為field,將value設定為1。統計訪問量時,使用HLEN命令獲取結果。
優點:
-
實現簡單,易於理解。
-
查詢方便,資料準確性高。
缺點:
-
隨著key的增多,記憶體佔用過大,效能可能下降。
-
對於訪問量巨大的網站(如拼多多),此方案可能無法承受。
方案二:使用Bitset
實現方法:利用Bitset對使用者ID進行壓縮儲存。透過SETBIT命令標記使用者訪問,使用GETBIT查詢使用者是否訪問,最後透過BITCOUNT統計訪問量。
優點:
-
佔用記憶體更小,適用於大規模使用者資料。
-
查詢方便,可指定查詢某個使用者。
缺點:
-
使用者稀疏時,記憶體佔用可能比方案一更大。
-
對於未登入使用者,可能需要額外的對映開銷。
方案三:使用機率演算法
實現方法:採用Redis中的HyperLogLog演算法,這是一種基數評估演算法。使用PFADD命令記錄使用者訪問,透過PFCOUNT命令計算訪問量。
優點:
-
佔用記憶體極小,每個key僅需要12KB。
-
非常適合超大規模使用者訪問量的網站。
缺點:
-
查詢指定使用者時可能存在誤差。
-
總數統計存在一定的誤差(約0.81%)。
以上三種方案各有優劣,具體選擇應根據實際業務需求和場景來決定:
-
若對資料準確性要求較高,且訪問量適中,可以選擇方案一。
-
若需要處理大規模使用者資料,且對記憶體佔用有要求,可以選擇方案二。
-
若對資料精度要求不高,但需要處理超大規模使用者訪問量,可以選擇方案三。
以上是針對Redis提供的解決方案,在專案中對前後端日誌埋點資料,透過流式計算以及大資料分析,也是常用的解決方案。
首先最簡單的做法就是當使用者訪問某個功能時,在進行查詢時自動傳遞一個攜帶使用者id,日期與功能欄位的資料傳入mysql中,當第二次進入先查詢,當日已存在便不再儲存該資料,但是該方案存在問題便是,每個使用者都在頻繁的與資料庫做互動,是否針對具體業務情況將其存入單獨的庫儲存也需要權衡,而且我們系統已經引入的redis,不妨直接用redis來做。首次先查詢redis,redis存在直接切斷,不存在存入則存入redis,後續透過定時任務排程在流量穩定時遷入資料庫。
redis的HyperLogLog
Redis 在 2.8.9 版本新增了 HyperLogLog 結構。Redis HyperLogLog 是用來做基數統計的演算法,HyperLogLog 的優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定 的、並且是很小的。在 Redis 裡面,每個 HyperLogLog 鍵只需要花費 12 KB 記憶體,就可以計算接近 2^64 個不同元素的基 數。這和計算基數時,元素越多耗費記憶體就越多的集合形成鮮明對比。
但是,因為 HyperLogLog 只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以HyperLogLog 不能像集合那樣,返回輸入的各個元素。比如資料集 {1, 3, 5, 7, 5, 7, 8}, 那麼這個資料集的基數集為 {1, 3, 5 ,7, 8}, 基數(不重複元素)為5。 基數估計就是在誤差可接受的範圍內,快速計算基數。
簡單的說HyperLogLog透過機率學統計訪問數量,統計的時候存在誤差,但是誤差很小。所以適合特殊場景,例如日訪問量統計。
操作指令如下:
使用者001訪問。如果 HyperLogLog 的內部被修改了,那麼返回 1,否則返回 0 .
PFADD visit_{data} “001”
使用者002訪問。
PFADD visit_{data} “002”
檢視訪問數量。
PFCOUNT visit_{data}
整合spring
public boolean add(String key, String obj) { return redisTemplate.opsForHyperLogLog().add(key, obj) > 0; } public long count(String key) { // pfcount 非精準統計 key的計數 return redisTemplate.opsForHyperLogLog().size(key); }
我們可以抽象出單獨介面方法,需要統計日活量的直接呼叫介面即可
功能介面
public interface UniqueVisitor { Boolean add(String id, String loginid); Boolean addMonth(String id, String loginid); Long getCount(String id); }
@Service public class UniqueVisitorImpl implements UniqueVisitor { @Resource private RedisUtil redisUtil; private static final String Unique_Visitor = "user_Unique_Visitor:"; @Override public Boolean add(String id, String loginid) { LocalDate today = LocalDate.now(); long dayOfYear = today.getDayOfYear(); return redisUtil.add(Unique_Visitor + id+dayOfYear, loginid); } @Override public Boolean addMonth(String id, String loginid) { LocalDate today = LocalDate.now(); long Month = today.getMonthValue(); return redisUtil.add(Unique_Visitor + id+Month, loginid); } @Override public Long getCount(String id) { LocalDate today = LocalDate.now(); long dayOfYear = today.getDayOfYear(); return redisUtil.count(Unique_Visitor + id+dayOfYear); } }
呼叫計入日活量
@GetMapping("/uniquevisitor") public Result<Boolean> uniqueVisitor(@RequestParam("id") String id) { try { return Result.ok(uniqueVisitor.getCount(id)); } catch (Exception e) { log.error("SubjectCategoryController.add.error:{}", e.getMessage(), e); return Result.fail("查詢瀏覽量失敗"); } }
/** * 統計使用者今日訪問新增題目 */ uniqueVisitor.add("addSubject",LoginUtil.getLoginId());
實現效果
我們測試了兩個賬號,可以看到今日新增題目的日活量為2次。