很多应用比如签到送积分、签到领取奖励:
- 签到 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中签到二进制数据:
签到:
获取签到结果:
本月累计签到天数:
本月连续签到天数:
每日签到详情: