基于redis的bitmap实现签到功能(后端)

项目环境

MacOS 

springboot: 2.7.12 

JDK 11 

maven 3.8.6 

redis 7.0.11 

StringRedisTemplate 的key和value默认都是String类型 可以避免不用写配置类,定义key和value的序列化。 

实现逻辑:

获取用户登录信息

根据日期获取当天是多少号

构建用户id 按月存储key 

判断用户是否已经签到

用户签到

返回用户连续签到次数

签到功能@Service业务层

package cn.devops.service;

import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

@Service
public class DailySignService {
    @Resource
    RedisTemplate redisTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 用户签到,可以补签
     * @param userId 用户ID
     * @param dateStr 查询的日期,默认当天 yyyy-MM-dd
     * @return 连续签到次数和总签到次数
     * */
    public Map<String, Object> doSign(String userId, String dateStr){
        //获取当前用户登录信息
        Map<String, Object> result = new HashMap<>();
        //获取日期
        Date date = getDate(dateStr);
        //获取日期对应的天数,多少号  偏移量
        int day = dayOfMonth(date) -1;
        //构建redis key
        String signKey = buildSignKey(userId,date);
        //查看指定日期是否已签到
        if (isSigned(userId, day)){
            result.put("message", "当前日期已完成签到,无需再签");
            result.put("code",400);
            return result;
        }
        // 签到
        redisTemplate.opsForValue().setBit(signKey, day, true);
        // 根据当前日期统计签到次数
        Date today = new Date();
        //统计连续签到次数
        int continuous =  getSignCount(userId, today);
        //统计总签到次数
        long count = getSumSignCount(userId, today);
        result.put("message","签到成功");
        result.put("code",200);
        result.put("continuous",continuous);
        result.put("count",count);
        return result;
    }

    /**
     *
     * 格式化日期
     * @param StrDate
     * @return
     * */
    private Date parseDate(String StrDate){
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date myDate = null;
        try{
            myDate = dateFormat.parse(StrDate);
        }catch (ParseException e){
            e.printStackTrace();
        }
        return  myDate;
    }

    private String format(Date date, String format){
        DateFormat dateFormat = new SimpleDateFormat(format);
        String myDate = dateFormat.format(date);
        return myDate;
    }
    /**
     * 获取用户当前的时间
     * @param dateStr yyyy-MM-dd
     * @return
     * */
    private Date getDate(String dateStr) {
        return Objects.isNull(dateStr) ? new Date() : parseDate(dateStr);
    }

    /**
     * 根据日期获取日期所在月份的天数
     * @param date
     * @return
     * */
    private int dayOfMonth(Date date){
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar.get(Calendar.DATE);
    }


    /**
     * 构建Redis key userId:yyyyMM
     * @param userId 用户ID
     * @param date 日期
     * @return
     * */
    private String buildSignKey(String userId, Date date){
        return String.format("img2d_user_daily_sign:%s:%s",userId,format(date,"yyyyMM"));
    }

    /**
     * 统计连续签到次数
     * 如今天16号 无符号 查询16个bit
     * @param userId 用户ID
     * @param date 查询日期
     * @return
     * */
    private int getSignCount(String userId, Date date){
        int dayOfMonth = dayOfMonth(date);
        // 构建 Redis key
        String signKey = buildSignKey(userId,date);
        // 获取日期对应的天数 多少号
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                        .valueAt(0);
        //获取用户从当前日期开始到1号的所有签到状态
        List<Long> signList = stringRedisTemplate.opsForValue().bitField(signKey,bitFieldSubCommands);
        if (signList == null || signList.isEmpty()){
            return 0;
        }
        //连续签到计数器
        int signCount = 0;
        long v = signList.get(0) == null ? 0 : signList.get(0);
        //位移计算连续签到次数
        for (int i = dayOfMonth; i > 0; i--){  //i表示位移操作次数
            //右移再左移,如果等于自己说明最低位是0 表示未签到
            if (v >> 1 << 1 == v){
                // 如果为0 表示未签到 判断是否为当前
                if (i != dayOfMonth) break;
            } else {
                // 右移再左移, 如果不等于自己,说明最低位是1 表示签到
                signCount++;
            }
            //右移一位并重新赋值,相当于把最低位丢弃一位然后重新计算
            v >>= 1;
        }
        return signCount;
    }

    /**
     * 统计总签到次数
     * @param userId 用户ID
     * @param date 查询的日期
     * */
    private Long getSumSignCount(String userId, Date date){
        //构建Redis Key
        String signKey = buildSignKey(userId, date);
        //e.g BITCOUNT user:sign:5:202306
        return (Long) redisTemplate.execute(
                (RedisCallback<Long>) con -> con.bitCount(signKey.getBytes())
        );
    }

    /**
     * 统计月份签到次数
     * @param userId 用户ID
     * @param dateStr 用户日期
     * */
    public String monthSigned(String userId, String dateStr){
        //获取日期
        Date date = getDate(dateStr);
        String signKey = buildSignKey(userId, date);
        //获取日期对应的天数, 多少号,
        int dayOfMonth = dayOfMonth(date);
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        // 获取月份的所有签到状态
        List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
        String total = Long.toBinaryString(list.get(0));
        return total;
    }

    /**
     * 判断用户是否已经签到
     * @param userId   用户ID String
     * @param offset     用户日期
     * */
    private Boolean isSigned(String userId, int offset ){
        //偏移量 offset 从 0 开始
        return redisTemplate.opsForValue().getBit(userId, offset);
    }
}

签到功能 Redis 工具类

package cn.devops.utils;

import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisUtils {
    @Resource
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 读取缓存 redis中key对应的值
     * @param key
     * @return
     * */
    public String get(final String key){
        return redisTemplate.opsForValue().get(key);
    }


    /**
     * 写入String类型 到redis
     * */
    public boolean set(final String key, String value){
        boolean result = false;
        try{
            redisTemplate.opsForValue().set(key, value);
            log.info("存入redis成功,key:{},value:{}",key, value);
            result = true;
        }catch (Exception e){
            log.info("存入redis失败,key:{},value:{}",key, value);
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 写入对象到redis      Json格式
     * */
    public boolean setJsonString(final String key, Object value){
        if (StringUtils.isBlank(key)){
            log.info("redis key值为空");
            return false;
        }
        try{
            redisTemplate.opsForValue().set(key, JSON.toJSONString(value));
            log.info("存入redis成功,key:{},value:{}",key, value);
            return true;
        }catch (Exception e){
            log.info("存入redis失败,key:{},value:{}",key, value);
            e.printStackTrace();
        }
        return false;
    }
    /**
     * 更新缓存
     * */
    public boolean getAndSet(final String key, String value){
        boolean result = false;
        try{
            redisTemplate.opsForValue().getAndSet(key,value);
            result = true;
        }catch (Exception e){
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 删除缓存
     * */
    public boolean delete(final String key){
        boolean result = false;
        try{
            redisTemplate.delete(key);
            result = true;
        }catch (Exception e){
            e.printStackTrace();
        }
        return  result;
    }

    /**
     * 一个指定的 key 设置过期时间
     * @param key
     * @param time
     * */
    public boolean expire(String key, long time){
        return redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    /**
     *  根据key 获取过期时间
     * @param key
     * */
    public long getTime(String key){
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 根据key 获取过期时间
     * @param key
     * */
    public boolean hasKey(String key){
        return redisTemplate.hasKey(key);
    }

    /**
     * 移除指定key 的过期时间
     * @param key
     * */
    public boolean persist(String key){
        return redisTemplate.boundValueOps(key).persist();
    }

签到功能@Test测试类

package cn.devops;

import cn.devops.model.RedisInfo;
import cn.devops.service.DailySignService;
import cn.devops.utils.RedisUtils;
import lombok.Data;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
@Data
public class RedisTest {
    @Resource
    private RedisUtils redisUtils;

    @Resource
    private DailySignService dailySignService;

    @Test
    /**
     * 测试写入数据到redis中 格式为json
     * */
    public  void contextLoads(){
        RedisInfo redisInfo = new RedisInfo();
        redisInfo.setId(1);
        redisInfo.setName("morey");
        redisInfo.setCreateTime(redisInfo.getCreateTime());
        //写入redis
        redisUtils.setJsonString("redisInfo",redisInfo);
        //从redis中获取
        System.out.println("获取redis数据"+redisUtils.get("redisInfo"));
    }

    @Test
    /**
     *   测试签到功能
     * */
    public void testSign(){
        System.out.println(dailySignService.doSign("testUser003", "2023-6-22").toString());
    }

}

效果图: 数据已写入redis中 以二进制的形式。

报错:没有 bitField方法 在idea中报出

stringRedisTemplate.opsForValue().bitField

错误原因: springboot2.0.5版本太低导致, JDK1.8版本太低导致 

redis 常用命令

设置密码

config set requirepass 123456       #123456 为密码

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

村长在路上

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值