9

一、使用redis-bitmap背景和优势

1.项目背景
  • 最近做的需求中,有个每日签到功能,需要用户每日签到,并且统计累计签到了多少天等这样的信息。
  • 在思考项目方案的时候,最简单的就是mysql,记录下用户每天签到的数据,每个用户每天签到的时候都insert一条数据,利用mysql统计起来也超级方便,然而;
  • 思考一个问题,一个用户一年的签到数据将是365条,如果用户数大的话,将是365*用户数这么多条记录,这些记录仅仅是一个状态数据,一个签到状态实际上用“1”或者“0”这样的数字就能完全表示出来,那么有没有更好更节省存储空间的方式呢?
  • 实际上,遇到这种只需要存储 “是”或者“否” 状态的数据的时候,可以尝试是否可以使用bitmap这种数据结构来存储数据。
2.什么是bitmap
  • bitmap可以想象成一个围棋盘这种点位阵,每个格子代表一个bit位,只能存储1或者0两种数据。
  • 因此,bitmap使用场景实际上就是只能存储“状态”类型数据,比如我们将在项目中用到的签到数据,因为签到状态只有两种,一种已签到可以用“1”表示,一种未签到可以用“0”表示。
3.bitmap优势
  • bitmap最大的优势就是可以存储海量数据并且快速检索到具体的数据。
  • 我们再回到之前说的签到功能的mysql方案,一个用户一天的签到数据包含uid、sign_status、created_at,最起码三个int类型字段,一个int类型是4个字节,三个int就是12个字节,也就是一个用户一天的数据最低占12个字节,千万用户一年的数据是1000w x 365 x 12=438000w字节=42773M,请记住这个42773M,是mysql存储占用的空间,大约41G。
  • 那么我们来计算一下用bitmap存储千万用户一年的数据占据多少空间。假如我们用户uid大部分是连续的,那么每天所有的用户签到数据都存在一个key,比如uid=1000的用户签到,就将这个key的第1000位设置为1,这样实际上所有用户一年的数据只有365条,一条数据保存1000w个bit,由于一个字节8个bit,就是125w字节,那么千万用户一年的数据是125w x 365=45625w字节=445M。
  • 以上,我们知道,使用mysql保存一年千万用户最低42773M空间,而使用bitmap保存一年千万用户最低445M,bitmap空间只占了mysql的1%,这个差别真的是天差地别。
  • 另外bitmap查询数据的时候,redis提供了相关命令可以直接统计一个key中有多少个1,也可以直接获取到具体那个位bit的值。
  • 其中,在一台2010MacBook Pro上,offset为2^32-1(分配512MB)需要~300ms,offset为2^30-1(分配128MB)需要~80ms,offset为2^28-1(分配32MB)需要~30ms,offset为2^26-1(分配8MB)需要8ms。所以查询某个位的值的时候速度也是相当快,redis中一个string类型的key限制不能超过512M。

二、redis-bitmap相关命令介绍

  1. Setbit

     语法:setbit key offset value
    
      描述:
    
        对key所储存的字符串值,设置或清除指定偏移量上的位(bit)。
    
        位的设置或清除取决于 `value` 参数,可以是 `0` 也可以是 `1` 。
    
        当 `key` 不存在时,自动生成一个新的字符串值。
    
        字符串会进行伸展(grown)以确保它可以将 `value` 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 `0` 填充。
    
     注意:
    
        `offset` 参数必须大于或等于 `0` ,小于 2^32 (bit 映射被限制在 512 MB 之内)。
    
        因为 Redis 字符串的大小被限制在 512 兆(megabytes)以内, 所以用户能够使用的最大偏移量为 2^29-1(536870911) , 如果你需要使用比这更大的空间, 请使用多个 `key。`
    
        当生成一个很长的字符串时, Redis 需要分配内存空间,  该操作有时候可能会造成服务器阻塞(block)。 在2010年出产的Macbook Pro上, 设置偏移量为 536870911(512MB  内存分配)将耗费约 300 毫秒, 设置偏移量为 134217728(128MB 内存分配)将耗费约 80 毫秒, 设置偏移量  33554432(32MB 内存分配)将耗费约 30 毫秒, 设置偏移量为 8388608(8MB 内存分配)将耗费约 8 毫秒。
  2. getbit

       语法:getbit key offset
    
       描述:
    
        对 key 所储存的字符串值,获取指定偏移量上的位(bit)。
    
        当 offset 比字符串值的长度大,或者 key 不存在时,返回 0
  3. bitcount

        语法:bitcount key [start] [end]
    
      返回值:被设置为 1 的位的数量
    
      描述:
    
        计算给定字符串中,被设置为 1 的比特位的数量
    
        一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的字节上进行。注意不是bit位,是字节。
          例如:假如key1的value是00001100 11001000 11110000
          <1> bitcount key1 0 0 
              这个是获取key1中第0个字节组中bit为1的count,也就是00001100 中查询,返回2
          <2> bitcount key1 0 1 
              这个是获取key1中第0-1个字节组中bit为1的count,也就是00001100 11001000中查询,返回5  
          <3> bitcount key1 1 2 
              这个是获取key1中第1-2个字节组中bit为1的count,也就是11001000 11110000中查询,返回7   
    
        start 和 end 参数的设置和 GETRANGE key start end 命令类似,都可以使用负数值: 比如 -1表示最后一个bit, -2 表示倒数第二个bit,以此类推。
    
        不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
  4. bitpos

       语法: bitpos key bit [start] [end] 
         
      返回值:返回字符串里面第一个被设置为1或者0的bit位。
    
      描述:
    
        返回一个位置,把字符串当做一个从左到右的字节数组,第一个符合条件的在位置0,其次在位置8,等等。
    
        默认情况下整个字符串都会被检索一次,只有在指定start和end参数(指定start和end位是可行的),该范围被解释为一个字节的范围,而不是一系列的位。所以start=0 并且 end=2是指前三个字节范围内查找。
  5. bitop

       语法:bitop operation destkey key [key ...]
    
        operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:
    
              BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
              BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
              BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
              BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。
    
        除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。    
    
      返回值:保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等
    
      描述:
    
        对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
    
      注意:处理不同长度的字符串
    
        当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0 。
    
        空的 key 也被看作是包含 0 的字符串序列。
  6. bitfield

       语法:bitfield key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
    
         
    
      描述:
    
        该命令将 Redis 字符串视为一个位数组,并且能够处理具有不同位宽和任意非(必要)对齐偏移量的特定整数字段。
        实际上,使用此命令可以将位偏移量为1234的带符号5位整数设置为特定值,从偏移量4567中检索31位无符号整数。类似地,该命令处理指定整数的递增和递减,提供保证和良好指定的溢出和下溢行为,用户可以配置。
    
      注意:详情可以查看文章:https://cloud.tencent.com/developer/section/1374165

三、bitmap应用场景

1.用户每日签到
  • 用户每日签到,可以使用redis-bitmap,关于key的保存方式,有不同方案,一种是一个key保存一天内所有用户的签到状态,这种方式注意,如果大部分签到的用户uid并不连续,那么整个key看到将会有大量的00000000,只有零星的1,所以这种方式适用于连续uid数据保存;
  • 另一种key存值方式,就是以key=uid_year_month保存一个用户一个月的签到数据,这种适合用户量少的方式。
2.统计用户活跃用户
  • 使用时间作为key,用户uid作为offset,活跃过就设置为1
  • 遇到同样的问题,就是uid不连续怎么办?
  • 这种方式可以考虑分片保存:例如uid对1000取模得到的数都是小于1000的,uid小于1000的作为分片1,uid在1000~2000作为分片2,那么可以设置key=分片id,具体的位就是uid对1000取模得到的值,这样保存的时候只是需要先确定当前uid在哪个分片即可。

四、思考及其他注意的点

  1. 具体的bitmap保存方式,要尽量考虑自己业务情况,通过计算具体哪种方式保存更节省空间来确定。
  2. redis中一个key限制不能大于512M,bit位个数尽量小于2^29-1(536870911),大约5亿,别最后超限制了。

繁星落眼眶
626 声望54 粉丝