设计签到 用redis 和 MySQL

发布于:2025-03-06 ⋅ 阅读:(15) ⋅ 点赞:(0)

方案 1:使用 Redis BitMap(推荐,效率高)

适用场景:高并发、数据量大、快速查询

设计思路

  • 使用 Redis BitMap 存储用户的签到记录,每天占用 1 bit。
  • 例如:sign:{userId}:{yearMonth},表示某个用户在某月的签到数据。
  • 通过 BITFIELD 或 BITCOUNT 计算连续签到天数

Redis存储

# 用户 ID 1001 在 2025 年 3 月 1 日签到
SETBIT sign:1001:202503 1 1  
# 用户 ID 1001 在 2025 年 3 月 2 日签到
SETBIT sign:1001:202503 2 1  

# 统计 2025 年 3 月的签到天数
BITCOUNT sign:1001:202503

Java 实现


public class SignService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String SIGN_KEY = "sign:%d:%s"; // sign:{userId}:{yearMonth}

    // 签到
    public void sign(Long userId, int day) {
        String key = String.format(SIGN_KEY, userId, getCurrentYearMonth());
        redisTemplate.opsForValue().setBit(key, day, true);
    }

    // 获取当月签到次数
    public long getSignCount(Long userId) {
        String key = String.format(SIGN_KEY, userId, getCurrentYearMonth());
        return redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
    }

    // 计算连续签到天数
    public int getContinuousSignDays(Long userId) {
        String key = String.format(SIGN_KEY, userId, getCurrentYearMonth());
        BitSet bitSet = BitSet.valueOf(redisTemplate.opsForValue().get(key).getBytes());
        int count = 0;
        for (int i = bitSet.length() - 1; i >= 0; i--) {
            if (!bitSet.get(i)) break;
            count++;
        }
        return count;
    }

    private String getCurrentYearMonth() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
    }
}

优缺点

● 空间占用小:每个用户每月仅需 4 字节(31 天)。✔️
● 查询快:Redis BITCOUNT 计算签到天数 O(1) 时间复杂度。✔️
● 适用于高并发:Redis 处理速度快。✔️

● 仅支持 按月存储,跨月签到数据需要额外处理。❌
● Redis 重启后数据可能丢失,需要持久化。❌

方案 2:使用 MySQL 记录签到天数

适用场景:用户量适中,需要持久化数据。

表设计

CREATE TABLE user_sign (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
sign_date DATE NOT NULL,
continuous_days INT DEFAULT 1,
UNIQUE (user_id, sign_date)
);

java 逻辑


 
public void sign(Long userId) {
    LocalDate today = LocalDate.now();
    LocalDate yesterday = today.minusDays(1);

    // 查询昨天是否签到
    Optional<UserSign> lastSign = signRepository.findByUserIdAndSignDate(userId, yesterday);
    int continuousDays = lastSign.map(s -> s.getContinuousDays() + 1).orElse(1);

    // 插入签到数据
    UserSign sign = new UserSign(userId, today, continuousDays);
    signRepository.save(sign);
}


优缺点

● 数据持久化,不会丢失。✔️
● 可以支持 跨月签到 计算。✔️

● 查询签到天数需要数据库查询,性能较低。❌
● 高并发场景 需要加缓存优化。❌

方案 3:使用 Redis + MySQL 结合

适用场景:既要高并发,又要持久化。
● Redis 作为缓存,存储当月签到数据,快速计算签到天数。
● MySQL 作为持久化,定期同步签到数据,防止数据丢失。

 
public void sign(Long userId) {
    // 先更新 Redis
    redisService.sign(userId);

    // 异步任务写入数据库
    executorService.submit(() -> signRepository.save(new UserSign(userId, LocalDate.now())));
}

● 结合了 Redis 高性能 和 MySQL 持久化 的优势。
● 常规查询用 Redis,数据持久化用 MySQL,适合生产环境。

奖励触发逻辑

无论哪种方案,奖励发放逻辑如下:

 
public void checkAndReward(Long userId) {
    int continuousDays = signService.getContinuousSignDays(userId);

    if (continuousDays == 7) {
        rewardService.giveReward(userId, "7天连续签到奖励");
    } else if (continuousDays == 30) {
        rewardService.giveReward(userId, "30天连续签到大奖");
    }
}

● 签到天数达到 7/30 天,自动发放奖励。
● 奖励可以存入 Kafka / MQ 进行异步发放。


● 高并发场景(如 1000w 用户):✅ 方案 1(Redis BitMap)。
● 需要持久化(但并发一般):✅ 方案 2(MySQL)。
● 既要高并发又要持久化:✅ 方案 3(Redis + MySQL 结合)。


网站公告

今日签到

点亮在社区的每一天
去签到