Redis修行 — 基数统计:HyperLogLog
學 無 止 境 ,與 君 共 勉 。
簡介
HyperLogLog是Redis中的高級數據結構,它主要用于對海量數據(可以統計2^64個數據)做基數統計(去重統計數量)。它的特點是速度快,占用空間小(12KB)。但是計算存會在誤差,標準誤差為0.81%。HyperLogLog只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以他并不能判斷給定的元素是否已經存在了。
基本指令
pfadd(key,value…)
將指定的元素添加到HyperLogLog中,可以添加多個元素
public void pfAdd(String key, String... value) {stringRedisTemplate.opsForHyperLogLog().add(key, value);}pfcount(key…)
返回給定HyperLogLog的基數估算值。當一次統計多個HyperLogLog時,需要對多個HyperLogLog結構進行比較,并將并集的結果放入一個臨時的HyperLogLog,性能不高,謹慎使用
public Long pfCount(String... key) {return stringRedisTemplate.opsForHyperLogLog().size(key);}pfmerge(destkey, sourcekey…)
將多個HyperLogLog進行合并,將并集的結果放入一個指定的HyperLogLog中
public void pfMerge(String destKey, String... sourceKey) {stringRedisTemplate.opsForHyperLogLog().union(destKey, sourceKey);}誤差測試
基于SpringBoot的進行誤差測試,初始化5個HyperLogLog,每個隨機添加10000個元素,然后調用pfcount查看具體誤差:
@RestController @RequestMapping("/redis/hll") public class HyperController {private final RedisService redisService;public HyperController(RedisService redisService) {this.redisService = redisService;}@GetMapping("/init")public String init() {for (int i = 0; i < 5; i++) {Thread thread = new Thread(() -> {String name = Thread.currentThread().getName();Random r = new Random();int begin = r.nextInt(100) * 10000;int end = begin + 10000;for (int j = begin; j < end; j++) {redisService.pfAdd("hhl:" + name, j + "");}System.out.printf("線程【%s】完成數據初始化,區間[%d, %d)\n", name, begin, end);},i + "");thread.start();}return "success";}@GetMapping("/count")public String count() {long a = redisService.pfCount("hhl:0");long b = redisService.pfCount("hhl:1");long c = redisService.pfCount("hhl:2");long d = redisService.pfCount("hhl:3");long e = redisService.pfCount("hhl:4");System.out.printf("hhl:0 -> count: %d, rate: %f\n", a, (10000 - a) * 1.00 / 100);System.out.printf("hhl:1 -> count: %d, rate: %f\n", b, (10000 - b) * 1.00 / 100);System.out.printf("hhl:2 -> count: %d, rate: %f\n", c, (10000 - c) * 1.00 / 100);System.out.printf("hhl:3 -> count: %d, rate: %f\n", d, (10000 - d) * 1.00 / 100);System.out.printf("hhl:4 -> count: %d, rate: %f\n", e, (10000 - e) * 1.00 / 100);return "success";} }初始化數據,調用接口:http://localhost:8080/redis/hll/init
線程【4】完成數據初始化,區間[570000, 580000) 線程【2】完成數據初始化,區間[70000, 80000) 線程【0】完成數據初始化,區間[670000, 680000) 線程【1】完成數據初始化,區間[210000, 220000) 線程【3】完成數據初始化,區間[230000, 240000)查看具體統計數,計算誤差:http://localhost:8080/redis/hll/count
hhl:0 -> count: 10079, rate: -0.790000 hhl:1 -> count: 9974, rate: 0.260000 hhl:2 -> count: 10018, rate: -0.180000 hhl:3 -> count: 10053, rate: -0.530000 hhl:4 -> count: 9985, rate: 0.150000實戰
比如要統計文章的熱度和有效用戶點擊數。可以通過Reis的計數器來統計熱度,每次請就執行incr指令。通過HyperLogLog來統計有效用戶數。
實現思路
通過AOP和自定義注解來對需要統計的文章進行統計:
- 在需要統計的文章接口上加上注解
- 設置自定義注解值為HyperLogLog對應的key
- 將AOP的切入點設為自定義注解
- AOP中獲取注解值
- AOP中通過token或者cookie判斷用戶信息
- 累計熱度和用戶量
pom
引入redis和aop
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- redis Lettuce 模式 連接池 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>定義自定義注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Article {/*** 值為對應HyperLogLog的key*/String value() default ""; }定義AOP
@Aspect @Component public class ArticleAop {private static final String PV_PREFIX = "PV:";private static final String UV_PREFIX = "UV:";@Autowiredprivate RedisService redisService;/*** 定義切入點*/@Pointcut("@annotation(org.ylc.note.redis.hyperloglog.annotation.Article)")private void statistics() {}@Around("statistics()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {// 獲取注解Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();Article visitPermission = method.getAnnotation(Article.class);String value = visitPermission.value();// 獲取請求信息ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 這里用來模擬,直接通過參數傳入。實際項目中可以根據token或者cookie來實現String userId = request.getParameter("userId");// 熱度redisService.incr(PV_PREFIX + value);// 用戶量redisService.pfAdd(UV_PREFIX + value, userId);// 執行具體方法return proceedingJoinPoint.proceed();} }定義接口
在需要統計的接口上加上@Article()注解
@RestController @RequestMapping("/redis/article") public class ArticleController {@Autowiredprivate RedisService redisService;@Article("it")@GetMapping("/it")public String it(String userId) {String pv = redisService.get("PV:it");long uv = redisService.pfCount("UV:it");return String.format("當前用戶:【%s】,當前it類熱度:【%s】,訪問用戶數:【%d】", userId, pv, uv);}@Article("news")@GetMapping("/news")public String news(String userId) {String pv = redisService.get("PV:news");long uv = redisService.pfCount("UV:news");return String.format("當前用戶:【%s】,當前news類熱度:【%s】,訪問用戶數:【%d】", userId, pv, uv);}@GetMapping("/statistics")public Object statistics() {String pvIt = redisService.get("PV:it");long uvIt = redisService.pfCount("UV:it");String pvNews = redisService.get("PV:news");long uvNews = redisService.pfCount("UV:news");redisService.pfMerge("UV:merge", "UV:it", "UV:news");long uvMerge = redisService.pfCount("UV:merge");Map<String, String> result = new HashMap<>();result.put("it", String.format("it類熱度:【%s】,訪問用戶數:【%d】;", pvIt, uvIt));result.put("news", String.format("news類熱度:【%s】,訪問用戶數:【%d】", pvNews, uvNews));result.put("merge", String.format("合并后訪問用戶數:【%d】", uvMerge));return result;} }訪問源碼
所有代碼均上傳至Github上,方便大家訪問
>>>>>> Redis實戰 — HyperLogLog <<<<<<
日常求贊
創作不易,如果各位覺得有幫助,求點贊 支持
求關注
微信公眾號: 俞大仙
總結
以上是生活随笔為你收集整理的Redis修行 — 基数统计:HyperLogLog的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿龙的感悟
- 下一篇: 区块链让公益更透明安心 | FinTec