Redis HyperLoglog、Bloom Filter 、Bitmap

一 HyperLoglog
redis 统计PV 可以用最简单的string类型
redis 统计UV 方案有多种:
set类型,以user_id 为值
使用方法:
sadd uv_type user_id
scard uv_type //1
爆款页面几千万UV 就需要很大的set集合来统计 非常浪费空间。若果这样的页面很多呢?
HyperLoglog 提供不精确的去重计数方案 误差在0.81%
使用方法:
pfadd uv_type user_id1
pfadd uv_type user_id2
pfadd uv_type user_id3 user_id4 user_id5
pfcount uv_type //5
pfmerge 可以把两个UV 加起来
pfmerge total uv_type uv_type1 :total 是俩uv合并之后的数字
pfcount total
pf 是 这个数据结构的发明人,Philippe Flajolet  菲利普 芙拉若莱
 Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间
在 Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。
二 布隆过滤器(Bloom Filter)
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?
你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?
如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查询,当系统并发量很高时,数据库是很难扛住压力的。
你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不缓存的话,性能又跟不上,这该怎么办
这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有一定的误判概率。
Bloom Filter 可以理解为已个不怎么精确的set 结构,使用contains方法判断某个对象是否存在时,可能会误判。当它说某个值存在,它就可能不存在;说不存在时候,肯定不存在。
redis4.0之后才提供布隆过滤器插件功能。
使用方法:
bf.add JackGF liuyifei
bf.add JackGF liujialing
bf.add JackGF linzhiling
bf.exists JackGF liuyifei //1
bf.exists JackGF jialing //0
bf.madd JackGF sijiali heben bulaini // 1 1 1
bf.mexists JackGF sijiali heben bulaini jialing // 1 1 1 0
默认参数的布隆过滤器 误判率会有点高,所以redis提供了自定义参数的布隆过滤器。如果对应的 key 已经存在,bf.reserve会报错。bf.reserve有三个参数,分别是 key, error_rateinitial_size。错误率越低,需要的空间越大。initial_size参数表示预计放入的元素数量,当实际数量超出这个数值时,误判率会上升。
所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默认的error_rate是 0.01,默认的initial_size是 100。
bf.reserve JackGF 0.001 50000
原理:
每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。
向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个位数组比较拥挤,判断正确的概率就会降低。
使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为 error_rate 不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间
其他使用场景:爬虫系统 对重复URL去重。
邮箱系统的垃圾邮件过滤功能,。
NOSQL 缓存穿透。
Bitmap 位图
在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。
为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。
当我们要统计月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。这时就可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到月底遍历一次位图就可以得到月度活跃用户数。不过这个方法也是有条件的,那就是 userid 是整数连续的,并且活跃占比较高,否则可能得不偿失
使用方法:
setbit key offset value
getbit key offset
bitcount key
bitfield key get
bitpos key bit start end
u:sign:1000:201902表示ID=1000的用户在2019年2月的签到记录。
场景一 用户签到
  1. # 用户2月17号签到
  2. SETBIT u:sign:1000:201902 16 1 # 偏移量是从0开始,所以要把17减1
  3. # 检查2月17号是否签到
  4. GETBIT u:sign:1000:201902 16 # 偏移量是从0开始,所以要把17减1
  5. # 统计2月份的签到次数
  6. BITCOUNT u:sign:1000:201902
  7. # 获取2月份首次签到的日期
  8. BITPOS u:sign:1000:201902 1 # 返回的首次签到的偏移量,加上1即为当月的某一天
统计和查找
Redis 提供了位图统计指令 bitcount 和位图查找指令 bitpos,bitcount 用来统计指定位置范围内 1 的个数,bitpos 用来查找指定范围内出现的第一个 0 或 1。
比如我们可以通过 bitcount 统计用户一共签到了多少天,通过 bitpos 指令查找用户从哪一天开始第一次签到。
如果指定了范围参数[start, end],就可以统计在某个时间范围内用户签到了多少天,用户自某天以后的哪天开始签到。
场景二
统计活跃用户
bitop and active_user bitset1 bitset2 bitset3
总活跃用户 bitCount active_user;
场景三 在线用户
在线 bitset online_today user_id 1
下线:bitset online_today user_id 0
bitcount online_today ;

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注