Redis 实现用户签到功能


签到功能对应的逻辑很常见,主要有以下几种场景

  • 签到 1 天送 10 积分,连续签到 2 天送 20 积分,3 天送 30 积分,4 天以上均送 50 积分等
  • 如果连续签到中断,则重置计数,每月初重置计数
  • 在日历控件上展示用户每月签到情况,可以切换年月显示

最简单的就是使用数据库保存,假设数据签到表设计如下

create table user_sign(
  id int primary key AUTO_INCREMENT,
  user_id int not null comment '用户id',
  sign_date datetime not null comment '签到日期',
  amount int not null comment '连续签到次数'
);

如果这样存数据,对于用户量大的应用,db可能扛不住,比如 1000W 用户,一天一条,那么一个月就是 3 亿数据,非常庞大。

使用bitmap

就是通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。

Redis从2.2.0版本开始新增了setbit,getbit,bitcount等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展。

内存开销小、效率高且操作简单,很适合用于签到这类场景。比如按月进行存储,一个月最多 31 天,那么我们将该月用户的签到缓存二进制就是 00000000000000000000000000000000,当某天签到将 0 改成 1 即可,而且 Redis 提供对 bitmap 的很多操作比如存储、获取、统计等指令,使用起来非常方便。

常用命令

命令 功能 参数 示例
setbit 指定偏移量 bit 位置设置值 key offset value【0=< offset< 2^32】 setbit user:sign:202105 0 1
getbit 查询指定偏移位置的 bit 值 key offset getbit user:sign:202105 0
bitcount 统计指定字节区间 bit 为 1 的数量 key [start end] bitcount user:sign:202105 0 31
bitfield 操作多字节位域 key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP/SAT/FAIL] user:sign:202105 get u31 0

设置签到

假设需要设置2021年5月份的签到数据

setbit user:sign:202105 0 1 # key: user:sign:202105, 0 代表1号,1 代表已签到
setbit user:sign:202105 1 1
setbit user:sign:202105 2 1
# 第三天断签了
setbit user:sign:202105 4 1
setbit user:sign:202105 5 1

Redis中数据为

11101100

统计这个月签到天数

bitcount user:sign:202105 0 31

范围指定从0-31,一个月最多就31天

连续签到次数

bitfield user:sign:202105 get u31 0 # 1979711488

user:sign:202105这个key 中获取31位,u代表无符号,1979711488代表获取到的十进制数字

位运算判断是否签到

假设获取到连续31天签到次数为 : 1979711488,如何判断某天是否已经签到

1979711488的二进制为

1110110000000000000000000000000
# 先右移一位
0111011000000000000000000000000
# 再左移一位, 和原数字一样
1110110000000000000000000000000

# 假设当天最后一天签到了,则最后一位会是1
1110110000000000000000000000001
# 先右移一位
0111011000000000000000000000000
# 再左移一位,和原数字不一样
1110110000000000000000000000000

所以,如果右移再左移等于之前的数字,则代表没有签到。如果右移再左移不等于之前的数字,则代表已经签到了。

按照上面逻辑,可以计算用户这个月连续签到的次数

Java代码实现计算用户这个月连续签到的次数

private int continuousSignIn(long userId, LocalDate date) {
    // 统计连续签到次数, 假设今天是31号
    int dayOfMonth = date.getDayOfMonth();
    ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    // bitfield user:sign:202105 get u31 0
    BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
        .valueAt(0);
    String signKey = "user:sign:202105";
    List<Long> bitFields = opsForValue.bitField(signKey, bitFieldSubCommands);
    if (CollectionUtils.isEmpty(bitFields)) {
        return 0;
    }
    AtomicInteger continuousSignInCount = new AtomicInteger();
    bitFields.stream()
        .findFirst()
        .ifPresent(v -> {
            // i: 操作多少次位移, 今天是多少号,则有多少次
            for (int i = dayOfMonth; i > 0; i--) {
                // 右移再左移等于自己, 代表移动的是0,表示未签到。也有可能今天是第一天签到,也要排除
                if ((v >> 1 << 1 == v) && (i != dayOfMonth)) {
                    // 低位是0且非当天签到
                    break;
                } else {
                    continuousSignInCount.getAndIncrement();
                }
                // 右移一位,开始前一天的判断
                v >>= 1;
            }
        });
    return continuousSignInCount.get();
}

Author: Re:0
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Re:0 !
 Previous
Kubernetes安装 Kubernetes安装
Kubernetes作为生产级别的容器编排系统。 是一个可移植的、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。Kubernetes 拥有一个庞大且快速增长的生态系统。Kubernetes 的服务、支持和工具广
2022-06-03
Next 
Redis GEO命令介绍 Redis GEO命令介绍
Redis 在 3.2版本之后,支持GEO类型的数据存储。可以计算两个经纬度之间的距离,也就是,可以利用GEO功能,实现类似滴滴打车附近的车辆,类似微信附近的人基于地理位置的功能。也可以计算两个城市之间的距离,两个位置之间的距离等。
2022-05-17
  TOC