深入业务场景的企业级实战项目,基于 Next.js 服务端渲染 + Spring Boot + Redis + MySQL + Elasticsearch 的 面试刷题平台。
管理员可以创建题库、题目和题解,并批量关联题目到题库;用户可以注册登录、分词检索题目、在线刷题并查看刷题记录日历等。
项目涉及大量企业级新技术的讲解,比如使用数据库连接池、热 Key 探测、缓存、高级数据结构来提升性能。通过流量控制、熔断、动态 IP 黑白名单过滤、同端登录冲突检测、分级反爬虫策略来提升系统和内容的安全性。
1)第一阶段,开发刷题平台的基础,熟悉项目开发流程,实战 Spring Boot 应用的快速开发。
2)第二阶段,对项目功能进行扩展,实战企业主流后端技术如 Redis 缓存和高级数据结构、Elasticsearch 搜索引擎、Druid 连接池、并发编程、热 key 探测的应用。
3)第三阶段,对项目安全性进行优化,比如基于 Sentinel 进行网站流量控制和熔断、基于 Nacos 实现动态的 IP 黑白名单、基于 Sa-Token 实现同端登录冲突检测、基于 Redis 实现分级反爬虫策略等。最终将项目上线。
- 刷题记录:基于 Redis BitMap + Redisson 实现用户年度刷题记录的统计,相比数据库存储节约几百倍空间。并通过本地缓存 + 返回值优化 + 位运算进一步提升接口性能。
- 分词搜索:自主搭建 ES 代替 MySQL 模糊查询,并为索引绑定 ik 分词器实现了更灵活的分词搜索。
- 使用 Spring Scheduler 定时同步近期发生更新的 MySQL 题目到 ES ,并通过唯一id保证每条数据同步的准确性。
- 基于 MyBatis 的batch操作实现题目批量管理,并通过任务拆分 + CompletableFuture 并发编程提升批处理性能。
- 使用 Caffeine 本地缓存提升题库查询性能,并通过接入 HotKey 并配置热 Key 探测规则来自动缓存热门题目,防止瞬时流量击垮数据库。
- 为限制恶意用户访问,基于 WebFilter + BloomFilter 实现 IP 黑名单拦截,并通过 Nacos 配置中心动态更新黑名单,便于维护。
- 为保护系统,基于 Sentinel 的热点参数限流机制对单 IP 获取题目进行流控,并通过拉模式配置将规则持久化到本地文件。
- 为防止账号共享,通过 UserAgent 识别用户设备,并基于 Sa-Token 快速实现同端登录冲突检测。
- 设计分级反爬虫策略:基于Redis实现用户访问题目频率统计,并通过Lua脚本保证原子更新,超限时自动给管理员发送告警和封禁用户,有效防止内容盗取
数据库按如下设计:
CREATE TABLE user_sign_in (
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 主键,自动递增(8字节)
userId BIGINT NOT NULL, -- 用户ID,关联用户表(8字节)
signDate DATE NOT NULL, -- 签到日期(3字节)
createdTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间(4字节)
UNIQUE KEY uq_user_date (userId, signDate) -- 用户ID和签到日期的唯一性约束(还需要额外空间)
);大小计算:
- 主键索引内:各字段(8+8+3+4) = 23 字节
- 行头信息+NULL位图(10+ceil(4/8)) = 11 字节
- 隐藏列:事务id6字节+回滚指针7字节 = 13 字节
- 以上总共47B加上一些页的信息,这里就按55算
- 唯一索引内:两个字段 8 + 3 ,加上主键 8 ,行头 + 页目录按 6 大约25字节
- 总大小 55 + 25 = 80B
- 80 * 100,0000 * 365 / 1024 / 1024 / 1024 = 27.19GB
而用 Redis Bitmap 一个用户存储一年的数据 ( value 部分 ) 仅需 46 字节,因为 46 * 8 = 368 ,能覆盖一年365天的记录。我们的键设计是:user:singin:{year}:{userid} 。以user:signin:2025:1927112888406622209举例。在redis控制台输入
MEMORY USAGE user:signin:2025:1927112888406622209返回 136
那一百万用户 100,0000 * 136 / 1024 /1024 = 129.70 MB
相比数据库相差 27.19 * 1024 / 129.7 = 214.67 倍
-
本地缓存是使用 JDK 自带的 bitset 一次性从 redis 拿到整个 bitmap 从而避免365次对 redis 的网络请求
-
原先使用Map<LocalDate, Boolean> 后使用 List<Integer> 缩短返回数据大小(返回的是一年当中第几天签到)
-
原先使用 for 循环遍历整个 bitmap 数据,后改进使用 NextSetBit 直接寻找下一个为 1 的位(注意:BitSet 类中的toString方法也使用了这个办法)

