redis分布式缓存(二十五)一一亿级的UV计算解决方案

PV、UV的定义

  • PV(访问量):Page View, 用户每次访问即被计算一次,即页面浏览量点击量
  • UV(独立访客):Unique Visitor,一天内相同的客户端只会计算一次

基数计数基本概念

  • 基数计数(cardinality counting)用来统计一个集合中不重复的元素个数,例如统计某个网站的UV,或者用户搜索网站的关键词数量。
  • 实现基数计数,最简单的做法是记录集合中所有不重复的元素集合
    • 当统计的数据量变大时,相应的存储内存也会线性增长
    • 当集合变大,判断其是否包含新加入元素的成本变大
  • 实际上目前还没有发现更好的在大数据场景中准确计算基数的高效算法,因此在不追求绝对准确的情况下,使用概率算法是一个不错的解决方案。
  • 概率算法不直接存储数据集合本身,通过一定的概率统计方法预估基数值,这种方法可以大大节省内存,同时保证误差控制在一定范围内
  • 注:HyperLogLog采用概率算法统计模型,允许有一定的误差

为什么一个hyperloglog键的大小固定为12KB?

  • hyperloglog键结构体组成:
struct hllhdr {
    char magic[4];      // 固定‘HYLL’,用于标识hyperloglog键
    uint8_t encoding;   // 编码模式,有密集标识Dence和稀疏模式sparse
    uint8_t notused[3]; // 未使用字段,留着日后用
    uint8_t card[8];    // 基数缓存,存储上一次计算的基数
    uint8_t registers[]; // 桶个数,用来存放数据,Redis中大小为16384
};
复制代码
  • Redis的hyperloglog结构中,card数组为64位bit,理论上可以存储近2^64个值。
  • Redis规定分桶为 2^14 个桶,即是个数为16384个,每一个桶内的数据采用6bit大小来存放,Redis存放的是第一次出现“1”的位置的值,6bit可以表示0~64位,即可以支持2^64个基数。
  • registers部分占用内存为(16384*6+7)/8 = 12288个字节,另外加上HLL结构的头占用了16个字节,加起来一共12304个字节,也就是说一个hyperloglog键占用了12KB左右的大小,最多可以计算2^64个不同元素的基数。

HyperLogLog 命令

  • HyperLogLog 目前只支持3个命令,PFADD、PFCOUNT、PFMERGE

PFADD-添加

  • 将元素加入到HyperLogLog数据结构中,如果 HyperLogLog 的基数估算值在命令执行之后出现了变化,那么命令返回1,否则返回0。

PFCOUNT-统计基数

  • 返回给定 HyperLogLog 的基数估算值
127.0.0.1:6379> pfadd uv 127.0.0.1 127.0.0.2 127.0.0.3 127.0.0.4
(integer) 1
127.0.0.1:6379> pfadd uv 127.0.0.1
(integer) 0
127.0.0.1:6379> pfcount uv
(integer) 4
复制代码

PFMERGE-合并HLL

  • 把多个HyperLogLog合并为一个HyperLogLog,合并后的HyperLogLog的基数是通过所有的HyperLogLog进行并集后,得出来的。
127.0.0.1:6379> pfadd uv1 127.0.0.1 127.0.0.2 127.0.0.3 127.0.0.4
(integer) 1
127.0.0.1:6379> pfcount uv1
(integer) 4
127.0.0.1:6379> pfadd uv2 127.0.0.1 127.0.1.2  127.0.1.3 127.0.1.4
(integer) 1
127.0.0.1:6379> pfadd uv3 127.0.0.1  127.0.2.2 127.0.2.3 127.0.2.4
(integer) 1
127.0.0.1:6379> pfmerge newuv uv1 uv2 uv3
OK
127.0.0.1:6379> pfcount newuv
(integer) 10
复制代码
  • 这个命令很重要,例如你可以统计一周一个月的uv 就可以使用此命令来轻易实现。

案例实战:基于Redis的UV计算

步骤1:模拟UV访问

@Service
@Slf4j
public class TaskService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     *模拟UV访问
     */
    @PostConstruct
    public void init(){
        log.info("模拟UV访问 ..........");
        new Thread(()->this.refreshData()).start();
    }

    /**
     * 刷新当天的统计数据
     */
    public void refreshDay(){
        Random rand = new Random();
        String ip=rand.nextInt(999)+"."+
                rand.nextInt(999)+"."+
                rand.nextInt(999)+"."+
                rand.nextInt(999);
        //redis 命令 pfadd
        long n=this.redisTemplate.opsForHyperLogLog().add(Constants.PV_KEY,ip);
        log.debug("ip={} , returen={}",ip,n);
    }

    /**
     * 按天模拟UV统计
     */
    public void refreshData(){
        while (true){
            //刷新当天的统计数据
            this.refreshDay();
            //TODO 在分布式系统中,建议用xxljob来实现定时
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
复制代码

步骤2:实现UV统计功能

@RestController
@Slf4j
public class Controller {

    @Autowired
    private RedisTemplate redisTemplate;
    
    @GetMapping(value = "/uv")
    public long uv() {
        //redis命令 pfcount
        long size=this.redisTemplate.opsForHyperLogLog().size(Constants.PV_KEY);
        return size;
    }

}
复制代码

redis分布式缓存系列文章

上一篇:redis分布式缓存(二十四)一一 微关系计算解决方案