四、Redis新类型之 hyperloglog
一、基础知识
1、常见名词
- UV:Unique Visitor,独立访客,一般理解为客户端IP。需要去重。
- PV:Page View,页面浏览量。不用去重。
- DAU:Daily Active User,日活跃用户量。常用于反映网站、互联网应用或者网络游戏的运营情况。
- MAU:MonthIy Active User,月活跃用户量。
2、HyperLogLog概念
去重复统计功能的基数估计算法-就是HyperLogLog,它是一种概率算法,结果是有误差的,牺牲准确率来换取空间。
HyperLogLog的标准错误为1.04 / sqrt (m),其中“m”是所使用的寄存器数。Redis使用16384个寄存器,因此标准误差为0.81%。
3、HyperLogLog占用大小
在Redis里面,每个HyperLogLog键只需要花费 12KB 内存,就可以计算接近2^64个不同元素的基数。
每个桶取6位,16384*6÷8 = 12kb,每个桶有6位,最大全部都是1,值就是63
4、HyperLogLog原理
只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容。其底层是string类型。
5、什么是基数
是一种数据集,去重复后的真实个数。
基数统计:用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算。
6、优缺点
优点
-
在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
缺点
- 统计结果不精确,有误差。
-
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
7、使用场景
- 统计某个网站的UV、统计某个文章的UV。
- 用户搜索网站关键词的数量。
- 统计用户每天搜索不同词条个数。
二、HyPerLogLog如何做的?如何演化出来的?
由去重统计的方式展开:
1、Java HashSet
不适合大数据统计,占空间大,hash冲突。
2、Redis set
不适合大数据统计,占用空间。亿级流量下,如果用set存储访问用户的ip,1.5亿访问量*15字节(ipv4地址)=2GB,一个月=60GB。
3、Redis bitmap
不适合大数据统计,样本元素越多内存消耗急剧增大,难以管控+各种慢。
bitmap是通过用位bit数组来表示各元素是否出现,每个元素对应一位,所需的总内存为N个bit。基数计数则将每一个元素对应到bit数组中的其中一位,比如bit数组010010101(按照从零开始下标,有的就是1、4、6、8)。新进入的元素只需要将已经有的bit数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。
如果一个样本就是1亿个数据的基数位值,大约需要内存100000000/8/1024/1024约等于12M,这样得到统计一个对象样本的基数值需要12M。
但是,如果统计10000个对象样本,就需要117.1875G将近120G,可见使用bitmaps还是不适用大数据量下(亿级)的基数计数场景,但是bitmaps方法是精确计算的。
4、HyperLogLog的诞生
通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存。
三、为什么Redis集群的最大槽数是16384个?
Redis集群并没有使用一致性hash而是引入了哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。但为什么哈希槽的数量是16384(2^14)个呢?
CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。换句话说值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?
(1)正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。因此16k处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。请注意,在小型群集中,位图将难以压缩,因为当N较小时,位图将设置的slot / N位占设置位的很大百分比。
(2)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb 因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。16348只有2kb。
(3)redis的集群主节点数量基本不可能超过1000个。集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
(4)槽位越小,节点少的情况下,压缩比高,容易传输Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
四、常见命令
五、网页UV统计Demo
Controller
@RestController @Slf4j public class HyperLogLogController { @Resource private RedisTemplate redisTemplate; @Resource private HyperLogLogService hyperLogLogService; public static final String MYKEY = "myhyper"; // 获取IP去重后的网页访问量 @RequestMapping(value = "/hyperloglog/uv", method = RequestMethod.GET) public long uv() { return redisTemplate.opsForHyperLogLog().size(MYKEY); } // 初始化数据 @RequestMapping(value = "/hyperloglog/init", method = RequestMethod.POST) public void init() { hyperLogLogService.init(); } }
Service
@Service @Slf4j public class HyperLogLogService { @Resource private RedisTemplate redisTemplate; public static final String MYKEY = "myhyper"; /** * 模拟后台有用户点击网页,每个用户来自不同ip地址 */ public void init() { log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址"); // 测试模拟,实际不用这种写法 new Thread(() -> { String ip = null; for (int i = 1; i <= 200; i++) { Random r = new Random(); ip = "192.168.1." + r.nextInt(256); Long hll = redisTemplate.opsForHyperLogLog().add(MYKEY, ip); log.info("ip={},该ip地址访问首页的次数={}", ip, hll); //暂停2秒钟线程 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1").start(); } }