在很多项目中都会有签到与统计功能,最容易想到的方案是创建一个签到表来记录每个用户的签到记录,比如设计一个mysql数据库表:
CREATE TABLE tb_sign
id bigint(20) unsigned NOT NULL AUTOINCREMENT COMMENT '主键',
user_id bigint(20) unsigned NOT NULL COMMENT '用户ID',
sign_date date NOT NULL COMMENT '签到的日期',
is_backup tinyint(1) unsigned DEFAUL TNULL COMMENT '是否补签',
PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW FORMAT=COMPACT;
用户签到一次就可以往表里添加一条记录;但是这样有一个坏处,就是占用的内存太大了,会极大的消耗内存空间;比如有1万用户,每个用户每个月签到10次,那么一个月就是10万条记录,一年就是120万条;如果用户更多并且签到的次数越多,那么数据量就会更大哦。
签到一次需要使用8+8+3+1 = 20个字节,如果使用redis中的bitmap来实现,每次签到与未签到用1与0来表示,那么只需要2个字节即可了,这样极大的节约了内存;那么接下来认识与使用bitmap。
1.bitmap基本操作指令
SETBIT:向指定位置(offset)存入一个0或1
GETBIT:获取指定位置(offset)的bit值
BITCOUNT:统计BitMap中值为1的bit位的数量
BITFIELD:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
BITFIELD_RO:获取BitMap中bit数组,并以十进制形式返回
BITOP:将多个BitMap的结果做位运算(与 、或、异或)
BITPOS:查找bit数组中指定范围内第一个0或1出现的位置
1.1 新增
1.2 查询
1.3 统计值为1的数量
1.4 查询1 和 0 第一次出现的坐标
2.springboot整合redis
-
创建一个spring boot项目,这里比较简单,不用过多介绍;
-
添加redis依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.x集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
- 配置配置文件
#redis服务器地址
spring.redis.host=127.0.0.1
#redis服务器连接端口
spring.redis.port=6379
#redis数据库索引(默认是0)
spring.redis.database=0
#连接超时时间
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0
- 测试一下
@SpringBootTest
class SpringbootRedisSigninApplicationTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void testRedisSet() {
stringRedisTemplate.opsForValue().set("name", "picacho");
String name = (String)stringRedisTemplate.opsForValue().get("name");
System.out.println(name);
}
}
在redis中也可以看到我们插入进去的数据;
3. 实现
我们可以用年和月作为BitMap的key,然后保存到一个BitMap中,每次签到就把对应的位上把数字从0变为1,如果是1,就表示这一天签到了,反之就表示没有签到。
3.1 实现签到的核心代码
这里主要讨论基本思路和处理流程,因此代码并没有非常规范,仅仅作为示例看待即可;
- UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/sign")
public String sign(){
return userService.sign();
}
}
- UserService
@Service
public class UserService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public String sign(){
// 1.模拟获取用户id
Long userId = 1L;
// 2.获取日期
LocalDateTime now = LocalDateTime.now();
// 3.拼接key字符串
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = "sign:" + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.写入redis
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return "ok";
}
}
- 测试一下
- 查看redis,可以看到本月26号完成了签到。
3.2 统计连续签到次数的核心代码
这里先构造几天签到的测试数据便于测试使用;我们这里构造了26,25,24,22号完成了签到。
我们需要获取本月到今天为止的所有签到数据,今天是26号,那么我们就可以从当前月的第一天开始,获得到26号的位数,那么就是26位,去拿这段时间的数据,就能拿到所有的数据了,那么这26天里边签到了多少次呢?统计有多少个1即可。
注意:bitMap返回的数据是10进制,哪假如说返回一个数字8,我们只需要让得到的10进制数字和1做与运算就可以了,因为1只有遇见1才是1,其他数字都是0 ,我们把签到结果和1进行与操作,每与一次,就把签到结果向右移动一位,依次类推即可。
- UserController
@PostMapping("/count")
public String countSign(){
return userService.countSign();
}
- UserService
public Integer countSign(){
// 1.模拟获取用户id
Long userId = 1L;
// 2.获取日期
LocalDateTime now = LocalDateTime.now();
// 3.拼接key字符串
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = "sign:" + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.统计签到次数
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
);
// 5.1 没有签到结果
if(result == null || result.isEmpty()){
return 0;
}
Long num = result.get(0);
if(num == null || num == 0){
return 0;
}
// 5.2 统计签到次数
int count = 0;
while(true){
if((num & 1) == 0){
break;
}else{
count++;
}
num >>>= 1;
}
return count;
}
- 测试一下
可以看到24,25,26号完成了连续3天的的签到,刚好是3天。
到这里demo就结束了,源码地址:demo地址