很多应用比如签到送积分、签到领取奖励:
- 签到 1 天送 10 积分,连续签到 2 天送 20 积分,3 天送 30 积分,4 天以上均送 50 积分等
- 如果连续签到中断,则重置计数,每月初重置计数
- 显示用户某个月的签到次数
- 在日历控件上展示用户每月签到情况,可以切换年月显示
一、使用MYSQL
最简单的设计思路就是利用MySQL保存签到数据(t_user_sign),如下:
字段名 | 描述 |
---|---|
if | 数据表主键(AUTO_INCREMENT) |
fk_diner_id | 用户 ID |
sign_date | 签到日期(如 2022-0.-31) |
amount | 连续签到天数(如 2) |
- 用户签到:往此表插入一条数据,并更新连续签到天数
- 查询根据签到日期查询
- 统计根据 amount 统计
如果这样存数据,对于用户量大的应用,db可能扛不住,比如 1000W 用户,一天一条,那么一个月就是 3 亿数据,非常庞大。
二、使用Redis中的bitmap
Bitmaps,位图,不是 Redis 的基本数据类型(比如 Strings、Lists、Sets、Hashes),而是基于 String 数据类型的按位操作,高阶数据类型的一种。Bitmaps 支持最大位数 232 位。使用 512M 内存就可以存储多达 42.9 亿的字节信息(232 = 4,294,967,296)。
它由一组 bit 位组成,每个 bit 位对应 0 和 1 两个状态,虽然内部还是采用 String 类型存储,但 Redis 提供了一些指令用于直接操作位图,可以把它看作是一个 bit 数组,数组的下标就是偏移量。
优点
内存开销小、效率高且操作简单,很适合用于签到这类场景。比如按月进行存储,一个月最多 31 天,那么我们将该月用户的签到缓存二进制就是 00000000000000000000000000000000,当某天签到将 0 改成 1 即可,而且 Redis 提供对 bitmap 的很多操作比如存储、获取、统计等指令,使用起来非常方便。
常用命令
命令 | 功能 | 参数 |
---|---|---|
SETBIT | 指定偏移量 bit 位置设置值 | key offset value【0=< offset< 2^32】 |
GETBIT | 查询指定偏移位置的 bit 值 | key offset |
BITCOUNT | 统计指定字节区间 bit 为 1 的数量 | key [start end]【@LBN】 |
BITFIELD | 操作多字节位域 | key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP/SAT/FAIL] |
BITPOS | 查询指定字节区间第一个被设置成 1 的 bit 位的位置 | key bit [start] [end]【@LBN】 |
位运算判断是否签到
签到
@GetMapping("/doSign")
public Map<String ,Object> doSign(String dateStr){
Map<String ,Object> map = new HashMap<>();
Date date = getDate(dateStr);
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
//获取日期对应的天数,多少号 从0开始
int offset = localDateTime.getDayOfMonth();
//构建key 【user:sign:1:yyyyMM】
//这里userId就模拟一个1,实际中通过cookie中获取用户信息
String userId = "1";
String signKey = buildSignKey(userId,date);
//查看是否签到
Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);
if (isSign){
map.put("message","今天已签到,明天再来吧~");
return map;
}
//签到
redisTemplate.opsForValue().setBit(signKey,offset,true);
//统计连续签到的次数
int count = getContinuousSignCount(userId,date);
map.put("count",count);
map.put("message","恭喜你签到成功~");
return map;
}
-
获取连续签到天数
private int getContinuousSignCount(String userId, Date date) { //获取日期对应的天数,多少号,假设是30 LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); int dayOfMonth = localDateTime.getDayOfMonth(); //构建key String signKey = buildSignKey(userId, date); // bitfield user:sign:1:202203 get u30 0 BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)) .valueAt(0); List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands); if (list == null || list.isEmpty()){ return 0; } int signCount = 0; long v = list.get(0) == null ? 0 : list.get(0); //i表示位移操作次数 for (int i = dayOfMonth; i > 0 ; i--){ //右移再左移,如果等于自己说明最低位是0 ,表示没有签到 if (v >> 1 << 1 == v){ //低位0 ,且非当天说明连续中断了 if (i != dayOfMonth){ break; } }else { signCount++; } //右移一位并重新赋值,相当于把最低位丢弃 v >>= 1; } return signCount; }
-
构建key 和 时间转换
private String buildSignKey(String userId, Date date) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM"); return "user:" + userId + ":" + simpleDateFormat.format(date); } private Date getDate(String dataStr) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); try { return simpleDateFormat.parse(dataStr); } catch (ParseException e) { e.printStackTrace(); } return new Date(); }
**统计用户签到情况(按月统计)**
获取某月签到情况,默认当月:
-
获取登录用户信息
-
构建 Redis 保存的 Key
-
获取月份的总天数(考虑 2 月闰、平年)
-
通过 BITFIELD 指令获取当前月的所有签到数据
-
遍历进行判断是否签到,并存入 TreeMap 方便排序
@GetMapping("/getSignInfo") public Map<String ,Object> getSignInfo(String dateStr){ Map<String ,Object> map = new TreeMap<>(); Date date = getDate(dateStr); //获取日期对应的天数,多少号,假设是30 LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); int dayOfMonth = localDateTime.getDayOfMonth(); //构建key String userId = "1"; String signKey = buildSignKey(userId, date); // bitfield user:sign:1:202203 get u30 0 BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)) .valueAt(0); List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands); if (list == null || list.isEmpty()){ return map; } long v = list.get(0) == null ? 0 : list.get(0); //i表示位移操作次数 for (int i = dayOfMonth; i > 0 ; i--){ localDateTime= localDateTime.withDayOfMonth(i); boolean flag = v >> 1 << 1 != v; map.put(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),flag); v >>= 1; } return map; }