Redis修行 — 基数统计:HyperLogLog

简介

HyperLogLogRedis中的高级数据结构,它主要用于对海量数据(可以统计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

引入redisaop

    <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:";

    @Autowired
    private 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 {

    @Autowired
    private 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 <<<<<<

日常求赞

创作不易,如果各位觉得有帮助,求点赞 支持


求关注

微信公众号: 俞大仙


https://juejin.im/post/5e148a6ee51d4540ef70a9dd

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论