Redis基本操作-统计

5.3 统计

记录用户一年的签到记录

如果你用 string 类型来存储,那你需要 365 个 key/value,操作起来麻烦。通过
位图可以有效的简化这个操作。

它的统计很简单:
01111000111
每天的记录占一个位,365 天就是 365 个位,大概 46 个字节,这样可以有效的节省存储空间,如果有
一天想要统计用户一共签到了多少天,统计 1 的个数即可。

5.3.1 操作位图

对于位图的操作,可以直接操作对应的字符串(get/set),可以直接操作位(getbit/setbit)

  • 基本操作
    Redis 的基本操作可以归为两大类:
  1. 零存整取
    通过setbit命令进行从左到右按位进行存储,从0开始,遇到二进制位置为1的时候设置为1
    例如:字符“J”的ASCLL为74,二进制为01001010

    127.0.0.1:6379> SETBIT name 1 1
    (integer) 0
    127.0.0.1:6379> SETBIT name 4 1
    (integer) 0
    127.0.0.1:6379> SETBIT name 6 1
    (integer) 0
    127.0.0.1:6379> get name
    "J"
    127.0.0.1:6379> setbit name 9 1
    (integer) 0
    127.0.0.1:6379> setbit name 10 1
    (integer) 0
    127.0.0.1:6379> setbit name 15 1
    (integer) 0
    127.0.0.1:6379> get name
    "Ja"
    
    
  2. 整存零取

    存一个字符串进去,但是通过位操作获取字符串

  • 统计
    例如签到记录:
    01111000111
    1 表示签到的天,0 表示没签到,统计总的签到天数:

    通过bitcount命令,来统计key中包含1的个数

    bitcount 中,可以统计的起始位置,但是注意,这个起始位置是指字符的起始位置而不是 bit 的起始位置。

    除了 bitcount 之外,还有一个 bitpos。bitpos 可以用来统计在指定范围内出现的第一个 1 或者 0 的位置,这个命令中的起始和结束位置都是字符索引,不是 bit 索引,一定要注意。

  • Bit批处理

    在 Redis 3.2 之后,新加了一个功能叫做 bitfiled ,可以对 bit 进行批量操作。
    例如:
    BITFIELD name get u4 0

    #name ja 0100101001100001
    #表示获取 name 中的位,从 0 开始获取,获取 4 个位,返回一个无符号数字
    127.0.0.1:6379> BITFIELD name get u4 0
    1) (integer) 4
    

    表示获取 name 中的位,从 0 开始获取,获取 4 个位,返回一个无符号数字。

    • u 表示无符号数字
    • i 表示有符号数字,有符号的话,第一个符号就表示符号位,1 表示是一个负数

    bitfiled 也可以一次执行多个操作。

    get:

    127.0.0.1:6379> BITFIELD name get u4 1 get u4 0
    1) (integer) 9
    2) (integer) 4
    
    

    set:

    #用无符号的 98 转成的 8 位二进制数字,代替从第 8 位开始接下来的 8 位数字。
    127.0.0.1:6379> BITFIELD name set u8 8 98
    1) (integer) 97
    127.0.0.1:6379> get name
    "Jb"
    
    

    incrby:

    对置顶范围进行自增操作,自增操作可能会出现溢出,既可能是向上溢出,也可能是向下溢出。Redis中对于溢出的处理方案是折返。8 位无符号数 255 加 1 溢出变为 0;8 位有符号数 127,加 1 变为 -128.

    127.0.0.1:6379> BITFIELD name incrby u2 6 1
    1) (integer) 3
    127.0.0.1:6379> get name
    "Kb"
    127.0.0.1:6379> BITFIELD name incrby u2 6 1
    1) (integer) 0
    127.0.0.1:6379> get name
    "Hb"
    
    

    也可以修改默认的溢出策略,可以改为 fail ,表示执行失败

    BITFIELD name overflow fail incrby u2 6 1
    127.0.0.1:6379> BITFIELD name overflow fail incrby u2 6 1
    1) (integer) 3
    127.0.0.1:6379> BITFIELD name overflow fail incrby u2 6 1
    1) (nil)
    
    

    sat 表示留在在最大/最小值

    BITFIELD name overflow sat incrby u2 6 1
    127.0.0.1:6379> BITFIELD name overflow sat incrby u2 6 1
    1) (integer) 3
    127.0.0.1:6379> BITFIELD name overflow sat incrby u2 6 1
    1) (integer) 3
    
    

5.3.2 HyperLogLog

一般我们评估一个网站的访问量,有几个主要的参数:
pv,Page View,网页的浏览量
uv,User View,访问的用户

一般来说,pv 或者 uv 的统计,可以自己来做,也可以借助一些第三方的工具,比如 cnzz,友盟 等。
如果自己实现,pv 比较简单,可以直接通过 Redis 计数器就能实现。但是 uv 就不一样,uv 涉及到另外一个问题,去重。
我们首先需要在前端给每一个用户生成一个唯一 id,无论是登录用户还是未登录用户,都要有一个唯一id,这个 id 伴随着请求一起到达后端,在后端我们通过 set 集合中的 sadd 命令来存储这个 id,最后通过 scard 统计集合大小,进而得出 uv 数据。
如果是千万级别的 UV,需要的存储空间就非常惊人。而且,像 UV 统计这种,一般也不需要特别精确,800w 的 uv 和 803w 的 uv,其实差别不大。所以,我们要介绍今天的主角---HyperLogLog

Redis 中提供的 HyperLogLog 就是专门用来解决这个问题的,HyperLogLog 提供了一套不怎么精确但
是够用的去重方案,会有误差,官方给出的误差数据是 0.81%,这个精确度,统计 UV 够用了。
HyperLogLog 主要提供了两个命令:pfadd 和 pfcount。

  • pfadd 用来添加记录,类似于 sadd ,添加过程中,重复的记录会自动去重。
  • pfcount 则用来统计数据。
127.0.0.1:6379> pfadd uv v1 v2 v3
(integer) 1
127.0.0.1:6379> PFCOUNT uv
(integer) 3
127.0.0.1:6379> pfadd uv v1 v2 v3 v4 v3 v2 v1
(integer) 1
127.0.0.1:6379> PFCOUNT uv
(integer) 4

数据量少的时候看不出来误差。

在 Java 中,我们多添加几个元素:

public class HyperLogLog {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            for (int i = 0; i <1000 ; i++) {
                jedis.pfadd("uv","u"+i,"u"+(i+1));
            }
            long uv = jedis.pfcount("uv");
            //理论值是1001
            System.out.println(uv);
            //结果998
        });
    }
}

理论值是 1001,实际打印出来 998,有误差,但是在可以接受的范围内。

除了 pfadd 和 pfcount 之外,还有一个命令 pfmerge ,合并多个统计结果,在合并的过程中,会自动去重多个集合中重复的元素。

127.0.0.1:6379> pfadd uv2 uvv1 uvv2
(integer) 1
127.0.0.1:6379> PFMERGE uv2 uv
OK
127.0.0.1:6379> PFCOUNT uv2
(integer) 1000

5.3.3 布隆过滤器

我们用 HyperLogLog 来估计一个数,有偏差但是也够用。HyperLogLog 主要提供两个方法:

  • pfadd

  • pfcount

但是 HyperLogLog 没有判断是否包含的方法,例如 pfexists 、pfcontains 等。没有这样的方法存在,但是我们有这样的业务需求。

例如刷今日头条,推送的内容有相似的,但是没有重复的。这就涉及到如何在推送的时候去重?
解决方案很多,例如将用户的浏览历史记录下来,然后每次推送时去比较该条消息是否已经给用户推送了。但是这种方式效率极低,不推荐。

解决这个问题,就要靠我们今天要说的布隆过滤器。

  1. Bloom Filter 介绍

    Bloom Filter 专门用来解决我们上面所说的去重问题的,使用 Bloom Filter 不会像使用缓存那么浪费空间。当然,他也存在一个小小问题,就是不太精确。

    Bloom Filter 相当于是一个不太精确的 set 集合,我们可以利用它里边的 contains 方法去判断某一个对象是否存在,但是需要注意,这个判断不是特别精确。一般来说,通过 contains 判断某个值不存在,那就一定不存在,但是判断某个值存在的话,则他可能不存在。

    以今日头条为例,假设我们将用户的浏览记录用 B 表示,A 表示用户没有浏览的新闻,现在要给用户推送消息,先去 B 里边判断这条消息是否已经推送过,如果判断结果说没推送过(B 里边没有这条记录),那就一定没有推送过。如果判断结果说有推送过(B 里边也有可能没有这条消息),这个时候该条消息就不会推送给用户,导致用户错过该条消息,当然这是概率极低的。

  2. Bloom Filter原理

    每一个布隆过滤器,在 Redis 中都对应了一个大型的位数组以及几个不同的 hash 函数。

    所谓的 add 操作是这样的:

    首先根据几个不同的 hash 函数给元素进行 hash 运算一个整数索引值,拿到这个索引值之后,对位数组的长度进行取模运算,得到一个位置,每一个 hash 函数都会得到一个位置,将位数组中对应的位置设置位 1 ,这样就完成了添加操作。

    当判断元素是否粗存在时,依然先对元素进行 hash 运算,将运算的结果和位数组取模,然后去对应的位置查看是否有相应的数据,如果有,表示元素可能存在(因为这个有数据的地方也可能是其他元素存进来的),如果没有表示元素一定不存在

    Bloom Filter 中,误判的概率和位数组的大小有很大关系,位数组越大,误判概率越小,当然占用的存储空间越大;位数组越小,误判概率越大,当然占用的存储空间就小。

  3. 编译安装

    cd redis-5.0.7
    git clone
    cd RedisBloom/
    make
    cd ..
    redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so
    

    安装完成后,执行 bf.add 命令,测试安装是否成功。
    每次启动时都输入 redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so 比较麻烦,我们可以将要加载的模块在 redis.conf 中提前配置好。

    loadmodule /root/redis-5.0.7/RedisBloom-2.2.6/redisbloom.so
    

    配置完成后,以后只需要 redis-server redis.conf 来启动 Redis

  4. 基本用法

    添加和判断是否存在

    • bf.add\bf.add 添加和批量添加
    • bf,exists\bf.mexists 判断是否存在和批量判断

    Jedis操作布隆过滤器

    • 添加依赖

      <dependency>
        <groupId>com.redislabs</groupId>
        <artifactId>jrebloom</artifactId>
        <version>1.2.0</version>
      </dependency>
      
    • 测试

      public class BloomFilter {
          public static void main(String[] args) {
              GenericObjectPoolConfig config = new GenericObjectPoolConfig();
              config.setMaxIdle(300);
              config.setMaxTotal(1000);
              config.setMaxWaitMillis(30000);
              config.setTestOnBorrow(true);
      
              JedisPool pool=new JedisPool(config,"ip地址",6379,30000,"xiaozhong");
      
              //BloomFilter对象
              Client client = new Client(pool);
              //for (int i = 0; i <100000 ; i++) {
              //    client.add("name","xiaoxin-"+i);
              //    System.out.println(i);
              //}
      
              boolean exists = client.exists("name", "xiaoxin-111111");
              System.out.println(exists);
          }
      }
      
      

      默认情况下,使用的布隆过滤器它的错误率是0.01,默认的元素大小是100.但是这两个参数是可以配置的

      调用bf.serve方法进行配置

      bf.serve k1 0.0001 100000
      

      第一个参数是 key,第二个参数是错误率,错误率越低,占用的空间越大,第三个参数预计存储的数量,当实际数量超出预计数量时,错误率会上升。

  5. 典型场景

    新闻推送过滤器

    解决Redis穿透(缓存击穿)问题

    假设我有 1亿 条用户数据,现在查询用户要去数据库中查,效率低而且数据库压力大,所以我们会把请求首先在 Redis 中处理(活跃用户存在 Redis 中),Redis 中没有的用户,再去数据库中查询。

    现在可能会存在一种恶意请求,这个请求携带上了很多不存在的用户,这个时候 Redis 无法拦截下来请求,所以请求会直接跑到数据库里去。这个时候,这些恶意请求会击穿我们的缓存,甚至数据库,进而引起“雪崩效应”。

    为了解决这个问题,我们就可以使用布隆过滤器。将 1亿条用户数据存在 Redis 中不现实,但是可以存在布隆过滤器中,请求来了,首先去判断数据是否存在,如果存在,再去数据库中查询,否则就不去数据库中查询。

posted @ 2021-09-02 11:08  Ishton  阅读(754)  评论(0)    收藏  举报