功能概述
- 用户连续登录天数
- 用户累计登录天数
1. 为什么选用 bitmap(位图)
占用内存更小,性能更高。这里偏实战,原理的东西就不细讲了。
2. 实战
2.1 基础指令
记录一个用户某天登录,只需要指令
redis:0> setbit key 6 1
"0"
bitmap 是一个bit数组,数据结构大概是长这样子的:
key 0 0 0 0 0 0 1 0 0
数字6是这个数组的偏移量(index,下标从0开始),表示第7天签到了
redis:0> getbit key 6
"1"
查看累计登录天数:
redis:0> bitcount key
"1"
因为 bitfield 指令无符号获取的偏移量最大是63,所以一个key只存一个月份的数据,这样key的结构可以是这样:
user:sign:userId:date
bitfield 指令其实就是获取这个key的数组下标的一个list
bitfield user:sign:5:202105 get u14 0
u 表示无符号 ,14 表示今天是14号,0 表示索引,即从第一天开始
2.2 伪代码
里面每一步的注释都写的非常明白,关键点在最后一个方法的移位操作
// 签到
public void doSign(Integer userId, String dateStr) {
// 获取日期
Date date = getDate(dateStr);
// 获取日期对应的天数,即多少号
int offset = DateUtil.dayOfMonth(date) - 1;
// 构建 key user:sign:id:yyyyMM
String signKey = buildKey(userId, date);
// 查看是否签到
Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);
AssertUtil.isTrue(isSign, "当前日期已签到");
// 签到
redisTemplate.opsForValue().setBit(signKey, offset, true);
}
private String buildKey(Integer dinerId, Date date) {
return String.format("user:sign:%d:%s", dinerId,
DateUtil.format(date, "yyyyMM"));
}
/**
* 统计连续签到的次数
*
* @param dinerId
* @param date
* @return
*/
private int getContinuousSignCount(Integer dinerId, Date date) {
// 当前日期是几号
int dayOfMonth = DateUtil.dayOfMonth(date);
// 构建 key
String key = buildKey(dinerId, date);
int signCount = getSignCountFromRedis(key, dayOfMonth);
if (dayOfMonth == signCount) {
Date lastMonth = DateUtil.offsetMonth(date, -1);
signCount += getLastMonthSignCount(dinerId, lastMonth);
}
return signCount;
}
/**
* 递归获取连续签到天数
* @param dinerId
* @param lastMonth
* @return
*/
private int getLastMonthSignCount(Integer dinerId, Date lastMonth) {
// 获取当月最后一天
Date lastDay = DateUtil.endOfMonth(lastMonth);
int dayOfMonth = DateUtil.dayOfMonth(lastDay);
// 构建 key
String key = buildKey(dinerId, lastMonth);
int signCountFromRedis = getSignCountFromRedis(key, dayOfMonth);
if (signCountFromRedis == dayOfMonth) {
Date lastMonth1 = DateUtil.offsetMonth(lastMonth, -1);
signCountFromRedis += getLastMonthSignCount(dinerId, lastMonth1);
}
return signCountFromRedis;
}
public int getSignCountFromRedis(String key, int dayOfMonth) {
// bitfield user:sign:5:202105 u14 0 ,u 表示无符号 ,14 表示今天是14号,0 表示索引,即从第一天开始
BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands
.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
.valueAt(0);
List<Long> list = redisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
if (list == null || list.isEmpty()) {
return 0;
}
int signCount = 0;
long count = list.get(0) == null ? 0 : list.get(0);
// 移位操作:先右移再左移,结果未变则表示未签到,结果变了则表示签到了
for (int i = dayOfMonth; i > 0; i--) { // i 表示位移的次数
if (count >> 1 << 1 == count) {
// 如果低位是0 且低位所在不是当天,说明连续签到中断
if (i != dayOfMonth) break;
} else {
signCount++;
}
// 把最后一位丢弃
count >>= 1;
}
return signCount;
}
public void setBit(String key, Integer offset) {
redisTemplate.opsForValue().setBit(key, offset, true);
}
public Boolean getBit(String key, Integer offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
public Long bitCount(String key) {
return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
}
public List<Long> bitField(String key, BitFieldSubCommands bitFieldSubCommands) {
return redisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
}
2.3 移位
待续...