方案 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 结合)。