![]()
![]()
🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 , 苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
今天由于没有很多的时间,然后又看到群友发的奥特曼的额度,多的用不完,所以跑了好几个项目,玩了一玩,搓写了个个人日记,当然只是自己使用,很简单,然后还在claude code中接入了一个skill,感觉还是挺实用的,superpowers,感兴趣的可以去搜一下,可以提高分析项目或者处理项目的效率。
![]()
如图:
![]()
这是一份面向“新手入门 + 老手复盘”的项目分析文档。新手可以跟着它理解项目怎么拆、代码怎么读;有经验的同学可以用它复盘 Redis、缓存、秒杀、MQ、Feed 流、GEO、限流等面试高频点。
1. 项目速览
黑马点评是一个简化版的大众点评类项目,核心业务围绕“本地生活服务”展开:用户可以登录、浏览商铺、查看附近店铺、发布探店博客、点赞关注、查看关注博主动态,也可以参与优惠券秒杀。
| 项目项 | 内容 |
|---|
| 项目名称 | 黑马点评 / hm-dianping |
| 项目类型 | 简化版大众点评,本地生活点评类应用 |
| 后端框架 | Spring Boot 2.3.12.RELEASE |
| Java 版本 | Java 8 |
| ORM | MyBatis-Plus 3.4.3 |
| 数据库 | MySQL |
| 缓存/中间件 | Redis、RabbitMQ |
| 网关/前端代理 | Nginx |
| 核心亮点 | 登录鉴权、商铺缓存、GEO 附近商铺、博客点赞、关注 Feed、签到、优惠券秒杀、Lua 原子校验、MQ 异步下单 |
1.1 这个项目适合学什么
| 学习方向 | 项目中的落地点 | 适合人群 |
|---|
| Spring Boot Web 开发 | Controller、Service、Mapper 分层 | Java Web 新手 |
| MyBatis-Plus 实战 | 实体、Mapper、ServiceImpl、分页插件 | 想熟悉 ORM 的同学 |
| Redis 实战 | 缓存、登录态、点赞、Feed、签到、GEO、限流 | 准备 Redis 项目的同学 |
| 高并发秒杀 | Lua、Redis 库存、RabbitMQ 异步下单、令牌桶限流 | 面试复盘 / 项目亮点包装 |
| 社交业务建模 | 关注、共同关注、点赞排行榜、关注推送 | 想理解内容社区模型的同学 |
| 项目排错能力 | 配置、端口、缓存一致性、MQ 消费 | 有一定基础的后端开发 |
2. 技术栈职责表
这个项目不是简单地把技术堆在一起,而是把 Redis 的多种数据结构放进了真实业务场景中,这是它最值得复盘的地方。
| 技术 | 在项目中的作用 | 适合复盘的问题 |
|---|
| Spring Boot | Web 容器、接口开发、依赖注入 | 一个后端项目如何分层? |
| MyBatis-Plus | 数据库 CRUD、分页、Service 基类 | 如何快速完成表操作? |
| MySQL | 保存用户、商铺、博客、关注、优惠券、订单等核心数据 | 哪些数据必须落库? |
| Redis String | 验证码、商铺缓存、秒杀库存、缓存空值 | 如何解决缓存穿透? |
| Redis Hash | 登录 token 对应的用户信息 | token 登录态如何保存? |
| Redis List | 商铺类型缓存 | 有序列表如何缓存? |
| Redis Set | 关注关系、一人一单记录 | 如何做去重和交集? |
| Redis ZSET | 点赞排行榜、Feed 收件箱、验证码限流 | Feed 流和排行榜如何实现? |
| Redis Bitmap | 用户签到 | 连续签到如何高效统计? |
| Redis GEO | 附近商铺查询 | 附近的人/店怎么查? |
| Lua | 秒杀库存和一人一单原子校验 | 如何防止超卖? |
| RabbitMQ | 秒杀订单异步落库 | 为什么要削峰填谷? |
| Redisson | 分布式锁配置能力 | 分布式锁如何接入? |
| Guava RateLimiter | 秒杀入口限流 | 如何保护系统入口? |
| Nginx | 静态资源服务、接口代理 | 前后端如何联调? |
3. 项目目录与阅读顺序
项目是单模块 Maven Spring Boot 应用,主启动类位于src/main/java/com/hmdp/HmDianPingApplication.java,通过@MapperScan("com.hmdp.mapper")扫描 MyBatis Mapper。
| 目录/文件 | 职责 | 新手阅读建议 |
|---|
pom.xml | Maven 依赖和 Spring Boot 版本 | 先看项目用了哪些技术 |
src/main/java/com/hmdp/HmDianPingApplication.java | Spring Boot 启动类 | 确认启动入口和 Mapper 扫描 |
src/main/java/com/hmdp/controller | HTTP 接口入口 | 先看接口,知道每个业务从哪里进来 |
src/main/java/com/hmdp/service | 业务接口 | 快速了解有哪些业务能力 |
src/main/java/com/hmdp/service/impl | 核心业务实现 | 重点阅读,Redis、MQ、缓存、秒杀都在这里 |
src/main/java/com/hmdp/mapper | MyBatis-Plus 数据访问层 | 对照数据库表理解 CRUD |
src/main/java/com/hmdp/entity | 数据库实体 | 对照hmdp.sql理解表结构 |
src/main/java/com/hmdp/dto | 接口传输对象 | 理解前后端交互数据结构 |
src/main/java/com/hmdp/utils | Redis key、拦截器、ID 生成器、缓存工具等 | 复盘通用技术点 |
src/main/java/com/hmdp/config | Spring MVC、MyBatis、Redisson、RabbitMQ 配置 | 跑项目和排错时重点看 |
src/main/resources/application.yaml | 后端运行配置 | 检查 MySQL、Redis、RabbitMQ 连接 |
src/main/resources/db/hmdp.sql | 数据库建表和初始化数据 | 跑项目前先导入 |
src/main/resources/seckill.lua | 秒杀 Lua 脚本 | 理解 Redis 原子扣库存 |
nginx-1.18.0 | 本地 Nginx 代理与静态资源目录 | 前端联调时查看 |
3.1 推荐阅读顺序
| 顺序 | 阅读对象 | 目标 |
|---|
| 1 | README.md、pom.xml、application.yaml | 知道项目依赖和运行环境 |
| 2 | Controller 包 | 知道项目有哪些接口 |
| 3 | Entity + SQL | 知道业务表怎么设计 |
| 4 | ServiceImpl 包 | 理解每个业务怎么实现 |
| 5 | Utils 包 | 复盘 Redis key、拦截器、ID 生成器 |
| 6 | seckill.lua+ RabbitMQ 相关类 | 深挖秒杀链路 |
4. 运行环境与基础配置
| 配置项 | 当前项目表现 | 说明 |
|---|
| 后端端口 | 8081 | application.yaml中server.port=8081 |
| MySQL | 127.0.0.1:3306/hmdp | 需要先导入src/main/resources/db/hmdp.sql |
| Redis | localhost:6379,database6 | Redis 版本建议不低于 6.2,因为 GEOSEARCH 需要 Redis 6.2+ |
| RabbitMQ | host 为192.168.88.128,port 为15672 | 这里需要注意,15672通常是 RabbitMQ 管理后台端口 |
| Nginx | 监听8080,代理/api/**到后端8081 | 用于本地前后端联调 |
如果你只是学习源码,可以先读代码;如果要完整跑通,需要检查 MySQL、Redis、RabbitMQ、Nginx 的本地环境是否和配置一致。
5. 核心业务模块拆解
5.1 模块:用户登录与鉴权
解决的问题
- 用户如何获取验证码。
- 用户如何登录。
- 后端如何识别当前请求是谁。
- 登录态如何续期。
- 用户签到如何记录和统计。
核心文件
| 文件 | 作用 |
|---|
UserController.java | 暴露验证码、登录、当前用户、用户信息、签到等接口 |
UserServiceImpl.java | 实现验证码发送、登录、签到统计等逻辑 |
RefreshTokenInterceptor.java | 根据 token 从 Redis 读取用户并刷新 TTL |
LoginInterceptor.java | 判断当前请求是否已登录 |
UserHolder.java | 使用 ThreadLocal 保存当前用户 |
RedisConstants.java | 维护登录相关 Redis key 前缀和 TTL |
核心流程
- 用户请求验证码。
- 服务端生成验证码。
- 当前项目通过邮箱发送验证码。
- 验证码写入 Redis:
login:code:{email}。 - 用户提交验证码登录。
- 服务端校验 Redis 中的验证码。
- 登录成功后生成 token。
- 用户信息写入 Redis Hash:
login:token:{token}。 - 前端后续请求携带 token。
RefreshTokenInterceptor从 Redis 读取用户信息并刷新 TTL。LoginInterceptor判断当前用户是否存在,不存在则拦截。
技术复盘
| 问题 | 项目方案 | 复盘关键词 |
|---|
| 为什么不用 Session? | 使用 token + Redis,更适合前后端分离和分布式部署 | 无状态、共享登录态 |
| token 存在哪里? | Redis Hash | StringRedisTemplate.opsForHash() |
| 如何续期? | 拦截器每次请求刷新 Redis TTL | 滑动过期 |
| 如何避免未登录访问? | 登录拦截器检查UserHolder | 拦截器链、ThreadLocal |
| 验证码如何限流? | 使用 Redis ZSET 记录发送时间窗口,并配合限制 key | 时间窗口、ZSET、TTL |
新手注意
当前代码里很多命名仍然偏向phone,但实际验证码发送使用的是邮箱能力。这一点适合在复盘中说明:项目从短信登录语义改成了邮箱验证码,但字段命名没有完全同步。
5.2 模块:商铺查询与缓存
解决的问题
- 用户如何查询商铺详情。
- 商铺数据如何缓存。
- 查询不存在的商铺时如何避免缓存穿透。
- 修改商铺后如何保持缓存和数据库一致。
核心文件
| 文件 | 作用 |
|---|
ShopController.java | 商铺查询、新增、更新、按类型查询、按名称查询接口 |
ShopServiceImpl.java | 商铺缓存、数据库查询、GEO 查询核心实现 |
CacheClient.java | 通用缓存工具,包含缓存穿透、互斥锁、逻辑过期等思路 |
RedisConstants.java | 商铺缓存 key 和 TTL 常量 |
核心流程:查询商铺详情
- 接收
/shop/{id}请求。 - 先查 Redis:
cache:shop:{id}。 - 如果 Redis 有正常数据,直接返回。
- 如果 Redis 命中空字符串,说明之前查过数据库且不存在,直接返回空结果。
- 如果 Redis 没有数据,查询 MySQL。
- MySQL 查不到时,向 Redis 写入空字符串,设置较短 TTL,防止缓存穿透。
- MySQL 查到时,将商铺 JSON 写入 Redis,设置正常 TTL。
- 返回商铺信息。
缓存策略复盘
| 场景 | 项目方案 | 说明 |
|---|
| 正常热点商铺 | Redis String 缓存 JSON | 降低数据库压力 |
| 查询不存在商铺 | 缓存空字符串 | 解决缓存穿透 |
| 商铺更新 | 先更新数据库,再删除缓存 | 常见 Cache Aside 策略 |
| 缓存重建 | 代码中保留互斥锁、逻辑过期思路 | 可作为扩展复盘点 |
面试表达
这个模块可以这样讲:
商铺详情是典型读多写少场景,所以使用 Redis 缓存。查询时先查缓存,缓存未命中再查数据库。为了防止恶意请求不存在 ID 导致缓存穿透,数据库查不到时会缓存空值,并设置较短过期时间。更新商铺时采用先更新数据库、再删除缓存的 Cache Aside 策略,保证最终一致性。
5.3 模块:附近商铺 GEO 查询
解决的问题
- 用户如何查询附近商铺。
- 如何按照距离排序。
- 如何把 Redis GEO 查询结果和数据库详情结合起来。
核心文件
| 文件 | 作用 |
|---|
ShopController.java | /shop/of/type接口入口 |
ShopServiceImpl.java | 根据类型和坐标查询附近商铺 |
HmDianPingApplicationTests.java | 测试/工具代码中包含商铺 GEO 数据预热逻辑 |
RedisConstants.java | shop:geo:key 前缀 |
核心流程
- 用户传入商铺类型、当前页、经纬度。
- 如果没有经纬度,走普通数据库分页查询。
- 如果有经纬度,使用 Redis GEO 查询
shop:geo:{typeId}。 - Redis 返回指定范围内的商铺 ID 和距离。
- 后端根据 ID 列表查询 MySQL 商铺详情。
- 使用
FIELD(id, ...)保持 Redis 返回的距离排序。 - 将距离字段补充到商铺对象中返回。
技术复盘
| 问题 | 项目方案 | 复盘关键词 |
|---|
| GEO 数据怎么存? | 按商铺类型分组存入shop:geo:{typeId} | 分组索引 |
| 如何查附近商铺? | Redis GEOSEARCH | 地理位置搜索 |
| 为什么还要查 MySQL? | Redis GEO 只适合存坐标和 member,不适合存完整商铺详情 | Redis + DB 组合查询 |
| 如何保持距离排序? | SQL 中使用FIELD(id, ...) | 顺序保持 |
5.4 模块:商铺类型缓存
解决的问题
- 首页商铺分类如何加载。
- 排序后的类型列表如何缓存。
- 简单列表数据如何用 Redis 缓存。
核心文件
| 文件 | 作用 |
|---|
ShopTypeController.java | /shop-type/list接口入口 |
ShopTypeServiceImpl.java | 查询商铺类型并缓存 |
RedisConstants.java | 商铺类型缓存 key |
核心流程
- 查询 Redis List:
cache:shoptype:。 - 如果 Redis 中有数据,将 JSON 反序列化为类型列表。
- 如果 Redis 没数据,查询数据库。
- 数据库按
sort字段排序。 - 将结果逐个写入 Redis List。
- 返回商铺类型列表。
技术复盘
| 问题 | 项目方案 | 适合复盘点 |
|---|
| 为什么用 List? | 商铺类型是有序列表 | 保序缓存 |
| 为什么不是 Hash? | 查询通常一次性返回全部类型 | 简化读取 |
| 如何避免每次查 DB? | Redis 缓存分类列表 | 热点基础数据缓存 |
5.5 模块:博客、点赞与 Feed 流
解决的问题
- 用户如何发布探店博客。
- 热门博客如何查询。
- 用户如何点赞/取消点赞。
- 点赞用户排行榜如何展示。
- 关注博主后如何收到新博客推送。
核心文件
| 文件 | 作用 |
|---|
BlogController.java | 博客发布、点赞、查询、Feed 接口入口 |
BlogServiceImpl.java | 博客发布、点赞、排行榜、Feed 流核心逻辑 |
FollowServiceImpl.java | 关注关系维护 |
ScrollResult.java | Feed 滚动分页返回对象 |
RedisConstants.java | 点赞和 Feed 相关 key |
点赞流程
- 用户点击点赞。
- 查询 Redis ZSET:
blog:liked:{blogId}。 - 如果当前用户不在 ZSET 中,说明未点赞。
- 数据库博客点赞数加一。
- 用户 ID 写入 ZSET,score 使用当前时间戳。
- 如果当前用户已在 ZSET 中,说明已点赞。
- 数据库博客点赞数减一。
- 从 ZSET 移除用户 ID。
Feed 推送流程
- 用户发布博客。
- 博客先保存到 MySQL。
- 查询当前作者的所有粉丝。
- 将新博客 ID 推送到每个粉丝的 Feed ZSET:
feed:{fanUserId}。 - 粉丝查看关注动态时,从自己的 Feed ZSET 按时间倒序滚动分页。
技术复盘
| 问题 | 项目方案 | 复盘关键词 |
|---|
| 点赞为什么用 ZSET? | 既能判断是否点赞,也能按时间排序 | 去重 + 排序 |
| 点赞排行榜怎么做? | ZSET score 存点赞时间戳 | TopN、时间顺序 |
| Feed 流采用什么模式? | 推模式,发布时写入粉丝收件箱 | 写扩散 |
| Feed 为什么用滚动分页? | 按时间戳滚动查询,避免传统分页重复/遗漏 | Scroll Pagination |
推模式和拉模式对比
| 模式 | 做法 | 优点 | 缺点 | 适用场景 |
|---|
| 推模式 | 发布时推给粉丝收件箱 | 读很快 | 大 V 写入压力大 | 普通用户社交动态 |
| 拉模式 | 读取时查询关注列表再聚合 | 写很轻 | 读很重 | 关注量少或实时性要求低 |
| 推拉结合 | 普通用户推,大 V 拉 | 平衡读写 | 实现复杂 | 大型内容社区 |
当前项目使用的是推模式,逻辑更直观,也适合教学项目。
5.6 模块:关注与共同关注
解决的问题
- 用户如何关注/取关其他用户。
- 如何判断是否已关注。
- 如何查询共同关注。
核心文件
| 文件 | 作用 |
|---|
FollowController.java | 关注、取关、共同关注接口 |
FollowServiceImpl.java | 关注关系数据库写入和 Redis Set 维护 |
Follow.java | 关注关系实体 |
核心流程:关注用户
- 用户点击关注。
- 后端向
tb_follow写入关注关系。 - 同时将被关注用户 ID 写入 Redis Set:
follows:{userId}。 - 判断共同关注时,对两个用户的 Set 做交集。
- 根据交集中的用户 ID 查询用户信息。
技术复盘
| 问题 | 项目方案 | 复盘关键词 |
|---|
| 关注关系为什么要落库? | 关系数据需要持久化 | MySQL 主存储 |
| 为什么还要写 Redis Set? | 共同关注需要高效交集计算 | Set Intersection |
| 取关怎么处理? | 删除数据库记录,同时从 Redis Set 移除 | 双写一致性 |
5.7 模块:优惠券与秒杀下单
解决的问题
- 商家如何发布优惠券。
- 秒杀库存如何控制。
- 高并发下如何防止超卖。
- 如何保证一人只能下一单。
- 如何降低数据库写入压力。
核心文件
| 文件 | 作用 |
|---|
VoucherController.java | 普通券、秒杀券发布和查询接口 |
VoucherServiceImpl.java | 新增秒杀券并初始化 Redis 库存 |
VoucherOrderController.java | 秒杀下单接口入口 |
VoucherOrderServiceImpl.java | 秒杀入口限流、Lua 调用、订单消息发送 |
seckill.lua | Redis 原子判断库存、一人一单、扣库存 |
RabbitMQTopicConfig.java | 秒杀队列、交换机、绑定关系配置 |
MQSender.java | 发送秒杀订单消息 |
MQReceiver.java | 消费秒杀订单消息并写入数据库 |
RedisIdWorker.java | 生成全局唯一订单 ID |
秒杀链路总表
| 阶段 | 做了什么 | 使用技术 |
|---|
| 请求入口 | 接收秒杀请求 | Controller |
| 入口限流 | 控制瞬时请求量 | Guava RateLimiter |
| 原子校验 | 判断库存、一人一单、扣 Redis 库存 | Lua |
| 记录用户 | 将用户加入已下单集合 | Redis Set |
| 生成订单 ID | 生成全局唯一 ID | Redis 自增 + 时间戳 |
| 异步下单 | 发送订单消息 | RabbitMQ |
| 数据落库 | 扣 DB 库存、保存订单 | MySQL + 事务 |
Lua 脚本做了什么
seckill.lua的核心职责是把多个 Redis 操作合并成一个原子操作。
| 判断 | Redis 数据 | 失败返回 | 目的 |
|---|
| 库存是否充足 | seckill:stock:{voucherId} | 1 | 防止库存不足还继续下单 |
| 用户是否下过单 | seckill:order:{voucherId} | 2 | 防止一人多单 |
| 扣减库存 | incrby stockKey -1 | 0 | Redis 侧预扣库存 |
| 记录用户 | sadd orderKey userId | 0 | 标记用户已参与 |
为什么要用 Lua
| 不用 Lua 的问题 | Lua 的价值 |
|---|
| 判断库存、判断是否下单、扣库存、记录用户是多个命令 | Lua 脚本在 Redis 中原子执行 |
| 多线程并发下可能出现竞态 | Redis 单线程执行脚本,天然串行 |
| Java 端加锁性能较差,分布式部署复杂 | 把并发控制前移到 Redis |
为什么要用 RabbitMQ
| 没有 MQ | 使用 MQ 后 |
|---|
| 秒杀请求直接打到数据库 | 请求先在 Redis 完成资格判断 |
| 数据库承受瞬时高并发写入 | 订单写入异步消费,削峰填谷 |
| 用户等待完整下单事务结束 | 用户更快拿到“排队成功/下单中”结果 |
| 系统峰值能力受 DB 限制明显 | Redis + MQ 承担入口压力 |
面试表达
这个模块可以这样讲:
秒杀接口入口先通过令牌桶做限流,避免瞬时流量直接打满系统。通过 Lua 脚本在 Redis 中原子完成库存判断、一人一单判断、库存扣减和用户记录,保证不会在 Redis 层超卖。校验成功后生成订单 ID,并将订单消息发送到 RabbitMQ,由消费者异步扣减数据库库存并保存订单,从而实现削峰填谷,保护数据库。
5.8 模块:签到统计
解决的问题
- 如何记录用户每天是否签到。
- 如何统计用户连续签到天数。
- 如何避免一人一天一条签到记录导致数据量过大。
核心文件
| 文件 | 作用 |
|---|
UserController.java | 签到和连续签到统计接口 |
UserServiceImpl.java | Bitmap 签到逻辑 |
RedisConstants.java | 签到 key 前缀 |
核心流程
- 用户请求签到。
- 根据用户 ID 和当前年月构造 Redis key:
sign:{userId}:{yyyyMM}。 - 以当月第几天作为 bit 位偏移。
- 使用 Bitmap 将当天标记为已签到。
- 统计连续签到时,读取从月初到今天的 bitmap。
- 从低位开始连续判断 1 的个数。
技术复盘
| 问题 | 项目方案 | 复盘关键词 |
|---|
| 为什么用 Bitmap? | 一个月最多 31 位即可表示每天是否签到 | 空间极小 |
| 如何按月隔离? | key 中拼接年月 | yyyyMM |
| 如何统计连续签到? | 位运算判断连续的 1 | Bit Operation |
5.9 模块:文件上传与 Nginx 代理
解决的问题
- 博客图片如何上传。
- 前端静态页面如何通过 Nginx 访问后端接口。
核心文件
| 文件 | 作用 |
|---|
UploadController.java | 博客图片上传、删除接口 |
SystemConstants.java | 上传路径等系统常量 |
nginx-1.18.0/conf/nginx.conf | Nginx 静态资源和接口代理配置 |
Nginx 联调思路
| 请求 | 处理方式 |
|---|
| 静态页面请求 | Nginx 从本地 html 目录读取 |
/api/**请求 | Nginx 代理到后端127.0.0.1:8081 |
| 后端接口 | Spring Boot 应用处理 |
新手注意
当前仓库中包含 Nginx 目录和配置,但完整前端页面目录可能并不完整。如果你要跑前端,需要确认nginx-1.18.0/html/hmdp这类静态资源目录是否存在。
6. Redis 在项目中的使用总表
这个项目最适合用来复盘 Redis,因为它几乎覆盖了常见数据结构在业务中的落地方式。
| Redis 数据结构 | 项目场景 | Key 示例 | 价值 |
|---|
| String | 验证码、商铺缓存、秒杀库存、缓存空值 | login:code:{email}、cache:shop:{id}、seckill:stock:{voucherId} | 简单 KV、TTL、缓存穿透处理 |
| Hash | token 登录态 | login:token:{token} | 保存用户字段,便于续期和读取 |
| List | 商铺类型缓存 | cache:shoptype: | 保存有序分类列表 |
| Set | 关注关系、一人一单记录 | follows:{userId}、seckill:order:{voucherId} | 去重、交集计算 |
| ZSET | 点赞排行榜、Feed、验证码限流 | blog:liked:{blogId}、feed:{userId}、sms:sendtime:{email} | 排序、滚动分页、时间窗口 |
| Bitmap | 用户签到 | sign:{userId}:{yyyyMM} | 高效记录连续签到 |
| GEO | 附近商铺 | shop:geo:{typeId} | 地理位置搜索 |
6.1 Redis key 设计复盘
| Key 前缀 | 业务含义 | 数据结构 |
|---|
login:code: | 登录验证码 | String |
login:token: | 登录用户信息 | Hash |
cache:shop: | 商铺详情缓存 | String |
cache:shoptype: | 商铺类型列表 | List |
lock:shop: | 商铺缓存重建锁 | String |
seckill:stock: | 秒杀券 Redis 库存 | String |
seckill:order: | 秒杀券已下单用户集合 | Set |
blog:liked: | 博客点赞用户排行 | ZSET |
feed: | 用户关注 Feed 收件箱 | ZSET |
shop:geo: | 商铺地理位置索引 | GEO |
sign: | 用户签到记录 | Bitmap |
sms:sendtime: | 验证码发送时间窗口 | ZSET |
limit:onelevel:/limit:twolevel: | 验证码登录限制 | String / Set 语义限制 key |
7. 重点技术方案深挖
7.1 缓存穿透
| 问题 | 说明 |
|---|
| 什么是缓存穿透? | 请求的数据在 Redis 和数据库都不存在,导致每次请求都会打到数据库 |
| 项目怎么做? | 数据库查不到商铺时,向 Redis 写入空字符串 |
| 为什么设置短 TTL? | 防止空值长期占用,同时允许后续真实数据创建后恢复 |
| 还能怎么优化? | 增加布隆过滤器、参数校验、热点保护 |
7.2 缓存更新策略
| 策略 | 项目使用情况 | 说明 |
|---|
| 先更新数据库,再删除缓存 | 商铺更新使用该思路 | 常见 Cache Aside 模式 |
| 互斥锁重建缓存 | 代码中有相关实现思路 | 适合热点 key 失效重建 |
| 逻辑过期 | 代码中有相关实现思路 | 适合高可用场景,牺牲短暂一致性换可用性 |
7.3 Redis ID 生成器
| 组成 | 作用 |
|---|
| 时间戳部分 | 保证 ID 趋势递增 |
| Redis 自增序列 | 保证同一时间内不重复 |
| 业务前缀 | 区分不同业务 ID |
这种方式适合分布式场景下生成全局唯一 ID,比单机自增更适合多实例部署。
7.4 限流设计
| 限流位置 | 项目方案 | 目的 |
|---|
| 验证码发送 | Redis ZSET 时间窗口 + 限制 key | 防止频繁发送验证码 |
| 秒杀入口 | Guava RateLimiter | 防止高并发瞬时打爆服务 |
限流的核心不是让所有请求都成功,而是让系统在高压下仍然可控。
8. 项目坑点与优化建议
这部分很适合老手复盘,也适合面试时主动补充“我不仅会用,还知道它的问题在哪里”。
| 问题 | 当前表现 | 影响 | 优化建议 |
|---|
| RabbitMQ 端口疑似配置错误 | application.yaml中配置为15672 | 15672通常是管理后台端口,AMQP 常用5672 | 本地启动时确认 RabbitMQ AMQP 端口 |
rebbitmq包名拼写异常 | RabbitMQ 包路径命名为rebbitmq | 不影响运行,但影响可读性 | 统一重命名为rabbitmq |
| 登录字段命名不一致 | 接口/字段偏phone,实际使用邮箱验证码 | 新手阅读时容易误解 | 统一改为 email,或恢复短信登录语义 |
| 登出接口未实现 | /user/logout返回失败/占位 | 登录态无法主动删除 | 删除 Redis token 并返回成功 |
| 秒杀时间未校验 | Lua/Java 秒杀路径未判断开始、结束时间 | 非活动时间也可能进入秒杀逻辑 | 在入口查询活动时间,或在 Redis 中预热时间并校验 |
| 一人一单缺少唯一索引 | 依赖 Redis Set 和 DB 查询判断 | 极端情况下 DB 层没有最后防线 | 给tb_voucher_order(user_id, voucher_id)加唯一索引 |
| Redis 与 DB 一致性边界 | Redis 预扣成功后,MQ/DB 失败可能不一致 | 库存和订单状态可能偏差 | 加消息确认、失败补偿、对账任务或库存回滚 |
| GEO 数据需要预热 | GEO 数据主要依赖测试/工具逻辑加载 | 新环境可能查不到附近商铺 | 增加启动预热任务或后台管理同步能力 |
| Nginx 前端资源可能不完整 | 仓库有 Nginx,但完整html/hmdp目录可能缺失 | 前端页面可能无法直接访问 | 补齐前端资源或在 README 中说明来源 |
| 上传路径硬编码 | 图片上传目录是本机绝对路径 | 换机器后容易失败 | 改为配置项,并区分本地/生产环境 |
| 配置存在明文账号密码 | application.yaml写有本地账号密码 | 公开部署存在安全风险 | 用环境变量、私密配置或配置中心 |
9. 新手学习路线
| 阶段 | 学习目标 | 建议阅读内容 |
|---|
| 第 1 阶段 | 跑通项目 | README.md、application.yaml、hmdp.sql |
| 第 2 阶段 | 看懂接口入口 | 所有 Controller |
| 第 3 阶段 | 理解基础 CRUD | 商铺、用户、博客基础查询 |
| 第 4 阶段 | 学 Redis 缓存 | ShopServiceImpl、ShopTypeServiceImpl、CacheClient |
| 第 5 阶段 | 学登录鉴权 | UserServiceImpl、两个 Interceptor、UserHolder |
| 第 6 阶段 | 学社交业务 | 关注、点赞、Feed 流 |
| 第 7 阶段 | 学高并发秒杀 | VoucherOrderServiceImpl、seckill.lua、RabbitMQ 发送/消费 |
| 第 8 阶段 | 做项目复盘 | 总结缓存、限流、Lua、MQ、一致性问题 |
9.1 新手不要一上来就钻秒杀
推荐先按这个顺序学:
- 先看普通 CRUD,比如商铺查询、用户信息查询。
- 再看 Redis 缓存,比如商铺详情缓存。
- 再看登录鉴权,理解 token、Redis Hash、拦截器、ThreadLocal。
- 然后看点赞、关注、Feed,理解 Redis ZSET 和 Set。
- 最后看秒杀,因为秒杀同时涉及限流、Lua、Redis、MQ、事务和一致性。
10. 老手面试复盘表
| 高频问题 | 回答关键词 | 项目落地点 |
|---|
| 这个项目的核心亮点是什么? | Redis 多数据结构实战、高并发秒杀、Feed 流、GEO | 商铺、博客、秒杀、签到模块 |
| 如何解决缓存穿透? | 缓存空值、短 TTL、布隆过滤器可扩展 | 商铺详情缓存 |
| 缓存和数据库如何保持一致? | 先更新 DB,再删除缓存,最终一致性 | 商铺更新 |
| 如何防止秒杀超卖? | Lua 原子判断库存并扣减,DB CAS 扣库存 | seckill.lua、订单消费 |
| 如何防止一人多单? | Redis Set 记录下单用户,DB 查询校验,建议加唯一索引 | seckill:order:{voucherId} |
| 为什么使用 Lua? | 多个 Redis 命令合并为原子操作 | 秒杀资格校验 |
| 为什么使用 RabbitMQ? | 异步下单、削峰填谷、保护数据库 | 秒杀订单消息 |
| Redis 扣库存成功但 MQ 失败怎么办? | 需要消息确认、补偿、对账、回滚策略 | 当前项目可优化点 |
| Feed 流怎么实现? | 推模式,Redis ZSET 做用户收件箱 | feed:{userId} |
| 点赞排行榜怎么实现? | ZSET,score 存时间戳 | blog:liked:{blogId} |
| 附近商铺怎么实现? | Redis GEO + MySQL 详情查询 | shop:geo:{typeId} |
| 签到怎么实现? | Bitmap 按月记录 | sign:{userId}:{yyyyMM} |
| 验证码如何限流? | ZSET 时间窗口 + 限制 key | sms:sendtime:{email} |
11. 一句话总结每个模块
| 模块 | 一句话总结 |
|---|
| 用户登录 | token + Redis Hash + 拦截器实现分布式登录态 |
| 商铺缓存 | Redis 缓存热点数据,空值解决缓存穿透 |
| 附近商铺 | Redis GEO 负责距离搜索,MySQL 负责详情数据 |
| 商铺类型 | Redis List 缓存有序分类数据 |
| 博客点赞 | Redis ZSET 同时实现是否点赞和点赞排行榜 |
| 关注 Feed | 发布博客时推送到粉丝 ZSET 收件箱 |
| 共同关注 | Redis Set 交集快速计算共同关注用户 |
| 秒杀下单 | RateLimiter + Lua + Redis + RabbitMQ + DB 事务组成高并发链路 |
| 签到 | Bitmap 用极小空间记录月度签到 |
| Nginx 代理 | 前端静态资源和后端 API 通过 Nginx 联调 |
12. 项目价值与复盘建议
这个项目的最大价值,不在于业务复杂,而在于它把 Redis 的常见能力串进了多个真实业务场景中:
| Redis 能力 | 业务场景 |
|---|
| 缓存 + TTL | 商铺详情、验证码 |
| 空值缓存 | 缓存穿透 |
| Hash | 登录态 |
| Set | 关注、一人一单 |
| ZSET | 点赞排行、Feed、时间窗口限流 |
| Bitmap | 签到 |
| GEO | 附近商铺 |
| Lua | 秒杀原子校验 |
如果你是新手,建议把它当成 Redis 实战项目来学;如果你准备面试,建议重点讲清楚三条线:
- 缓存线:商铺缓存、缓存穿透、缓存更新。
- 社交线:点赞、关注、Feed 流。
- 高并发线:限流、Lua、Redis 预扣库存、RabbitMQ 异步下单、DB 最终落库。
最后,复盘项目时不要只讲“我用了 Redis 和 MQ”,更好的表达是:
我在不同业务场景下选择了不同 Redis 数据结构:String 做缓存和库存,Hash 做登录态,Set 做关注和一人一单,ZSET 做点赞排行和 Feed,Bitmap 做签到,GEO 做附近商铺。秒杀场景下,为了避免超卖和一人多单,使用 Lua 保证 Redis 校验和扣减的原子性,再通过 RabbitMQ 异步落库,达到削峰和保护数据库的目的。同时我也意识到项目还有一致性补偿、唯一索引、秒杀时间校验、配置安全等可以继续优化的点。