九,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);
}
相关推荐
码熔burning1 小时前
Redis分片集群
数据库·redis·分片集群
小五Z2 小时前
Redis--主从复制
数据库·redis·分布式·后端·缓存
马达加斯加D2 小时前
缓存 --- 缓存击穿, 缓存雪崩, 缓存穿透
数据库·redis·缓存
Hellc0073 小时前
完整的 .NET 6 分布式定时任务实现(Hangfire + Redis 分布式锁)
redis·分布式·.net
Z_z在努力4 小时前
【Redis】Redis 特性
数据库·redis
normaling6 小时前
五,redis实现优惠卷秒杀(消息队列,分布式锁,全局id生成器,lua脚本)
redis
normaling6 小时前
七,Redis实现共同关注和关注推送
redis
normaling6 小时前
六,Redis实现点赞排行表
redis
xxy!6 小时前
Redis的数据持久化是怎么做的?
数据库·redis·缓存