ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

黑马点评项目复盘:缓存、分布式锁与秒杀等高频技术点解析

黑马点评项目复盘:缓存、分布式锁与秒杀等高频技术点解析 金三银四那阵子我密集面过一轮 Java 岗位十个候选人里简历上写“黑马点评”的至少有四五个。问法可能不太一样但内核永远绕不开这几个缓存穿透你怎么解决的、秒杀超卖怎么处理、分布式锁为什么选 Redis 不选 Zookeeper。说实话这个项目在面试市场里的存在感已经高到有点“烂大街”了——但烂大街不代表没价值恰恰说明它踩中的技术点是 Java 后端日常开发里真正高频、真正绕不过去的东西。这篇总结不是把黑马点评的代码从头到尾抄一遍而是把我自己从跟着视频敲代码、到改造成自己版本、再到面试时被反复追问的完整复盘按几个看得见摸得着的维度拆开。你现在如果在学这个项目或者已经敲完准备面试或者简历里写了但心里没底这篇文章应该都对得上胃口。我会把重点放在“为什么要这么做”和“面试会怎么问”上而不是又贴一遍源码。1. 黑马点评到底练的是什么项目含金量拆解1.1 从“会调接口”到“会做技术选型”很多学生项目长一个样一个管理后台几张表增删改查套上 Controller-Service-Mapper 三层就完事了。这种项目写进简历面试官基本只看一眼就跳过因为谁都清楚那里面没有“问题”自然也就没有“方案”。黑马点评不一样。它表面上是仿大众点评的点评平台实际上是一连串现实业务问题的集合体。你能在一个项目里同时遇到登录态共享、热点缓存、高并发抢购、地理位置检索、用户签到、Feed 流这类问题——这些问题不是设计者编出来的是大众点评这个体量的产品真实会遇到的。把一个真实业务模型的核心矛盾搬到一个可控的 Demo 里让你用尽量小的成本演练一整套解法这是它最大的价值。换句话说这个项目逼着你从“接口能调通”往前走一步去思考这个功能在并发 10 和并发 10000 的情况下设计上有什么不同Redis 放这儿到底是当缓存用还是当存储用数据库扣库存和缓存扣库存有什么区别这些思考本身就是后端开发的核心能力。1.2 技术栈与业务模块对照这个项目藏了多少知识点我见过有人把黑马点评写成“基于 Spring Boot 和 Redis 的仿大众点评项目”这描述太单薄了。真正聊起来的时候面试官关心的是你在每个模块里用了什么方案、为什么用、有没有对比过其他方案。我习惯把项目的模块和技术点拆成一张对照表这样自己心里清楚简历上也写得明白业务模块使用的核心技术对应的重点问题短信验证码登录Redis 存储验证码与登录 Token、拦截器Session 共享问题、Token 有效期续期商铺查询缓存Spring Cache、Redis 缓存、缓存更新策略缓存穿透、击穿、雪崩、更新顺序优惠券秒杀全局唯一 ID、乐观锁、分布式锁、Lua 脚本、消息队列超卖、一人一单、异步下单、削峰附近商铺Redis GEO地理位置检索、距离排序用户签到BitMap位图存储、连续签到计算UV 统计HyperLogLog海量数据去重、内存效率好友关注与 Feed 流Redis 中的 List / ZSet、推拉结合模式粉丝量大的时候推送怎么扛住这张表拿出来项目就不再是一个“黑马点评”的笼统名字而是一组可以逐项深入的技术话题。面试官随便挑一个你都有东西可以讲。后面几节我就按这张表的路线逐个复盘。2. 登录与缓存主链路里最容易讲深的两块2.1 短信登录Session 换成 Redis 绝不只是“换存储”黑马点评的第一个模块是短信验证码登录。这个模块第一版用 Session 保存验证码和登录状态后来改成用 Redis 保存。很多人看完只知道“改用 Redis 了”但面试官问“为什么不用 Session”时给出的答案往往很浅。Session 方案最大的问题是分布式环境下的会话共享。项目部署在单机的时候Session 存在这台 Tomcat 里用户下次请求只要还在同一台机器上就没问题。可一旦上集群用户第一次请求落在 A 机器第二次被负载均衡转发到 B 机器B 机器上找不到他的 Session用户就被当成未登录了。要让 Session 在多个节点间共享要么做 Session 粘滞要么引入 Spring Session 把 Session 数据扔进 Redis——本质上都是把会话数据外置化。黑马点评的做法更直接干脆不依赖 Session 这个容器登录成功后生成一个随机 Token 作为 key用户信息序列化后作为 value 存进 Redis并设置过期时间。客户端把 Token 存在请求头里每次请求由拦截器从 Redis 中读取用户信息并刷新过期时间。这里有两个容易被问住的细节。第一个是 Token 的过期时间怎么设计。如果用户一直在操作不能让他 30 分钟一到就被迫重新登录。黑马点评用了一个很实用的技巧拦截器先刷新 Token 的有效期再判断是否登录。你可以用两个拦截器实现——第一个拦截器负责刷新所有请求的 Token第二个拦截器只拦截需要登录的路径。有些资料里会写成“拦截器链”其实核心就一句话刷新过期时间的逻辑不能放在登录校验失败之后否则用户未登录时 Token 永远不会被续期体验很割裂。第二个细节是为什么要存用户对象而不是只存 userId。如果每个接口都从 Redis 拿 userId 再去数据库查用户压力全打到 MySQL 上。把用户基本信息直接缓存在 Redis 里接口可以直接用这是典型的“读写分离缓存加速”思路。代价是用户昵称这类信息修改后缓存可能不一致后面要配合缓存更新策略处理。2.2 缓存更新先更新数据库还是先删缓存别背答案商铺查询模块是典型的缓存应用场景查询店铺时先查缓存缓存没有就查数据库再把结果写回缓存。但“缓存里有数据”不等于“缓存里的数据是对的”于是缓存更新策略就成了必考题。黑马点评采用的是 Cache Aside Pattern也就是旁路缓存模式。具体来说读请求先查缓存没命中再查数据库然后回填缓存写请求先更新数据库再删除缓存。为什么是“删除缓存”而不是“更新缓存”因为更新缓存是写操作如果这个缓存 key 被频繁更新而很少被读取每次更新都要重写缓存浪费性能而删除缓存是懒惰策略等下次读的时候发现没命中再回填能省掉很多不必要的写操作。面试官通常会追问一个问题“先更新数据库再删缓存如果删缓存失败了怎么办”这个问题没有完美答案但对方案的理解深度一下就拉开了。常规的补偿手段是延迟双删先删缓存再更新数据库隔一小段时间比如 500ms再删一次缓存目的是把并发读请求在更新期间写入的旧缓存清掉。更工业级的做法是订阅 MySQL 的 binlog通过 Canal 之类的组件把变更消息推给缓存服务由缓存服务删除对应 key相当于把删缓存的操作异步化避免和业务代码耦合。这里我还要多说一句很多资料把“先更新数据库再删缓存”当成银弹实际在“读多写少”的高并发场景这个策略确实是最稳的但如果你业务里写非常频繁删缓存会导致大量缓存重建这时候反而要考虑其他模式。黑马点评是典型读多写少用这套没问题但你要能说出“为什么在这个场景下适用”而不是背结论。2.3 穿透、击穿、雪崩三个容易混的高频问题这三个问题名字像面试也常连着考但很多人讲不清区别。我用自己的话拆一遍缓存穿透是指请求的数据在缓存和数据库中都不存在每次请求都直接打到数据库。攻击者可以故意构造大量不存在的 ID把数据库击穿。黑马点评的解法是缓存空值查询数据库没结果时也往 Redis 里写一个空值并设置短 TTL比如 2 到 5 分钟。这样同一个“不存在的数据”短时间内不会反复打到数据库。布隆过滤器也能解决但布隆过滤器有误判率还会增加系统复杂度在一个 Demo 项目里用空值缓存足够面试时提到布隆过滤器作为进阶方案即可。缓存击穿是指某个热点 key 在缓存过期的一瞬间大量请求同时发现没命中一起打到数据库。黑马点评给了两个方案。方案一是互斥锁只有一个线程能拿到锁去重建缓存其他线程等待。方案二是逻辑过期不给 key 设置物理过期时间而是在 value 里存一个过期时间戳查询时发现逻辑过期就返回旧数据同时开一个独立线程去重建缓存。这两个方案讲清楚对比表就赢了维度互斥锁逻辑过期数据一致性强一致重建期间读不到旧数据弱一致读到的可能是过期数据用户体验缓存重建期间请求会短暂阻塞请求无感知性能最好实现复杂度需要分布式锁 / JVM 锁需要线程池异步重建适用场景对一致性要求高的数据数据一致性要求不高、并发极高的热点数据缓存雪崩是指大量 key 在同一时间段过期导致数据库瞬间压力激增。解决思路很简单给 TTL 加随机值让过期时间分散开。黑马点评里的实际做法是在设置缓存时给基础过期时间加上一个随机数比如baseTTL random(100, 500)秒。再往深一层Redis 本身的高可用部署主从哨兵也是防止雪崩的兜底手段。3. 秒杀模块是这个项目最硬核的部分从超卖到异步削峰3.1 全局唯一 ID 和超卖问题第一道坎怎么拆秒杀模块是整个项目技术含量最密集的地方没有之一。业务逻辑很简单用户秒杀券校验库存和资格生成订单。但并发一上来处处都是坑。先看全局唯一 ID。订单表的主键如果用数据库自增 ID在分表场景下多个表各自自增会重复同时自增 ID 有规律容易被竞争对手根据 ORDER_ID 推测出订单量。黑马点评用 Redis 的 INCR 命令生成一个自增序列再结合时间戳拼出一个 64 位 long最高位符号位 0中间放时间戳低位放自增序列。这样全局唯一、趋势递增、并且安全性也高一些。然后是超卖问题。扣减库存的 SQL 如果写成这样UPDATE tb_seckill_voucher SET stock stock - 1 WHERE voucher_id #{id}并发情况下多个线程同时读到 stock1同时执行减 1最后可能出现 stock-1。这就是典型的超卖。最简单可靠的解法是在 SQL 里加库存条件UPDATE tb_seckill_voucher SET stock stock - 1 WHERE voucher_id #{id} AND stock 0受影响行数为 0 就说明库存不够了。这就是乐观锁的思路——不加锁靠带条件的更新保证数据安全。面试时如果只是答出这个 SQL只能算及格你还需要说为什么不用悲观锁悲观锁SELECT ... FOR UPDATE在并发高的时候会锁表/锁行性能损失大而且如果锁范围控制不好会引发死锁。乐观锁在秒杀这种“冲突率高但冲突时间短”的场景下已经足够。3.2 从 synchronized 到分布式锁锁的演进本身就是一道面试题黑马点评里还有一个需求叫“一人一单”也就是同一个用户不能秒杀多张券。只扣库存还不够还要校验“这个用户是否已经下单”。初学者一般会想到synchronized但这里有两个问题第一synchronized只对单体应用有效项目部署多节点后A 节点的锁管不住 B 节点的线程第二如果锁加在save方法上两个请求可能因为不同用户 ID 也互相排队导致吞吐量下降。进阶做法是锁用户维度synchronized(userId.toString().intern())把锁粒度从方法级降到用户级。但依然解决不了多节点问题。这时候就得上分布式锁。Redis 分布式锁最经典的实现是SETNX key value EX seconds但要注意几个坑。第一个坑是锁必须设置过期时间否则持有锁的线程崩溃锁永远不会释放而 SETNX 和 EXPIRE 必须原子执行所以要用SET key value NX EX seconds一条命令完成。第二个坑是释放锁时要判断是不是自己加的锁——如果线程 A 的锁已经过期线程 B 加了新锁A 执行 DEL 会误删 B 的锁。所以释放锁要用 Lua 脚本比较 value 线程标识后再删除。第三个坑是锁不可重入同一个线程在嵌套调用里会把自己的锁卡死。这三个坑踩完面试官大概率会追问一句“你还有更好的方案吗”这时候抛出 Redisson。Redisson 的分布式锁天然支持可重入并且内置看门狗机制默认锁超时是 30 秒如果业务没执行完看门狗每 10 秒自动续期到 30 秒。这个设计说实话很精妙它试图解决的是“业务执行时间不可预估”的问题。面试时能讲清楚看门狗原理这个模块就会被认为你是真正用过的。3.3 异步秒杀Lua 脚本 消息队列把并发峰值拉平黑马点评原版比较靠后的内容引入了异步秒杀核心是把“判断库存和校验一人一单”这两步放进 Redis 里用 Lua 脚本执行保证原子性然后用消息队列异步创建订单。这里 RabbitMQ 和 Stream 都是可选项根据你简历写的技术栈来定。为什么用 Lua 脚本因为判断库存、扣库存、校验用户是否已下单这三步如果分开执行中间任何一步都可能被其他请求打断产生并发问题。Lua 脚本在 Redis 中是原子执行的整个脚本要么全部执行要么全部不执行天然解决了并发竞争。这个思路在面试中非常加分因为它展示了你对 Redis 原子性的理解而不是简单地把 Redis 当成缓存。异步化的收益你得会算账同步秒杀下一次请求要完成“扣库存 创建订单”高峰时数据库的写压力直接拉满异步化之后前端请求只做一件事——用 Lua 预扣库存并返回“秒杀成功”后续的订单创建丢给队列慢慢消费。这样数据库的负载从瞬时尖峰变成一个平稳的流量。面试时你如果能把这个“削峰填谷”的过程画出来讲清楚就已经很加分了。但我得提醒一句这种异步化引入了新的问题——消息队列消费失败会导致用户明明秒杀成功却查不到订单。所以生产者要确认消息真的发出去了消费者要做幂等处理还要有失败重试机制。面试的时候主动聊这些比被动等面试官问要有效得多。4. 容易被忽视的 Redis 高级数据结构GEO、BitMap 与 HyperLogLog4.1 GEO附近商铺是怎么查出来的附近商铺这个功能放在十年前可能要自己做地理栅格计算现在 Redis 提供了 GEO 系列命令几十行代码就搞定了。核心思路是把商铺的经纬度存进 Redis 的 GEO 集合然后用GEOSEARCH按经纬度和半径来查询附近店铺。GEOADD shop:geo 116.397128 39.916527 shop:1 GEOSEARCH shop:geo FROMLONLAT 116.397128 39.916527 BYRADIUS 5 km ASC面试官会问的一个问题是GEO 底层数据结构是什么答案是基于 ZSet 实现的。你把一个经纬度编码成一个 score 值存进 ZSet查询附近就是一次按 score 范围扫描。能答出这一层说明你不是只会调命令。GEO 适合半径几公里到几十公里的“附近”检索数据量在百万级以内性能都还不错。如果店铺数量达到亿级需要考虑分片或者引入专业的 LBS 服务但这已经超出黑马点评的定位了。面试时主动说出这个边界条件反而显得你有架构意识。4.2 BitMap 和 HyperLogLog数据量大了以后的内存神技用户签到功能学生项目一般是一张签到表一个用户一天一条记录。一个月下来1000 万用户就要 3 亿条记录数据库压力很大。黑马点评换了个思路——用 BitMap。每个用户每个月的签到状态用一个 31 位的二进制串表示某一位是 1 就代表当天签到了。统计连续签到天数时只需要把当天的几个位做位移和与运算。这种做法的内存开销小到可以忽略不计一个用户一年也就 366 个 bit。面试时讲 BitMap你要学会算账一条签到记录如果存数据库至少几十字节用 BitMap一个用户一个月只要 4 字节左右。差了 10 倍以上。这种“一个字节都不浪费”的思路在物联网、风控、推荐系统里到处都有面试官很吃这一套。UV 统计同理。统计一个页面的独立访客数如果用 Set 存储每个用户的 ID1000 万用户就要存 1000 万个字符串内存爆炸。HyperLogLog 是一种概率数据结构固定占用 12KB 左右内存就能统计 2 的 64 次方规模的基数。代价是有约 0.81% 的误差。如果业务上能接受 99% 的准确性——比如统计访问量看趋势这个误差完全可以接受。可如果要用它做精确对账那就得考虑用 Set 或精确去重方案了。5. 简历和面试怎么讲术语、话术与追问防御5.1 简历上这样写面试官才愿意往下问很多人简历上是这样写的“使用 Redis 实现缓存、解决缓存穿透、完成秒杀功能。”这句话信息量接近零。面试官看到这种写法基本认定你只是把视频里的关键词抄了一遍。我的建议是每一个技术点都写成“问题—方案—收益”的结构并且量化。拿秒杀模块举例可以这样写“负责优惠券秒杀模块设计基于 Redis 的 Lua 脚本实现库存预扣和一人一单校验结合 RabbitMQ 异步生成订单将高峰下单请求同步阻塞降低约 70% 左右支持单机 QPS 约 2000 以上的秒杀场景。”哪怕这些数字是你自己压测出来的也比空泛的描述可信得多。我见过有人直接在简历里写“解决超卖问题”但完全提不出超卖发生的条件这种反而更容易被拆穿。所以简历里出现的每个词你都要准备好被追问三层。5.2 面试现场最容易被连续追问的六个问题我根据自己的真实面试经历和被周围人问到的题目整理了六个出现频率极高的问题。每个问题都不好答但每个都有明确的思路。第一“为什么用 Redis 做缓存而不用本地缓存”你得答出 Redis 是分布式共享的多个服务实例之间能拿到同一份数据本地缓存比如 Caffeine 虽然快但每个实例各存一份更新不一致。同时要提一嘴 Redis 的持久化能力。第二“缓存穿透、击穿、雪崩的区别以及你项目里分别怎么处理的”这个问题必问我前面已经详细拆过把那张对比表背熟就够用了。第三“你用的分布式锁如果 Redis 节点挂了怎么办”这是个进阶题直指 Redis 分布式锁的缺陷。你至少要能说出 RedLock 的概念并承认它也有争议生产环境需要结合业务权衡。第四“Lua 脚本执行失败怎么办”Lua 脚本在 Redis 中原子执行一般不会执行到一半失败。如果脚本本身语法错误或者运行时出错Redis 会返回错误不会执行任何写操作。更常见的问题是脚本里判断的逻辑不对导致库存扣了但订单没发出来这时需要消息队列的消费者做补偿。第五“RabbitMQ 消息丢失怎么处理”要从三个环节答生产者确认机制、消费者手动确认、持久化交换机/队列/消息。能串起来讲说明你真正处理过消息队列的问题。第六“为什么不用 Zookeeper 分布式锁”这个问题考的是你对两种组件特性的理解。Zookeeper 的临时顺序节点天然支持可重入和自动释放但 ZK 本身的写入吞吐远低于 Redis而且引入 ZK 会增加运维复杂度。Redis 锁在极端情况下可能因为主从切换导致锁丢失但绝大多数互联网场景下 Redis 锁的可用性已经足够Zookeeper 更多用在强一致性的分布式协调场景。5.3 一些常见的翻车现场和应对思路我见过几个特别典型的翻车现场这里直接列出来你复习的时候可以对号入座。第一个翻车是把课程里“为了演示”的代码当生产方案讲。比如有人被问“缓存空值为什么 TTL 设那么短”他完全答不上来。这时候你应该主动说出“空值缓存只能缓解穿透不能根治真正要根治得靠参数校验、布隆过滤器或者限流”这样面试官会认为你有判断力。第二个翻车是过度包装。有一个候选人简历写了“使用 ElasticSearch 实现点赞列表”结果现场连倒排索引都解释不清。这种虚写一眼就被看穿。我给的建议是宁可只写自己真正动手验证过的功能也不要堆砌名词。黑马点评本身已经够用想区分度高可以加一个你真正跑通的扩展点比如用 Jmeter 压测并给出优化前后的数据对比。第三个翻车是太小看这个项目觉得自己只是“照着视频做的”。面试最怕这种回答因为它证明你没有思考。你完全可以换个角度虽然项目是跟着教程做的但你把里边的方案拆开分析过重构过代码换过消息队列补过单元测试做过压测对比——这些都是自己的产出。6. 一些最后想说的话如果你问我黑马点评这个项目值不值得写进简历我的回答是值得但前提是你得把它变成“你自己的项目”。不是课程代码敲一遍就叫掌握了而是你要能回答出每个技术方案为什么存在、它解决了什么问题、又带来了什么新问题。我给自己复盘的时候最大的感触是一开始以为难点在“实现”后来发现难点在“选型”和“权衡”。缓存策略选哪种、同步还是异步、锁的粒度多大、要不要引入消息队列这些才是后端开发真正的日常。黑马点评把一个又一个这样的选择题摆在面前你每解决一个都能在面场上多一分底气。如果你正在准备面试最后再分享一个我自己的建议不要只在脑子里过项目拿一张纸把三个问题写下来——“这个模块遇到了什么问题”“我用什么方案解决的”“这个方案的缺点和替代方案是什么”。能写完三页纸面试官问不倒你。
返回列表