springboot + redis实现签到与统计功能

在很多项目中都会有签到与统计功能,最容易想到的方案是创建一个签到表来记录每个用户的签到记录,比如设计一个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地址