功能概述
- 用戶連續(xù)登錄天數(shù)
- 用戶累計(jì)登錄天數(shù)
1. 為什么選用 bitmap(位圖)
占用內(nèi)存更小,性能更高。這里偏實(shí)戰(zhàn),原理的東西就不細(xì)講了。
2. 實(shí)戰(zhàn)
2.1 基礎(chǔ)指令
記錄一個(gè)用戶某天登錄,只需要指令
redis:0> setbit key 6 1
"0"
bitmap 是一個(gè)bit數(shù)組,數(shù)據(jù)結(jié)構(gòu)大概是長(zhǎng)這樣子的:
key 0 0 0 0 0 0 1 0 0
數(shù)字6是這個(gè)數(shù)組的偏移量(index,下標(biāo)從0開(kāi)始),表示第7天簽到了
redis:0> getbit key 6
"1"
查看累計(jì)登錄天數(shù):
redis:0> bitcount key
"1"
因?yàn)?bitfield 指令無(wú)符號(hào)獲取的偏移量最大是63,所以一個(gè)key只存一個(gè)月份的數(shù)據(jù),這樣key的結(jié)構(gòu)可以是這樣:
user:sign:userId:date
bitfield 指令其實(shí)就是獲取這個(gè)key的數(shù)組下標(biāo)的一個(gè)list
bitfield user:sign:5:202105 get u14 0
u 表示無(wú)符號(hào) ,14 表示今天是14號(hào),0 表示索引,即從第一天開(kāi)始
2.2 偽代碼
里面每一步的注釋都寫(xiě)的非常明白,關(guān)鍵點(diǎn)在最后一個(gè)方法的移位操作
// 簽到
public void doSign(Integer userId, String dateStr) {
// 獲取日期
Date date = getDate(dateStr);
// 獲取日期對(duì)應(yīng)的天數(shù),即多少號(hào)
int offset = DateUtil.dayOfMonth(date) - 1;
// 構(gòu)建 key user:sign:id:yyyyMM
String signKey = buildKey(userId, date);
// 查看是否簽到
Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);
AssertUtil.isTrue(isSign, "當(dāng)前日期已簽到");
// 簽到
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"));
}
/**
* 統(tǒng)計(jì)連續(xù)簽到的次數(shù)
*
* @param dinerId
* @param date
* @return
*/
private int getContinuousSignCount(Integer dinerId, Date date) {
// 當(dāng)前日期是幾號(hào)
int dayOfMonth = DateUtil.dayOfMonth(date);
// 構(gòu)建 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;
}
/**
* 遞歸獲取連續(xù)簽到天數(shù)
* @param dinerId
* @param lastMonth
* @return
*/
private int getLastMonthSignCount(Integer dinerId, Date lastMonth) {
// 獲取當(dāng)月最后一天
Date lastDay = DateUtil.endOfMonth(lastMonth);
int dayOfMonth = DateUtil.dayOfMonth(lastDay);
// 構(gòu)建 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 表示無(wú)符號(hào) ,14 表示今天是14號(hào),0 表示索引,即從第一天開(kāi)始
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);
// 移位操作:先右移再左移,結(jié)果未變則表示未簽到,結(jié)果變了則表示簽到了
for (int i = dayOfMonth; i > 0; i--) { // i 表示位移的次數(shù)
if (count >> 1 << 1 == count) {
// 如果低位是0 且低位所在不是當(dāng)天,說(shuō)明連續(xù)簽到中斷
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 移位
待續(xù)...