Spring Boot + Redis BitMap:高性能签到系统实战
下面我将介绍如何使用 Spring Boot 和 Redis BitMap 数据结构实现用户签到与统计功能,同时结合高并发、跨月统计、补签和排行榜等优化方案。

1. 项目概述
我们将实现以下功能:
- 用户每日签到
- 检查某日签到状态
- 统计本月签到次数
- 获取本月连续签到天数(支持跨月)
- 查看某月签到情况
- 支持补签功能
- 支持签到排行榜与奖励扩展
2. 技术栈
- Spring Boot 2.x
- Redis + Redisson 客户端
- Maven
3. 实现步骤
3.1 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.8</version>
</dependency>
</dependencies>
3.2 配置 Redis 连接
spring:
redis:
host: localhost
port: 6379
database: 0
3.3 核心工具类实现
import org.redisson.api.RBitSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Component
public class SignService {
@Autowired
private RedissonClient redissonClient;
private static final DateTimeFormatter KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");
/**
* 用户签到
*/
public boolean sign(Long userId, LocalDate date) {
String key = buildKey(userId, date);
int offset = date.getDayOfMonth() - 1;
RBitSet bitSet = redissonClient.getBitSet(key);
if (bitSet.get(offset)) {
return false; // 已签到
}
bitSet.set(offset);
return true;
}
/**
* 补签功能
*/
public boolean retroactiveSign(Long userId, LocalDate date) {
return sign(userId, date); // 与签到逻辑一致
}
/**
* 检查某日是否签到
*/
public boolean checkSign(Long userId, LocalDate date) {
String key = buildKey(userId, date);
int offset = date.getDayOfMonth() - 1;
RBitSet bitSet = redissonClient.getBitSet(key);
return bitSet.get(offset);
}
/**
* 获取当月签到次数
*/
public long getSignCount(Long userId, LocalDate date) {
String key = buildKey(userId, date);
RBitSet bitSet = redissonClient.getBitSet(key);
return bitSet.cardinality();
}
/**
* 获取连续签到天数(支持跨月)
*/
public int getContinuousSignCount(Long userId, LocalDate date) {
int count = 0;
LocalDate current = date;
while (true) {
String key = buildKey(userId, current);
RBitSet bitSet = redissonClient.getBitSet(key);
int day = current.getDayOfMonth() - 1;
boolean signed = bitSet.get(day);
if (!signed) break;
count++;
current = current.minusDays(1);
if (current.getDayOfMonth() == current.lengthOfMonth()) {
// 跨月继续统计
continue;
}
}
return count;
}
/**
* 获取当月签到详情
*/
public List<Boolean> getSignInfo(Long userId, LocalDate date) {
String key = buildKey(userId, date);
YearMonth yearMonth = YearMonth.from(date);
int daysInMonth = yearMonth.lengthOfMonth();
RBitSet bitSet = redissonClient.getBitSet(key);
return IntStream.range(0, daysInMonth)
.mapToObj(bitSet::get)
.collect(Collectors.toList());
}
/**
* 构建 Redis 键
*/
private String buildKey(Long userId, LocalDate date) {
return String.format("sign:%d:%s", userId, date.format(KEY_FORMATTER));
}
}
3.4 控制器实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/sign")
public class SignController {
@Autowired
private SignService signService;
@PostMapping("/{userId}")
public String sign(@PathVariable Long userId,
@RequestParam(required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
if (date == null) date = LocalDate.now();
boolean result = signService.sign(userId, date);
return result ? "签到成功" : "签到失败或已签到";
}
@PostMapping("/{userId}/retroactive")
public String retroactiveSign(@PathVariable Long userId,
@RequestParam
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
boolean result = signService.retroactiveSign(userId, date);
return result ? "补签成功" : "补签失败";
}
@GetMapping("/{userId}/check")
public boolean checkSign(@PathVariable Long userId,
@RequestParam
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
return signService.checkSign(userId, date);
}
@GetMapping("/{userId}/count")
public long getSignCount(@PathVariable Long userId,
@RequestParam
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
return signService.getSignCount(userId, date);
}
@GetMapping("/{userId}/continuous")
public int getContinuousSignCount(@PathVariable Long userId,
@RequestParam(required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
if (date == null) date = LocalDate.now();
return signService.getContinuousSignCount(userId, date);
}
@GetMapping("/{userId}/info")
public List<Boolean> getSignInfo(@PathVariable Long userId,
@RequestParam
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
return signService.getSignInfo(userId, date);
}
}
4. 功能测试
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.util.List;
@SpringBootTest
public class SignServiceTest {
@Autowired
private SignService signService;
@Test
public void testSignFeatures() {
Long userId = 1001L;
LocalDate today = LocalDate.now();
System.out.println("签到结果: " + signService.sign(userId, today));
System.out.println("检查签到: " + signService.checkSign(userId, today));
System.out.println("本月签到次数: " + signService.getSignCount(userId, today));
System.out.println("连续签到天数: " + signService.getContinuousSignCount(userId, today));
List<Boolean> info = signService.getSignInfo(userId, today);
System.out.println("本月签到详情: " + info);
}
}
5. 原理解析
5.1 BitMap 原理
- Redis BitMap 是基于 String 类型 的位操作,每个位可存储 0 或 1。
- 每个用户每月使用一个 BitMap 键:
- 键名格式:sign:{userId}:{yyyyMM}
- 位偏移:0 表明当月 1 日,30 表明当月 31 日
- 优势:
- 极省空间:31 位≈4 字节存储一个月数据
- 高效操作:签到、查询、统计均为 O(1)
- 灵活性强:支持连续签到计算、补签、位统计
5.2 注意事项
- 位偏移从 0 开始,对应日期需要减 1
- Redis BitMap 最大偏移量为 2^32-1,日期存储足够
- 跨月连续签到需统计前一个月最后一天的状态
6. 扩展功能
- 补签功能:允许用户补签之前漏签日期
- 签到日历:返回图形化签到状态给前端展示
- 签到奖励:根据连续签到天数发放奖励
- 签到排行榜:
- 使用 Redis Sorted Set 存储每月签到次数:
- ZINCRBY sign:ranking:{yyyyMM} 1 {userId}
- 可实时获取本月签到排行
- 高并发优化
- Redis 单线程原子操作已足够
- 复杂业务可配合 Redisson 分布式锁
- 数据维护
- 设置键过期,如每年清理老数据
- 定期统计 BitMap 内存占用,防止暴增
7. API 调用示例
|
功能 |
请求方式 |
示例 |
|
用户签到 |
POST |
/sign/1001?date=2023-05-15 |
|
补签 |
POST |
/sign/1001/retroactive?date=2023-05-10 |
|
检查签到 |
GET |
/sign/1001/check?date=2023-05-15 |
|
获取当月签到次数 |
GET |
/sign/1001/count?date=2023-05-15 |
|
获取连续签到天数 |
GET |
/sign/1001/continuous?date=2023-05-15 |
|
获取本月签到详情 |
GET |
/sign/1001/info?date=2023-05-15 |
8. 总结
通过 Spring Boot + Redis BitMap 的结合,我们实现了一个高效、节省空间、可扩展的签到系统:
- 高性能:位操作 O(1)
- 低成本:每用户每月仅需 4 字节存储
- 易扩展:支持补签、排行榜、奖励、日历
优化提议:
- 添加缓存预热、分布式锁、备份恢复
- 监控 BitMap 大小和内存占用
- 可结合 Sorted Set 或其他 Redis 数据结构扩展排行榜和奖励功能
该方案适合 大规模用户签到场景,性能与存储效率兼顾,方便在实际生产环境中落地。
© 版权声明
文章版权归作者所有,未经允许请勿转载。如内容涉嫌侵权,请在本页底部进入<联系我们>进行举报投诉!
THE END
















暂无评论内容