MacOS
springboot: 2.7.12
JDK 11
maven 3.8.6
redis 7.0.11
StringRedisTemplate 的key和value默认都是String类型 可以避免不用写配置类,定义key和value的序列化。
获取用户登录信息
根据日期获取当天是多少号
构建用户id 按月存储key
判断用户是否已经签到
用户签到
返回用户连续签到次数
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 doSign(String userId, String dateStr){
//获取当前用户登录信息
Map 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 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) 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 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);
}
}
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 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();
}
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版本太低导致