九,Redis通过BitMap实现用户签到

一,BitMap用法

1.1 引言

假如我们用一张表来存储用户签到信息,其结构应该如下

字段 说明
id 主键
user_id 用户id
year 签到的年
month 签到的月
date 签到的日期
is_backup 是否补签

假如有1千万用户,平均每人每年签到10次,则这张表一年数据量为1亿条,对于数据库来讲压力非常大是不可取的。

解决办法:

  • 模拟签到卡,按月统计用户签到信息,签到记录为1未签到为0

    把每一个bit位对应当月的每一天,形成了映射关系。用0/1标识业务状态,这种思路就叫位图(BitMap)


Redis中是利用string 类型数据结构实现BitMap,因此最大上限是512M,转换为bit是2^32个bit位

1.2 BitMap相关命令

BitMap的操作命令有:

  • SetBit:向指定位置(offset)存入一个0或1

    • 语法:SetBit key offset value
  • GetBit:获取指定位置(offset)的bit值

    • 语法:GetBit key offset
  • BitCount:统计BitMap中值为1的bit位的数量

    • 语法:BitCount key [start end]
  • BitField:操作(查询,修改,自增)BitMap中bit数组中指定位置(offset)的值

    • 语法

      bash 复制代码
      BitField key [Get type offset] [Set type offset value] [IncrBy type offset increment] [OverFlow Wrap|Sat|Fail ]
      #参数说明
      # key:要操作的 Redis 键。
      # [Get type offset]:获取指定类型和偏移量的位字段值。
      # [Set type offset value]:设置指定类型和偏移量的位字段值。
      # [IncrBy type offset increment]:对指定类型和偏移量的位字段值进行递增操作。
      # [Overflow Wrap|Sat|Fail]:指定溢出行为,适用于递增操作。
      # ----------------------------------------
      # 数据类型type
      # 分为i和u
      # i:有符号
      # u:无符号
      # i2就代表要查询2个bit位,从offset开始,且返回有符号的数字
      # u2就代表要查询2个bit位,从offset开始,且返回没有符号的数字
      #--------------------------------------------------
      # offset:位字段的偏移量,以位为单位。
      # value:要设置的值。
      # increment:递增的值。
      # Overflow:溢出行为,Wrap(环绕)、Sat(饱和)或 Fail(失败)。
      # ---------------------------------------------------
      # 将 mykey 键中,从偏移量 0 开始的 8 位有符号整数设置为 127。
      BITFIELD mykey SET i8 0 127
      # 取 mykey 键中,从偏移量 0 开始的 8 位有符号整数的值。
      BITFIELD mykey GET i8 0
      # 对 mykey 键中,从偏移量 0 开始的 8 位有符号整数的值递增 1。
      BITFIELD mykey INCRBY i8 0 1
      # 对 mykey 键中,从偏移量 0 开始的 8 位无符号整数的值递增 255,溢出时采取饱和处理。
      BITFIELD mykey INCRBY u8 0 255 OVERFLOW SAT
      • GET 操作返回请求的位字段值(十进制)。

      • SET 操作返回旧值。

      • INCRBY 操作返回递增后的新值。

  • BitField_ro:获取BitMap中bit数组,以十进制形式返回,和BitField用法一样

  • BitOp:将多个BitMap的结果做位运算(与,或,异或)

    • 语法

      bash 复制代码
      BitOp operation destKey key [key...]
      # operation:位运算操作类型。可以是以下之一:
      # AND:按位与操作
      # OR:按位或操作
      # XOR:按位异或操作
      # NOT:按位非操作(仅适用于单个键)
      # destKey:存储操作结果的目标键。
      # key:要进行运算的源键,可以指定一个或多个源键(在 AND、OR、XOR 操作中),或者仅一个源键(在 NOT 操作中)。
      # ----------------------------------------------------------------------------
      # 对 key1、key2 和 key3 进行按位与操作,将结果存储到 destKey 中。
      BITOP AND destKey key1 key2 key3
      # 对 key1 和 key2 进行按位或操作,将结果存储到 destKey 中。
      BITOP OR destKey key1 key2
      # 对 key1 和 key2 进行按位异或操作,将结果存储到 destKey 中。
      BITOP XOR destKey key1 key2
      # 对 key 进行按位非操作,将结果存储到 destKey 中。
      BITOP NOT destKey key
      # BITOP 命令返回一个整数,表示目标键(destKey)的位图大小,以字节为单位。这个大小是所有参与运算的键中最大的位图大小。
  • BitPos:查找bit数组中指定范围内第一个0或1出现的位置

    • 语法BitPos key bit [Start] [End]

二,签到功能

代码实现

java 复制代码
public Result sign(){
    //1.获取当前登录的用户
    Long userId=UserHolder.getUser().getId();
    //2.获取日期
    LocalDateTime now=LocalDateTime.now();
    //3.拼接key
    String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));
    String key="sign:"+userId+keySuffix;
    //4.获取今天是本月的第几天
    int dayOfMonth=now.getDayOfMonth();
    //5.写入redis setBit key offset 1
    stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
    return Result.ok();
}

三,签到统计

连续签到天数:

  • 最后 一次签到开始向前 统计,直到遇到第一次未签到为止,计算总签到的次数,就是连续签到天数。

拿到本月到今天为止的所有签到数据:

  • BitField key get u[dayOfMonth] 0

如何从后向前遍历每个bit位

  • 与1做And运算,就能得到最后一个bit位了
  • 将数字向右移1位,下一位bit位就成了最后一位bit位了。

代码实现:

java 复制代码
public Result signCount(){
     //1.获取当前登录的用户
    Long userId=UserHolder.getUser().getId();
    //2.获取日期
    LocalDateTime now=LocalDateTime.now();
    //3.拼接key
    String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));
    String key="sign:"+userId+keySuffix;
    //4.获取今天是本月的第几天
    int dayOfMonth=now.getDayOfMonth();
    //5.获取本月截至今天为止所有的签到记录,返回的是一个10进制的数字
    //BitField key get u[dayOfMonth] 0
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
    	key,
        BitFieldSubCommands.create()
        	.get(BitFieldType.unsigned(dayOfMonth))
        	.valueAt(0)
    );
    if(result==null || result.isEmpty()){
        //没有签到结果
        return Result.ok(0);
    }
    Long num=result.get(0);
    if(num == null || num == 0){
        //没有签到结果
        return Result.ok(0);
    }
    //6.循环遍历
    int count=0;
    while(true){
        //1.让这个数和1做And位运算,得到数字的最后一位bit位
        if((num & 1) == 0){
            //2.判断这个bit位是否位0
            //如果为0 说明没有签到,结束
            break;
        }else{
         	//如果不为0,说明已签到,计数器+1
        	count++;
        }
        //把数字右移一位,抛弃最后一个bit位,继续下一个bit位
        num>>>=1;
    }
    return Result.ok(count);
}
相关推荐
fanTuanye1 小时前
redis 缓存穿透,缓存击穿,缓存雪崩
java·redis·缓存
星星点点洲8 小时前
【Redis】谈谈Redis的设计
数据库·redis·缓存
Lion Long13 小时前
CodeBuddy 中国版 Cursor 实战:Redis+MySQL双引擎驱动〈王者荣耀〉战区排行榜
数据库·redis·mysql·缓存·腾讯云·codebuddy首席试玩官·codebuddy
柯南二号21 小时前
MacOS 用brew 安装、配置、启动Redis
redis
星星点点洲1 天前
【Redis】RedLock实现原理
redis·缓存
我来整一篇1 天前
用Redis的List实现消息队列
数据库·redis·list
加什么瓦1 天前
Redis——数据结构
数据库·redis·缓存
lybugproducer1 天前
浅谈 Redis 数据类型
java·数据库·redis·后端·链表·缓存
青山是哪个青山1 天前
Redis 常见数据类型
数据库·redis·bootstrap