使用Redis的Bitmaps位图手写用户签到功能


很多应用比如签到送积分、签到领取奖励:

  • 签到 1 天送 10 积分,连续签到 2 天送 20 积分,3 天送 30 积分,4 天以上均送 50 积分等
  • 如果连续签到中断,则重置计数,每月初重置计数
  • 显示用户某个月的签到次数
  • 在日历控件上展示用户每月签到情况,可以切换年月显示

一、使用MYSQL

最简单的设计思路就是利用MySQL保存签到数据(t_user_sign),如下:

字段名描述
if数据表主键(AUTO_INCREMENT)
fk_diner_id用户 ID
sign_date签到日期(如 2022-0.-31)
amount连续签到天数(如 2)
  • 用户签到:往此表插入一条数据,并更新连续签到天数
  • 查询根据签到日期查询
  • 统计根据 amount 统计

如果这样存数据,对于用户量大的应用,db可能扛不住,比如 1000W 用户,一天一条,那么一个月就是 3 亿数据,非常庞大。

二、使用Redis中的bitmap

Bitmaps,位图,不是 Redis 的基本数据类型(比如 Strings、Lists、Sets、Hashes),而是基于 String 数据类型的按位操作,高阶数据类型的一种。Bitmaps 支持最大位数 232 位。使用 512M 内存就可以存储多达 42.9 亿的字节信息(232 = 4,294,967,296)。

它由一组 bit 位组成,每个 bit 位对应 0 和 1 两个状态,虽然内部还是采用 String 类型存储,但 Redis 提供了一些指令用于直接操作位图,可以把它看作是一个 bit 数组,数组的下标就是偏移量。


优点


内存开销小、效率高且操作简单,很适合用于签到这类场景。比如按月进行存储,一个月最多 31 天,那么我们将该月用户的签到缓存二进制就是 00000000000000000000000000000000,当某天签到将 0 改成 1 即可,而且 Redis 提供对 bitmap 的很多操作比如存储、获取、统计等指令,使用起来非常方便。

常用命令


命令功能参数
SETBIT指定偏移量 bit 位置设置值key offset value【0=< offset< 2^32】
GETBIT查询指定偏移位置的 bit 值key offset
BITCOUNT统计指定字节区间 bit 为 1 的数量key [start end]【@LBN】
BITFIELD操作多字节位域key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP/SAT/FAIL]
BITPOS查询指定字节区间第一个被设置成 1 的 bit 位的位置key bit [start] [end]【@LBN】

位运算判断是否签到


在这里插入图片描述
签到


 @GetMapping("/doSign")
    public Map<String ,Object> doSign(String dateStr){
        Map<String ,Object> map = new HashMap<>();
        Date date = getDate(dateStr);
        LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        //获取日期对应的天数,多少号 从0开始
        int offset = localDateTime.getDayOfMonth();
        //构建key 【user:sign:1:yyyyMM】
        //这里userId就模拟一个1,实际中通过cookie中获取用户信息
        String userId = "1";
        String signKey = buildSignKey(userId,date);
        //查看是否签到
        Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);

        if (isSign){
            map.put("message","今天已签到,明天再来吧~");
            return  map;
        }
        //签到
        redisTemplate.opsForValue().setBit(signKey,offset,true);

        //统计连续签到的次数
        int count = getContinuousSignCount(userId,date);
        map.put("count",count);
        map.put("message","恭喜你签到成功~");
        return map;
    }
  • 获取连续签到天数

    	private int getContinuousSignCount(String userId, Date date) {
            //获取日期对应的天数,多少号,假设是30
            LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
            int dayOfMonth = localDateTime.getDayOfMonth();
            //构建key
            String signKey = buildSignKey(userId, date);
            // bitfield user:sign:1:202203 get u30 0
            BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                    .valueAt(0);
            List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
            if (list == null || list.isEmpty()){
                return  0;
            }
            int signCount = 0;
            long v = list.get(0) == null ? 0 : list.get(0);
    
            //i表示位移操作次数
            for (int i = dayOfMonth; i > 0 ; i--){
                //右移再左移,如果等于自己说明最低位是0 ,表示没有签到
                if (v >> 1 << 1 == v){
                    //低位0 ,且非当天说明连续中断了
                    if (i != dayOfMonth){
                        break;
                    }
                }else {
                    signCount++;
                }
                //右移一位并重新赋值,相当于把最低位丢弃
                v >>= 1;
            }
            return signCount;
        }
    
  • 构建key 和 时间转换

    	private String buildSignKey(String userId, Date date) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM");
    
            return "user:" + userId + ":" + simpleDateFormat.format(date);
        }
    
        private Date getDate(String dataStr) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            try {
                return simpleDateFormat.parse(dataStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return new Date();
        }
    

**统计用户签到情况(按月统计)**

获取某月签到情况,默认当月:

  • 获取登录用户信息

  • 构建 Redis 保存的 Key

  • 获取月份的总天数(考虑 2 月闰、平年)

  • 通过 BITFIELD 指令获取当前月的所有签到数据

  • 遍历进行判断是否签到,并存入 TreeMap 方便排序

    @GetMapping("/getSignInfo")
        public Map<String ,Object> getSignInfo(String dateStr){
            Map<String ,Object> map = new TreeMap<>();
            Date date = getDate(dateStr);
            //获取日期对应的天数,多少号,假设是30
            LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
            int dayOfMonth = localDateTime.getDayOfMonth();
            //构建key
            String userId = "1";
            String signKey = buildSignKey(userId, date);
            // bitfield user:sign:1:202203 get u30 0
            BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                    .valueAt(0);
            List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
            if (list == null || list.isEmpty()){
                return  map;
            }
    
            long v = list.get(0) == null ? 0 : list.get(0);
    
            //i表示位移操作次数
            for (int i = dayOfMonth; i > 0 ; i--){
                localDateTime= localDateTime.withDayOfMonth(i);
                boolean flag = v >> 1 << 1 != v;
                map.put(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),flag);
                v >>= 1;
            }
            return map;
        }
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值