东东
发布于 2024-12-20 / 1 阅读 / 0 评论 / 0 点赞

如何设计用户签到(活跃)记录功能

1、要存储用户的签到记录或者活跃记录,实际上就是每天该用户的状态是0还是1

2、页面要展示用户一年的活跃记录图,后端如何设计更加高效?

单纯基于数据库

设计一张签到表,每个用户每天一条数据。

这里我们要分析一点,这张表的数据属于是稳定增长的。

如果数据量比较小,那还可以接受。但是如果用户量比较大,假如有一万用户,那一个月,一年下来可能就有上百万条记录。

CREATE TABLE user_sign_in (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- 主键,自动递增
  userId BIGINT NOT NULL,               -- 用户ID,关联用户表
  signDate DATE NOT NULL,            -- 签到日期
  createdTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- 记录创建时间
  UNIQUE KEY uq_user_date (userId, signDate)  -- 用户ID和签到日期的唯一性约束
);

使用redis中的set集合

我们可以为每个用户定义一个set,用来存储用户活跃的日期,活跃一天添加一天

SADD user:signins:123 "2024-09-01"
SADD user:signins:123 "2024-09-02"

这个时候,我们发现,存储的数据中,年份的重复量非常大。我们可以考虑把年份提取到key中

即每个用户每年一个key,进一步减少存储量

SADD user:signins:2024:123 "09-01"
SADD user:signins:2024:123 "10-01"

位图bitmap

我们之前也说过,用户当天是否活跃,代表就是用户在那一天的状态是0还是1, 那么用户当天是否活跃其实只需要1位来存储就够了。

按照刚才redis中key的定义来看。每个用户每年的活跃记录,只需要366位数据就可以存储。

这时候就可以用到redis中的bitmap数据结构。

我们给每个用户每年都设置一个bitmap,大小就是366,然后计算日期是属于年中的第几天。然后作为bitmap中的坐标,给对应位置存储具体的状态。

例如:

-- 表示用户在第 240 天打卡
SETBIT user:signins:2024:123 240 1
-- 表示用户在第 241 天打卡
SETBIT user:signins:2024:123 241 1

实现

引入redisson

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.21.0</version>
</dependency>

编写redis配置

@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

    private String host;

    private Integer port;

    private Integer database;

    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
        .setAddress("redis://" + host + ":" + port)
        .setDatabase(database)
        .setPassword(password);
        return Redisson.create(config);
    }
}

编写签到接口

public interface RedisConstant {

    /**
     * 用户签到记录的 Redis Key 前缀
     */
    String USER_SIGN_IN_REDIS_KEY_PREFIX = "user:signins";

    /**
     * 获取用户签到记录的 Redis Key
     * @param year 年份
     * @param userId 用户 id
     * @return 拼接好的 Redis Key
     */
    static String getUserSignInRedisKey(int year, long userId) {
        return String.format("%s:%s:%s", USER_SIGN_IN_REDIS_KEY_PREFIX, year, userId);
    }

}

签到业务实现接口

/**
 * 添加用户签到记录
 *
 * @param userId 用户签到
 * @return 当前是否已签到成功
 */
public boolean addUserSignIn(long userId) {
    LocalDate date = LocalDate.now();
    String key = RedisConstant.getUserSignInRedisKey(date.getYear(), userId);
    RBitSet signInBitSet = redissonClient.getBitSet(key);
    // 获取当前日期是一年中的第几天,作为偏移量(从 1 开始计数)
    int offset = date.getDayOfYear();
    // 检查当天是否已经签到
    if (!signInBitSet.get(offset)) {
        // 如果当天还未签到,则设置
        return signInBitSet.set(offset, true);
    }
    // 当天已签到
    return true;
}