项目环境
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 {@ResourceRedisTemplate redisTemplate;@Resourceprivate 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 keyString 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 keyString 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 KeyString signKey = buildSignKey(userId, date);//e.g BITCOUNT user:sign:5:202306return (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 {@Resourceprivate 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 {@Resourceprivate RedisUtils redisUtils;@Resourceprivate DailySignService dailySignService;@Test/*** 测试写入数据到redis中 格式为json* */public void contextLoads(){RedisInfo redisInfo = new RedisInfo();redisInfo.setId(1);redisInfo.setName("morey");redisInfo.setCreateTime(redisInfo.getCreateTime());//写入redisredisUtils.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版本太低导致