redis bitmap实现签到(包含工具类)

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

  • 签到 1 天送 10 积分,连续签到 2 天送 20 积分,3 天送 30 积分,4 天以上均送 50 积分等

  • 如果连续签到中断,则重置计数,每月初重置计数

  • 显示用户某个月的签到次数

  • 在日历控件上展示用户每月签到情况,可以切换年月显示

bitmaps

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

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

优点

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

在Java代码中实现此功能:
工具类:
package alioss.utils;

import cn.hutool.core.date.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Component
public class SignUtils {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 签到
     * @param userId  用户id
     * @param date 日期
     * @return
     */
    public String sign(int userId, Date date){
        String key = buildSignKey(userId, date);
        int dayOfMonth = DateUtil.dayOfMonth(date);
        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth - 1,true);
        return "签到成功";
    }

    /**
     * 获取连续签到次数
     * @param userId 用户id
     * @param date 日期
     * @return
     */
    public Integer getContinuousSignCount(int userId,Date date){
        String key = buildSignKey(userId, date);
        int dayOfMonth = DateUtil.dayOfMonth(date);
        //获取用户从当前日期开始到1号的签到状态
        List<Long> list = stringRedisTemplate.opsForValue().bitField(
          key,
          BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
        );
        if (list == null || list.isEmpty()){
            return 0;
        }
        //连续签到计数器
        int signCount = 0;
        long v = list.get(0) == null ? 0 : list.get(0);
        //位移运算连续签到次数
        for (int i = dayOfMonth; i > 0; i--){
            //i表示位移操作的次数,右移再左移如果等于自己说明最低位是0,表示未签到
            if (v >> 1 << 1 == v){
                //用户可能还未签到,所以要排除是否是当天的可能性
                if (i != dayOfMonth) break;
            }else {
                //右移再左移,如果不等于自己说明最低位是1,表示签到
                signCount++;
            }
            v >>= 1;
        }
        return signCount;
    }

    /**
     * 获取本月累计签到数
     * @param userId
     * @param date
     * @return
     */
    public long getSumSignCount(int userId,Date date){
        String key = buildSignKey(userId, date);
        int dayOfMonth = DateUtil.dayOfMonth(date);
        return stringRedisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount(key.getBytes()));
    }

    /**
     * 查询当天是否有签到
     * @param userId 用户id
     * @param date 日期
     * @return
     */
    public boolean checkSign(int userId,Date date){
        String key = buildSignKey(userId, date);
        int dayOfMonth = DateUtil.dayOfMonth(date);
        return stringRedisTemplate.opsForValue().getBit(key,dayOfMonth - 1);
    }

    /**
     * 获取本月签到信息
     * @param userId 用户id
     * @param date 日期
     * @return
     */
    public Map<String,String> getSignInfo(int userId,Date date){
        String key = buildSignKey(userId, date);
        int dayOfMonth = DateUtil.dayOfMonth(date);
        Map<String,String> signMap = new LinkedHashMap<>(dayOfMonth);
        //获取BitMap中的bit数组,并以十进制返回
        List<Long> bitFieldList = (List<Long>) stringRedisTemplate.execute((RedisCallback<List<Long>>) cbk
                -> cbk.bitField(key.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)));
        if (bitFieldList != null && bitFieldList.size() > 0){
            Long valueDec = bitFieldList.get(0) != null ? bitFieldList.get(0) : 0;
            //使用i--,从最低位开始处理
            for (int i = dayOfMonth; i > 0; i--) {
                LocalDate tempDayOfMonth = LocalDate.now().withDayOfMonth(i);
                //valueDec先右移一位再左移以为得到一个新值,这个新值最低位的二进制为0,再与valueDec做比较,如果相等valueDec的最低位是0,否则是1
                if (valueDec >> 1 << 1 != valueDec){
                    signMap.put(tempDayOfMonth.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),"1");
                }else {
                    signMap.put(tempDayOfMonth.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),"0");
                }
                //每次处理完右移一位
                valueDec >>= 1;
            }
        }
        return signMap;
    }

    /**
     * 构建redis Key  user:sign:userId:yyyyMM
     * @param userId 用户id
     * @param date 日期
     * @return
     */
    public String buildSignKey(int userId,Date date){
        return String.format("user:sign:%s:%s",userId, DateUtil.format(date,"yyyyMM"));
    }
}
接口实现:
package alioss.controller;

import alioss.entity.dto.SignDto;
import alioss.utils.SignUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.Map;

/**
 * 签到功能
 */
@Slf4j
@RestController
@RequestMapping
public class SignController {

    @Autowired
    private SignUtils signUtils;

    /**
     * 本月连续签到次数
     * @param userId
     * @param date
     * @return
     */
    @GetMapping("getContinuousSignCount")
    public Integer getContinuousSignCount(@RequestParam("userId") int userId,@RequestParam("date") Date date){
        return signUtils.getContinuousSignCount(userId,date);
    }

    /**
     * 获取累计签到数
     * @param userId
     * @param date
     * @return
     */
    @GetMapping("getSumSignCount")
    public long getSumSignCount(@RequestParam("userId") int userId,@RequestParam("date") Date date){
        return signUtils.getSumSignCount(userId,date);

    }

    /**
     * 签到
     * @return
     */
    @PostMapping("/sign")
    public  String  sign(@RequestBody SignDto signDto){
        int userId = signDto.getUserId();
        Date date = signDto.getDate();
        return signUtils.sign(userId,date);
    }


    /**
     * 签到结果
     * @param userId
     * @param date
     * @return
     */
    @GetMapping("/getSignResult")
    public boolean getSignResult(@RequestParam("userId") int userId,@RequestParam("date") Date date){
        return signUtils.checkSign(userId,date);
    }

    /**
     * 签到信息
     * @param userId
     * @param date
     * @return
     */
    @GetMapping("/getSignInfo")
    public Map<String,String> getSignInfo(@RequestParam("userId") int userId,@RequestParam("date") Date date){
        return signUtils.getSignInfo(userId,date);
    }
}

redis中签到二进制数据:

签到:

获取签到结果:

本月累计签到天数:

本月连续签到天数:

每日签到详情:

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值