Redis实战天花板:黑马点评项目全链路技术拆解与架构思考
先说结论黑马点评这个项目非常适合作为 Redis 实战的“串讲教材”。如果你是 Java 后端学过 Redis 基础命令但不知道这些命令在真实业务里怎么拼装、怎么取舍那把这个项目从头到尾跟一遍Redis 的实际应用基本就通了。这篇文章不是照着课程笔记复述是我自己把整个项目拆开再合上之后的理解重点讲清楚每个模块“为什么这么做”以及“还能怎么做”。1. 黑马点评到底练了什么项目链路与 Redis 的存在感黑马点评从业务形态上讲是一个仿大众点评的“点评类 App”有商户、用户、优惠券、签到、关注、探店笔记这些模块。它之所以在 Java 学习圈里被称为“Redis 实战天花板”不是因为它业务复杂而是因为它几乎把所有 Redis 的高频应用场景都塞进了一套完整业务里。整个项目的主链路可以理解为用户先用手机号验证码完成登录登录后去首页刷商户列表点进商户详情看评价看到优惠券就去抢抢到后下单下单后可以去签到、看好友动态还可以按距离找附近的商户。这条链路覆盖了用户、商户、交易、社交、LBS 五个维度Redis 在每一环都有自己的角色。登录环节用 Redis 替代 Session 保存用户登录态解决多机 Session 共享问题商户查询环节做缓存并处理缓存穿透、缓存击穿、缓存雪崩三座大山优惠券秒杀环节用全局 ID 生成器 乐观锁 Lua 脚本保证库存扣减的原子性签到环节用 Bitmap 做用户签到记录省空间又高效好友关注与 Feed 流用 ZSet 做关注列表和粉丝列表用推模式做动态流靠滚动分页实现不重复读取附近商户用 GEO 数据结构实现按距离排序。这个技术清单放在简历上很好看但真正值钱的是每个场景背后的思考过程。比如秒杀场景为什么要从乐观锁演变成 Lua 脚本缓存穿透为什么要用空值缓存而不是布隆过滤器Feed 流为什么要用推模式而不是拉模式这些“为什么”才是黑马点评真正的教学内容。项目的前后端结构也不复杂后端是传统的 Spring Boot MyBatis-PlusRedis 用来做缓存和数据结构的承载前端是一个移动端风格的 H5 页面直接用浏览器打开就能跑通全部功能。对初学者来说这个项目最大的优势是“看得见”抢券成功了页面有反馈签到后日历上出现标记发布动态后好友 feed 流里能刷到。这些正反馈比纯 API 调试要直观得多。我在过代码的时候有一个明显的感受项目里的 Redis 工具类、缓存工具类写得并不花哨每一个都是标准写法但组合起来的业务逻辑非常连贯。这其实是企业开发最需要的能力——不是会写某个高级命令而是能把基础命令合理地编排进业务流程里。2. 登录模块Session 方案的痛点与 Redis 替代方案的取舍2.1 短 Token 长生命周期的设计细节黑马点评的登录验证码流程是这样的用户输入手机号点击获取验证码后端生成一个 6 位验证码存入 Rediskey 是login:code:手机号有效期 5 分钟然后把验证码通过日志打出来——毕竟这是个教学项目没有真的接短信服务商。用户输入验证码后后端对比 Redis 里的值一致就登录成功。登录成功后后端生成一个随机 Token用的是 UUID以 Token 为 key用户信息 Hash 为 value存入 Redis有效期设为 30 分钟。后续用户每次请求前端都会在请求头里携带这个 Token后端拦截器从 Redis 里查用户信息查到就放行。这里有一个值得反复琢磨的设计为什么不直接把用户 ID 当 key而是再造一个随机 Token原因很简单用户 ID 是有规律的递增数字如果直接把 ID 暴露在请求头里别人可以遍历 ID 伪装成任意用户。UUID 这种随机字符串作为 Token相当于一把“不知道就没法伪造”的钥匙接口的访问凭证就不能被猜测。另一个细节是用户信息的存储结构。项目里没有用 String 把用户对象序列化成一个 JSON 字符串存进去而是用 Hash 结构每个字段对应用户的一个属性比如 id、phone、nickName、icon。这么做的直接好处是如果只想修改用户的某一个字段比如昵称可以直接 HSET 指定字段不用先把整个 JSON 取出来反序列化再写回去另一个隐藏好处是查看 Redis 内存时能直观看到用户的字段结构排错的时候一眼就能看出是哪个字段出了问题。不过 Hash 结构也有代价——每次读取用户信息需要把整个 Hash 取出来转成对象如果用户对象字段特别多序列化和反序列化的开销会比 String 更大。黑马点评的用户字段不多所以这种取舍是划算的。如果以后业务上用户属性扩展到几十个字段那就要重新评估是继续用 Hash 还是改回 String JSON。2.2 拦截器为什么拆成两层登录态校验在黑马点评里是用 Spring MVC 拦截器实现的而不是用 Spring AOP 或过滤器。这里有一个很实用的工程经验拦截器拆分成了两层。第一层拦截器拦截所有请求核心任务只有一个——尝试从请求头里取 Token查 Redis。如果有用户信息就放到 ThreadLocal 里如果查不到也不拦截直接放行。第二层拦截器只拦截需要登录的接口通过配置指定路径它的任务是从 ThreadLocal 里取用户如果取不到说明用户没登录直接返回 401。为什么要拆成两层这是为了兼顾“需要登录的接口”和“不需要登录的接口”两类场景。比如首页商户列表、商户详情这些接口游客也能看不能强制登录但下单、签到、发布笔记这些操作必须登录。如果只有一个拦截器要么所有接口都强制登录要么所有接口都放行然后每个接口自己判断登录态——前者不现实后者太啰嗦。拆成两层后刷新登录态的职责和校验登录态的职责解耦了加新接口时只需要配置第二层拦截器的路径规则。第一层拦截器还有“刷新 Token 有效期”的作用。实现方式是只要从 Redis 查到了用户信息就用EXPIRE把 Token 的过期时间重新设置为 30 分钟。这样用户只要在 30 分钟内有操作登录态就能一直续期超过 30 分钟没操作Token 自然会过期。这个设计解决了 Session 固定有效期的老毛病——用 Session 时Session 的过期时间从创建时算起用户用着用着突然就被踢下线体验很差。Redis 方案下活跃用户的登录态可以被持续刷新逻辑上更接近“活跃即有效”。这里有一个我在自己项目里踩过的坑如果刷新过期时间的逻辑放在“取用户信息”之后但取用户信息没有判断是否为“真实登录用户”那么无效 Token 也会走到刷新逻辑白白浪费一次 Redis 调用。黑马点评第一层拦截器里对“查到用户”和“查不到用户”是分别处理的查不到就直接放行不做任何写操作这个细节很重要。3. 商户查询缓存穿透、击穿、雪崩不是选择题而是组合题商铺查询是黑马点评里最像真实生产环境的部分。它的基本逻辑是根据商铺 ID 查询商铺详情先查 Redis没有再查数据库然后回填缓存。这个最简单流程通常被称为“缓存加旁路”写起来几十行代码就够但一旦把并发和异常场景考虑进去问题就成串出现了。3.1 缓存穿透的两种打法缓存穿透指的是查询一个根本不存在的数据。比如有人恶意用一个不存在的店铺 ID比如 -1、99999999反复请求每次请求都会穿透 Redis 打到数据库数据库压力瞬间就会被打满。黑马点评的解决方式是缓存空值如果数据库查不到商铺就往 Redis 里写一个空值比如 JSON 字符串并设置一个较短的过期时间比如 2 到 5 分钟。缓存空值的优点是实现简单、几乎零额外成本穿透的数据在短时间内不会再打到数据库。缺点也很明显——如果恶意请求构造了大量不同的不存在 ID那 Redis 里会被塞满空值 key占用大量内存而且空值过期后一旦同一批 ID 再次请求还是会穿透。所以缓存空值更适合不存在 data 比较“固定”的场景。另一个业界常用的方案是布隆过滤器在请求进入缓存层之前先经过一层布隆过滤器判断这个 ID 是否可能存在。布隆过滤器说“不存在”就一定不存在直接拦截说“存在”则可能误判继续走缓存和数据库。它的优点是内存占用极小适合“ID 集合巨大且不存在请求大量”的场景缺点是实现复杂度高——需要提前把所有合法 ID 初始化到过滤器里还需要处理新增数据的同步问题。黑马点评选择了空值缓存这个选择很聪明因为它是一个教学项目代码量少、逻辑清晰就足够了。但如果你在公司里做高并发系统我会建议优先考虑布隆过滤器或者两者的结合布隆过滤器做第一层拦截空值缓存做第二层兜底。毕竟空值缓存的 key 是可预测的仍然存在被塞满的风险。3.2 缓存击穿互斥锁和逻辑过期怎么选缓存击穿指的是某个热点 key 在过期的瞬间大量请求同时打到数据库。跟穿透不同击穿的数据是真实存在的但因为缓存恰好失效了流量直接穿透了保护层。黑马点评在商铺详情查询里专门做了击穿处理用的是两种方案。第一种是互斥锁查询缓存发现没有值尝试获取锁拿到锁的线程去查数据库并回填缓存其他线程没拿到锁就短暂等待后重新查询缓存。代码上可以用 Redis 的SETNX命令实现一个简单的锁比如set lock:shop:1 threadId ex 10 nx拿到锁的线程执行数据库查询结束后删除锁。这种方式能保证同一时刻只有一个请求会打到数据库逻辑严谨但也带来了一个问题——如果拿锁的线程执行时间过长后面的线程会一直等待用户体验变差极端情况下锁忘记释放还会造成死锁阻塞所有请求。第二种是逻辑过期不给缓存设置物理过期时间而是在缓存对象里手动加一个 expireTime 字段表示这个缓存的逻辑过期时间。读取时判断当前时间是否晚于 expireTime如果没有过期就直接返回如果过期了则尝试获取锁获取到锁的线程重新查询数据库、更新缓存和逻辑过期时间然后返回新数据没拿到锁的线程直接返回过期的旧数据。逻辑过期方案的最大优点是“不影响可用性”——即使缓存过期了用户依然能很快拿到一份旧数据新数据在后台异步刷新。这在秒杀、热点新闻这种“旧数据还能接受但不能等”的场景下非常合适。缺点是数据一致性弱极端场景下用户会读到比较陈旧的数据需要业务上有容忍度。我在实际项目中选型时的经验是如果业务对一致性要求高比如库存、交易优先互斥锁如果业务是读多写少且数据时效性要求不高比如商品详情、热门文章优先逻辑过期。黑马点评两个方案都实现了但默认演示的是互斥锁版本逻辑过期作为进阶方案在后面的秒杀场景有应用这种前后呼应是我很欣赏的编排。3.3 雪崩的处理往往在工程配置里缓存雪崩指大量 key 在同一时间过期导致大量请求同时打到数据库。黑马点评在商铺查询里对每个 key 设置过期时间时都加了一个随机值。比如基础过期时间是 30 分钟实际设置的过期时间是 30 分钟加一个 1 到 5 分钟的随机值。这样一来这些商铺缓存不会在同一天同一刻集体失效数据库的压力被摊平了。这个处理其实非常简单但非常容易被忽略。我自己刚接触缓存时也犯过这个错给一批商品设置缓存全部用统一的固定过期时间结果到了整点大量 key 同时过期数据库 QPS 瞬间飙高。后来排查了很久才意识到是“缓存集体失效”问题。黑马点评把这个逻辑写成了一个工具方法每次设置缓存时都自动加随机值我觉得这是一个非常值得带进工作习惯的细节。另外值得一提的是黑马点评会给缓存设置一个“逻辑过期时间”字段但物理过期时间往往设得很长甚至不设置这是因为逻辑过期机制已经能保证数据在新线程进来时被刷新物理过期反而会增加“某个 key 恰好物理过期 逻辑过期都失效”的概率。这个细节我在自己实现逻辑过期时才真正体会出来。4. 秒杀核心从超卖问题到原子性保障4.1 全局唯一 ID 为什么要自己造秒杀券的订单 ID 不能依赖数据库自增 ID原因在分布式环境下非常明确数据库自增 ID 是基于单表的在分库分表后会产生重复而且自增 ID 是连续的、可预测的容易被恶意猜测。黑马点评自己实现了一个全局 ID 生成器用 Redis 的INCR命令来实现。它的实现方式是这样的ID 是一个 long 类型64 位。最高位是符号位固定为 0。接下来 31 位是时间戳表示当前时间与某个起始时间比如 2024 年 1 月 1 日的时间差值单位为秒。再接下来 32 位是序列号通过 Redis 对某个业务 key比如icr:order做INCR每秒内自增得到的值。最终把这个时间戳和序列号拼接成一个 long就得到了一个全局唯一、趋势递增的 ID。这种 ID 的好处有三个趋势递增方便数据库索引全局唯一不依赖单库生成过程不依赖数据库性能非常高。对比 UUIDUUID 虽然也是全局唯一但它不是递增的作为数据库主键会导致 BTree 频繁页分裂性能很差。而一个纯自增的 long 在主键索引上写入效率最高。我当时对INCR有一个误解以为它是“线程不安全”的后来想通了Redis 是单线程执行命令INCR是原子操作多个客户端并发调用它能保证每个请求拿到的序列号不同。这也是 Redis 适合做 ID 生成器的核心原因。实际生产环境中ID 生成器还会考虑时钟回拨、跨机房等问题黑马点评这个方案已经能覆盖绝大多数中小业务场景。4.2 乐观锁优化与超卖问题的三种解法秒杀的核心是库存扣减。如果代码逻辑是“先查库存判断库存大于 0再执行扣减”在高并发下必然会超卖。因为多个线程可能同时查到了同一个库存值1都判断大于 0然后都执行扣减最终库存变成负数。黑马点评的第一版方案是乐观锁。乐观锁的思路是“CASCompare And Swap”更新库存时带上一个条件只更新“库存大于 0”的记录。SQL 大概是这样的UPDATE tb_seckill_voucher SET stock stock - 1 WHERE voucher_id ? AND stock 0如果这次更新影响的行数是 0说明库存已经没了或者被别人扣了就返回失败。这种方案解决了超卖但会带来另一个问题——一旦库存剩余很少大量并发请求同时执行 UPDATE只有极少数能成功其他都失败用户体验是“抢不到”。这里没有“重试”机制因为库存扣减是幂等敏感的你不可能因为没抢到就无限重试。后来演进的方向是“一人一单”限制同一个用户只能下一单秒杀券防止黄牛反复抢。实现思路是先判断用户是否已经购买过如果没有才允许执行扣减。但“先查后扣”在并发场景下并不安全两个请求可能同时查到“没有购买记录”然后都下单成功。这时候有两个进阶方案可选数据库层面给用户和优惠券组合加唯一索引重复插入会被数据库拒绝分布式锁层面在用户 ID 上加锁同一个用户串行执行。黑马点评使用的是分布式锁路径——它用 Redis 的SETNX做一个简单的锁key 是用户 ID拿到锁的线程才允许执行下单逻辑操作完成后释放锁。从课程演示角度讲这个方案直观地展示了SETNX的业务价值。但真实项目中这种简单锁会有很多坑锁没有设置过期时间程序崩溃会死锁锁误删问题A 线程的锁被 B 线程删除锁重入问题等。这些问题需要引入 Redisson 等成熟方案解决项目里做了简化。4.3 Lua 脚本为什么能解决终极一致性黑马点评在使用乐观锁和分布式锁之后到了最终优化版本把“判断库存是否充足、扣减库存、判断用户是否已购买、记录购买”这四个操作全部写成一个 Lua 脚本用 Redis 的EVAL命令原子性执行。这样一个脚本执行下来要么全部成功要么全部失败不存在中间状态。为什么需要 Lua因为 Java 代码里即使每一步都用了 Redis 命令但这些命令之间是分离的中间可能插入其他客户端的操作导致逻辑不严密。比如“判断库存”和“扣减库存”是两个命令A 客户端判断完库存充足后B 客户端可能已经把库存扣成 0 了A 客户端再执行扣减就会造成超卖。把这两个操作放进同一个 Lua 脚本Redis 会保证整个脚本在执行期间不会被其他命令打断这就是原子性的价值。Lua 脚本在项目里的逻辑大致是这样-- 判断库存是否充足 if tonumber(redis.call(get, KEYS[1])) 0 then return 0 end -- 判断用户是否已购买 if redis.call(sismember, KEYS[2], ARGV[1]) 1 then return 0 end -- 扣减库存 redis.call(incrby, KEYS[1], -1) -- 记录购买用户 redis.call(sadd, KEYS[2], ARGV[1]) return 1这里用到了两个 Redis 结构一个 String 保存库存一个 Set 记录已购买用户 ID。脚本内部用tonumber做类型转换用sismember判断是否已购买整体行数不到 15 行但解决了库存超卖和一人一单两个核心问题。这是我在整个项目里最喜欢的一段代码因为它完美展示了“把多步业务操作封装成原子操作”的工程思想。脚本模式的一个遗留问题是“下单”这个动作本身不等于“真正的订单落库”。黑马点评的最终实现是Lua 脚本只负责“预扣库存 记录用户”脚本执行成功后Java 层面再异步创建订单。这里的异步化很关键——秒杀瞬间高并发如果每个请求都同步写数据库数据库同样扛不住。异步化可以用线程池或者消息队列来做项目用 Stream 消息队列做的简化版本。这套组合拳下来秒杀模块的最终形态是Lua 脚本保证库存扣减原子性 异步下单削峰 全局唯一 ID 生成订单号。如果你以后在公司里做秒杀系统这个框架完全可以作为初始版本再根据团队情况替换成 Redisson RocketMQ 等更重型的组件。5. 签到、Feed 流、附近商户三个被低估的数据结构5.1 Bitmap 做签到一张位图存一年记录每月签到是黑马点评里比较轻量的功能但它用到的数据结构 Bitmap 很有意思。Bitmap 本质上是 String 类型Redis 把它当成一系列 bit 位来操作。一个字节有 8 个 bit一个月的签到记录可以用 31 个 bit 表示也就是不到 4 个字节。一年的签到记录也才 365 个 bit约 46 字节。如果用传统数据库表来存一个人一年就会产生 365 条记录100 万用户就是 3.65 亿条这对存储和查询都是巨大压力。签到功能的实现思路是key 设计为sign:用户ID:年月比如sign:1001:202401每天签到就用SETBIT把对应日期的 bit 位设置为 1。查询某月签到记录用BITFIELD一次性取出所有 bit然后按位解析就能知道这个月哪天签到了。这里最有价值的技巧是“统计连续签到天数”的处理。黑马点评的做法是取出本月签到 bitmap从当天开始往回遍历连续遇到 1 就累加遇到 0 就停止。Java 端可以用位运算逐位检查也可以用 Redis 的BITCOUNT配合范围查询。我自己的实用经验是如果数据量特别大可以把这个统计脚本也写成 Lua避免 Java 与 Redis 之间的多次网络交互。还有一个容易忽略的细节月份跨年时key 要从sign:1001:202412切换到sign:1001:202501如果业务上需要统计跨月连续签到比如期末结算奖励那就要额外处理跨月连接的问题。黑马点评没有做跨月连续统计但如果你要在真实系统里做全勤奖励这个坑一定要提前考虑。5.2 Feed 流推模式与滚动分页Feed 流信息流在社交类产品里几乎必然是核心功能。黑马点评做的是“关注的人发布探店笔记我能在我的信息流里刷到”。技术选型里有个关键分支推模式 vs 拉模式。拉模式是每次我刷新 feed 流时去关注列表里查询每个用户最近发布的笔记然后合并排序。优点是实现简单、实时性好缺点是每次请求都要查 N 个用户的笔记而且还要做去重和排序延迟随关注人数线性增长。推模式也叫写扩散是用户发笔记时系统就把这条笔记推送到他的每个粉丝的收件箱里收件箱的数据结构就是 ZSet。这样粉丝刷新时只需要从自己的 ZSet 里按时间戳读取一批数据。黑马点评采用的就是推模式发布笔记后遍历粉丝列表把笔记 ID 作为 ZSet 的 member时间戳作为 score写入每个粉丝的 feed key。推模式的代价是发布笔记时需要遍历粉丝并写多个 key如果粉丝量巨大比如百万级写放大的问题会非常严重。黑马点评作为教学项目没有做“大 V 特殊处理”但如果你要落地真实产品一般会做“虎鲸模式”普通用户用推模式大 V 用拉模式粉丝刷 feed 时把两种来源合并。这就是所谓“混合模式”。Feed 流另一个核心问题是分页。传统分页用LIMIT offset, size但 feed 流是持续的、动态变化的流如果用 offset 分页可能会出现“翻页时新数据插进来导致下一页数据重复或遗漏”。黑马点评用的是滚动分页记录上一次查到的最小 score下次查询时只查 score 小于这个值的笔记这样新发布的数据不会干扰翻页过程。ZSet 的ZREVRANGEBYSCORE配合 score 游标就能实现这个效果每次查询的条数固定游标越来越小直到没有数据为止。滚动分页是我在真实项目中最常推荐的 feed 流分页方式它不是最“先进”的方案但是最容易理解、实现成本最低的方案。5.3 GEO 做附近的人一条命令的魔力黑马点评的“附近商户”功能用到的是 Redis 3.2 之后引入的 GEO 类型。GEO 底层实现是 ZSetmember 存商户 IDscore 存的是经纬度经过编码后的值。添加商户时用GEOADD查询附近时用GEOSEARCH可以同时指定中心点和半径Redis 会返回距离范围内的所有成员并支持按距离排序。值得一提的是GEOSEARCH查询中还可以通过WITHDIST选项让 Redis 同时返回每个成员跟中心点的距离这样就不用在 Java 端再用公式计算距离了。这种将空间计算下沉到 Redis 的做法能显著减少应用层的计算压力。不过 GEO 方案也有一个边界它适合“以点为中心、半径查找”的场景但如果是更复杂的“多边形区域查询”或者需要实时更新的地理位置轨迹跟踪GEO 就不够用了通常要交给专门的 LBS 服务或空间数据库。黑马点评选 GEO是因为它正好匹配“给用户返回 5 公里内的商户”这个需求。我在实际项目里用到 GEO 时通常会搭配一个简单的缓存策略对查询结果再做一层短时间缓存比如 1 分钟因为“附近商户”这种数据的实时性要求并不高但查询频率可能很高减少对 Redis 的重复计算能节约不少性能。6. 项目复盘哪一段代码最值得反复琢磨6.1 缓存工具类的封装思路黑马点评的缓存代码有一个明显的特点工具类做得非常薄没有过度抽象。比如商铺查询它就写了一个queryById方法里面包含查缓存、判断存在、解决穿透、解决击穿的全流程。这个方法代码量不小但它把业务逻辑和缓存逻辑放在一起读起来反而比拆成七八个工具方法更直观。我见过很多工程里把缓存操作抽成了“通用缓存工具类”结果业务调用处只剩一行cacheUtil.get(key, type)排查问题时反而要跳好几层代码才能理解到底发生了什么。黑马点评的这种“模块内自闭环”的写法在中小项目里其实是更好的选择——代码可读性优先复用性其次。6.2 什么时候不该用 Redis复盘这个项目时我发现最值得思考的问题反而不是“Redis 能做什么”而是“Redis 不能做什么”。项目里有几处 Redis 的用法是不得不用的比如登录态、秒杀库存、签到位图、feed 流收件箱但也有一些场景其实用数据库更合理。以优惠券库存为例实际上 MySQL 的行锁配合UPDATE ... WHERE stock 0也能解决超卖问题。用 Redis 的最大优势是性能而不是“必须”。如果你的系统并发根本没到每秒几百 QPS完全可以直接用数据库事务。Redis 不是银弹引入它就意味着要处理缓存与数据库的一致性问题、缓存过期策略问题、缓存雪崩问题——这些复杂度都是实打实的。黑马点评作为一个教学项目肯定会把 Redis 的戏份拉满因为它的目的是让你练习 Redis。但在真实工作中我会劝你先画清楚系统的瓶颈到底在哪里再去决定要不要上 Redis。曾遇到过同事给用户列表加缓存结果每次用户信息更新后缓存和数据库不一致最后不得不花很大的力气去清理缓存这个教训告诉我——缓存永远是“为了让读更快而接受的写延迟”如果业务写的频率远远大于读的频率那缓存可能弊大于利。6.3 我认为最值得加强的方向黑马点评覆盖的技术点很广但毕竟是教学项目有些地方做了简化。如果你想把它的含金量再提一层建议从这几个方向去补分布式锁替换项目里手写的SETNX锁在真实场景下不够稳建议学会 Redisson并理解看门狗机制、锁重入、红锁等概念消息队列升级异步下单用的 Stream 结构虽然能工作但真正的生产级秒杀会用 RocketMQ 或 Kafka 做更可靠的削峰与重试缓存一致性项目对缓存与数据库一致性的处理相对简化比如更新数据库后没有主动删除缓存这对一致性要求严格的场景会有风险。可以补充先更新数据库再删缓存的“Cache Aside Pattern”并思考失败补偿策略全链路压测学完整个项目后可以尝试用 JMeter 对秒杀接口做压测看不同并发下的错误率和响应时间这比单纯“跑通代码”带来的认知提升大得多。我自己在学这个项目时最大的收获不是某个命令而是“用 Redis 设计一个完整业务链路”的全局视角。在校招面试里黑马点评几乎是 Java 后端候选人的必背项目但真正能把它讲清楚的人很少——多数人是背了缓存穿透、缓存击穿的术语却说不清项目里到底用的哪种方案、为什么选这种方案。这篇总结写下来也是希望你在看源码的时候能像我一样多问一句“为什么不那么写”。想通了这几个为什么这个项目才真正变成了你自己的东西。