1.签到功能的实现思路
最近有研究到用户的签到功能,对功能进行设计的时候想到使用msyql存储用户的签到记录,将用户的每日签到记录存储到表中,然后又想到每次签到就往表里面存一条记录,用户量小还好,如果是一些互联网应用,用户量体量比较大的话,每天往表里面新增几万几十万的记录,随着数据的一直增加,就会带来很大的IO消耗,而且这些数据占据的存储空间也会越来越大,甚至会带来性能问题。
最近在深入学习redis,发现它里面的bitmap数据结构就能很好的用户签到功能的实现,而且签到数据占用的内存也很小,查询统计的性能也不错,很好的解决了常规思路存在的问题。
bitmap也叫位图,可以理解成二进制的bit数组,数组每位只有两种数字:0或1。使用Reids bitmap实现签到功能的思路就是将对应下标数字中的二进制数字置为1(即标记为签到).
比如: 0 0 0 1 1 0 0 这个这个二进制中第四天和第五天签到了,因为第四为和第五位的值是1.
redis bitmap 常用命令:
#向指定位置(offset)存入0或1
SETBIT key offset value
#获取指定位置(offset)的bit值
GETBIT key offset
#统计bitmap中(从start到end,如果不写起始位置,就统计整个key)值为1的bit数量
BITCOUNT key [start end]
#操作(查询,修改,自增)Bitmap中指定的位置(offset)的值
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
#获取Bitmap中的bit数组,并以十进制返回
BITFIELD_RO
2.java代码实现签到功能
以下使用RedisTemplate操作Redis
@Autowired
public RedisTemplate redisTemplate;
2.1 实现签到功能
public void sign() {
// 组合成key= sign:userId:年月
String key = buildSignKey(1234, LocalDate.now());
// 获取当前日期是当月的第多少天
LocalDate now = LocalDate.now();
int dayOfMonth = now.getDayOfMonth();
// 存到redis中的bitmap中,由于bitmap从0开始,第多少天从1开始,dayOfMonth需要减1
redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
log.info("用户签到:" + key);
}
2.2 查询当天是否有签到
/**
* 查询当天是否有签到
*
* @param uid 用户id
* @param localDate localDate 对象
* @return
*/
public boolean checkSign(int uid, LocalDate localDate) {
return redisTemplate.opsForValue().getBit(buildSignKey(uid, localDate), localDate.getDayOfMonth() - 1);
}
2.3 获取连续签到的次数
/**
* 获取连续签到的次数
* @return
*/
@Override
public int getSign() {
//用户Uid
String userId = "1234";
//获取当前日期
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
// 组合成key= sign:userId:年月
String key = buildSignKey(1234, LocalDate.now());;
// 获取当前日期是当月的第多少天
int dayOfMonth = now.getDayOfMonth();
// 获取本月截止今天为止的所有签到记录,返回的是一个十进制的数字
List<Long> result = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
log.info("查询数据:{}", result);
if (result == null || result.isEmpty()) {
return 0;
}
Long number = result.get(0);
int count = 0;
if (number == null || number == 0) {
return 0;
}
while (true) {
// 让这个数字与1做与运算,得到数字的最后一位bit,并判断这个bit是否为0
if ((number & 1) == 0) {
// 如果为0说明不是连续签到,直接终止
break;
} else {
// 如果不为0,那就为1,说明有签到,继续下次循环,并且计数器加一
count++;
}
// 把数字右移一位,抛弃最后一位的bit,继续下一个bit
number = number >>> 1; // 也可以写成 number >>>= 1
}
return count;
}
2.4 获取当月首次签到日期
/**
* 获取当月首次签到日期
*
* @param uid 用户ID
* @param date 日期
* @return 首次签到日期
*/
public LocalDate getFirstSignDate(int uid, LocalDate date) {
long bitPosition = (Long) redisTemplate.execute((RedisCallback) cbk -> cbk.bitPos(buildSignKey(uid, date).getBytes(), true));
return bitPosition < 0 ? null : date.withDayOfMonth((int) bitPosition + 1);
}
2.5 获取本月签到信息
/**
* 获取本月签到信息
* @param uid
* @param localDate
* @return
*/
public Map<String, String> getSignInfo(int uid, LocalDate localDate) {
Map<String, String> signMap = new LinkedHashMap<>(localDate.getDayOfMonth());
int lengthOfMonth = localDate.lengthOfMonth();
//获取Bitmap中的bit数组,并以十进制返回
List<Long> bitFieldList = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) cbk
-> cbk.bitField(buildSignKey(uid, localDate).getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(lengthOfMonth)).valueAt(0)));
if (bitFieldList != null && bitFieldList.size() > 0) {
long valueDec = bitFieldList.get(0) != null ? bitFieldList.get(0) : 0;
//使用i--,从最低位开始处理
for (int i = lengthOfMonth; i > 0; i--) {
LocalDate tempDayOfMonth = LocalDate.now().withDayOfMonth(i);
//valueDec先右移1位再左移1位得到一个新值,这个新值最低位的二进制值为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");
}
//每次处理完右移1位
valueDec >>= 1;
}
}
return signMap;
}
2.6 工具方法
/**
* 拼接rediskey
* @param uid 用户id
* @param date 日期对象
* @return
*/
private static String buildSignKey(int uid, LocalDate date) {
return String.format("sign:%s:%s", uid, formatDate(date));
}
/**
* 日期格式化
* @param date
* @return
*/
private static String formatDate(LocalDate date) {
return formatDate(date, "yyyyMM");
}