一、使用场景
使用场景:
统计网页访问量。
思考:怎么样统计网页访问量,并且一个IP一天访问多次同一个页面,只能算一次?
分析:1.首先分析该统计数,是否需要正确,其实产品只需要一个大概的,一天100W,和一天110W,其实差不多。如果使用Java的话,那个list可以去重,同时在内存等相关上要占很小的比率。
解决方式:
方式一:传统方式,集合实现。
使用集合(set)来储存每个访客的 IP ,通过集合性质(集合中的每个元素都各不相同)来得到多个独立 IP .但是缺点也很大,假如访问量很大,你需要一个很大的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?
使用字符串来储存每个 IPv4 地址最多需要耗费 15 字节(格式为 'XXX.XXX.XXX.XXX' ,比如'192.168.10.127')。
下表给出了使用集合记录不同数量的独立 IP 时,需要耗费的内存数量:
独立 IP 数量一天一个月一年
一百万15 MB 450 MB 5.4 GB
一千万150 MB 4.5 GB 54 GB
一亿1.5 GB 45 GB 540 GB
随着集合记录的 IP 越来越多,消耗的内存也会越来越多。
另外如果要储存 IPv6 地址的话,需要的内存还会更多一些
方式二:redis 2.8.6 版本之后,新的命令功能,HyperLogLong。
Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。
二、概念和实战
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存(因为 Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。),就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
1.使用命令模式
pfadd test:aaron:ip "191.168.1.23"
pfadd test:aaron:ip "191.168.1.24"
#结果为2
pfcount test:aaron:ip
2.py代码
import redis
#redis 连接
pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)
#HyperLogLog
def her_log():
for i in range(100000):
r.pfadd("test:log:aaron", "user%d" % i)
print (100000, r.pfcount("test:log:aaron"))
#主函数,执行行数
if __name__ == '__main__':
her_log()
3.Java代码
package com.example.redis.zfr.demoredis.bit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @author 繁荣Aaron
* redis工具类
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* 设置 HyperLogLong 的 key 值
* @param key
* @param value
*/
public Long pfadd(String key, String value){
return redisTemplate.execute((RedisCallback<Long>) con -> con.pfAdd(key.getBytes(),value.getBytes()));
}
/**
* 获取HyperLogLong 数量
* @param key
* @return
*/
public Long pfCount(String key){
return redisTemplate.execute((RedisCallback<Long>) con -> con.pfCount(key.getBytes()));
}
}
三、思考
1.pf 的内存占用为什么是 12k?
解决:Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。
2.两个页面合并访问量怎么做?
可以使用pfmerge命令。
命令:
pfadd test:aaron:ip "191.168.1.23"
pfadd test:aaron:ip "191.168.1.24"
pfadd test:aaron:ip:merge "191.168.1.24"
pfadd test:aaron:ip:merge "191.168.1.23"
PFMERGE test:aaron:ip:merge:result test:aaron:ip test:aaron:ip:merge
#结果为2
PFCOUNT test:aaron:ip:merge:result
四、总结
HyperLogLog 实现独立 IP 计算功能:
独立 IP 数量一天一个月一年一年(使用集合)
一百万12 KB 360 KB 4.32 MB 5.4 GB
一千万12 KB 360 KB 4.32 MB 54 GB
一亿12 KB 360 KB 4.32 MB 540 GB
下表列出了使用 HyperLogLog 记录不同数量的独立 IP 时,需要耗费的内存数量:
可以看到,要统计相同数量的独立 IP ,HyperLogLog 所需的内存要比集合少得多。