ARTICLE DETAIL

资讯详情

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

黑马点评项目面试攻略:Redis缓存穿透、分布式锁与秒杀实战解析

黑马点评项目面试攻略:Redis缓存穿透、分布式锁与秒杀实战解析 做黑马点评这个项目的人这几年真的是肉眼可见地多。我身边投Java岗的朋友十个里有六七个简历上都挂着它剩下几个在做苍穹外卖或者自己魔改的商城。黑马点评这个项目之所以这么受追捧原因其实很简单它是少有的、把Redis用到了“有点过分”程度的单体项目。别人还在讲缓存穿透怎么解决它已经让你自己动手写布隆过滤器、自己做分布式锁、自己做Stream消息队列。对于校招和初中级社招来说只要你把这里面的逻辑真正吃透面试官很难在缓存和并发这个话题上问倒你。这篇内容不是给你复述黑马课程里的代码而是把整个项目拆成面试官真正会追问的知识点再结合我自己的面经和辅导过的同学遇到的真实情况告诉你每个模块应该怎么讲、哪些坑必须提前补。无论你是刚把项目敲完准备背题还是已经在面试中被问到过“你这个分布式锁有没有问题”这篇文章都值得你花半小时读完。1. 项目整体梳理从业务功能到技术亮点的映射1.1 这个项目到底在做什么黑马点评模拟的是一个类似大众点评的本地生活类App核心功能包括短信验证码登录、商品查询与缓存、秒杀抢购、好友关注与Feed流推送、附近商铺查询以及UV统计。虽然项目体量不大但它把一个典型的业务闭环完整地做了出来用户登录、浏览、下单、互动、个性化查询每一个环节都塞进了对应的Redis高级特性。很多同学在准备面试时容易犯一个错误就是把自己当成“代码的搬运工”只记得项目里用了什么技术但讲不出这些技术解决的到底是什么问题。比如你写了缓存那缓存到底缓存了什么为什么这个数据适合缓存如果数据更新了怎么办这些才是面试官想听的。黑马点评的价值恰恰在于它的每一个技术选型背后都有明确的业务痛点。比如商铺数据是典型的读多写少场景用Redis缓存可以大幅降低数据库压力秒杀活动是典型的瞬时高并发场景必须用分布式锁Lua脚本保证不超卖Feed流则是典型的推拉结合场景需要根据粉丝量级选择推送还是拉取模式。面试时只要能把这些“技术-业务-效果”的映射关系讲清楚就已经超过了一半只会背八股文的候选人。1.2 技术栈与模块结构盘点我先把项目的主要技术栈列出来你对照着查漏补缺技术在项目里承担的角色面试常见追问点Redis缓存、分布式锁、消息队列、计数器、GEO缓存一致性、锁的可靠性、Stream和RabbitMQ的对比Spring Boot应用基础框架自动配置原理、启动流程MyBatis Plus数据访问层分页、条件构造器源码MySQL业务主数据库索引优化、事务隔离级别Nginx前端静态资源部署反向代理、负载均衡策略Lombok / Hutool开发效率工具这俩一般不问但Hutool里用到的工具类可以提一嘴从功能模块上看黑马点评最核心的几条线是登录模块用Redis保存短信验证码和登录令牌Token替代传统的Session方案这是分布式环境下会话管理的经典做法商铺查询模块以商铺详情和商铺类型数据为例引出缓存穿透、缓存击穿、缓存雪崩三大问题秒杀模块基于乐观锁和分布式锁解决超卖问题再引入Lua脚本保证原子性关注与Feed流模块使用Redis的Set、ZSet、Stream实现关注关系和消息推送附近商铺模块基于Redis GEO实现按距离范围搜索UV统计模块基于HyperLogLog做基数统计解决海量去重计数问题。这些模块单独拿出来任何一个都能撑起一个五分钟的“项目亮点”介绍。但前提是你真的去看了源码而不是只在Idea里CtrlC、CtrlV。下一步我逐个模块拆解重点讲清楚底层原理和面试追问时的应对思路。2. 核心知识点逐个拆解面试官真正会深挖的地方2.1 缓存三兄弟穿透、击穿、雪崩的完整处理方案缓存穿透、缓存击穿、缓存雪崩这“三兄弟”几乎是Java面试的必考题。黑马点评里商铺详情查询模块把这三大问题的解决方案全部用上了所以面试官特别喜欢借着这个项目问。很多人能背出定义但一到“你项目里怎么解决的”就支支吾吾。下面我把每个问题的处理逻辑都展开讲一遍。缓存穿透指的是查询一个根本不存在的数据缓存和数据库里都没有导致每次请求都直接打到数据库。恶意攻击时大量这种请求足以拖垮数据库。黑马点评里的做法是如果从数据库查询的结果为空也把这个空值写入Redis并设置一个较短的过期时间比如2到5分钟。这样后续同样的请求就直接命中空值缓存不会穿透到数据库。这个方案简单有效但有一个很核心的漏洞如果恶意请求每次都伪造不同的ID空值缓存就完全失效了。所以在面试里一定要主动补充更进阶的方案——布隆过滤器。你可以在请求进来时先用布隆过滤器判断ID是否存在布隆过滤器说“不存在”就一定不存在直接拦截掉说“存在”才放行继续查缓存和数据库。黑马点评的视频里没有写布隆过滤器但这恰恰是你展示思考深度的机会可以跟面试官说“生产环境我会在缓存前加一层布隆过滤器来兜底”这就是加分项。缓存击穿指某个热点key在缓存过期的瞬间大量并发请求同时打到数据库。黑马点评里用的是互斥锁方案在缓存失效后不是让所有线程都去重建缓存而是先让一个线程获取分布式锁去查数据库并重建缓存其他线程先等待等缓存重建完成后再次查询缓存。我用伪代码表示一下核心逻辑public Shop queryWithMutex(Long id) { // 1. 先查缓存 String shopJson redisTemplate.opsForValue().get(CACHE_SHOP_KEY id); if (isNotNull(shopJson)) { return JSONUtil.toBean(shopJson, Shop.class); } // 2. 缓存未命中尝试获取互斥锁 String lockKey lock:shop: id; boolean isLock tryLock(lockKey, 10, TimeUnit.SECONDS); if (!isLock) { // 3. 没拿到锁休眠后重试 Thread.sleep(50); return queryWithMutex(id); } try { // 4. 拿到锁后再次查缓存double check防止上一个线程刚重建完缓存 shopJson redisTemplate.opsForValue().get(CACHE_SHOP_KEY id); if (isNotNull(shopJson)) { return JSONUtil.toBean(shopJson, Shop.class); } // 5. 查数据库并重建缓存 Shop shop getById(id); // 模拟缓存重建耗时 Thread.sleep(200); redisTemplate.opsForValue().set(CACHE_SHOP_KEY id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES); return shop; } finally { // 6. 释放锁 unlock(lockKey); } }这段代码里有两个细节面试官会追问第一为什么拿到锁之后还要再次查缓存因为可能存在两个线程几乎同时获取锁第一个线程释放锁后第二个线程恰好拿到了锁如果不做double check就会重复查询数据库。这种“双重检查锁”的思路和单例模式里的DCL是一模一样的可以顺便提一嘴显得你基础扎实。第二为什么休眠时间取50毫秒这是经验值太短会导致CPU空转严重太长又会拖慢响应时间。更好的做法是使用Java的LockSupport.parkNanos来做更精细的等待控制。缓存雪崩指大量Key同时过期或Redis宕机导致请求全部打到数据库。黑马点评课里给出了两个方案给缓存过期时间加随机值避免同一时刻集体失效以及用Redis集群保证高可用。这俩方案方向上没错但在面试里最好补充一点——你项目里缓存过期时间是30分钟实际生产环境更常见的做法是设置一个基础过期时间加一个随机偏移量比如30分钟 random(0, 300秒)这样可以把过期时间打散。另外如果公司允许还可以考虑服务降级在数据库压力过大时直接返回一些兜底数据比如默认的商铺信息而不是让用户看到报错页面。这样讲面试官会觉得你不仅知道解决方案还想到了异常场景下的兜底策略。2.2 分布式锁的设计为什么不能只用Synchronized黑马点评里最容易暴露基础薄弱的地方就是分布式锁这个模块。很多同学知道服务端要用锁来防止并发问题但提到分布式锁只说“用了Redis的setnx”这就太浅了。先理解为什么单机锁不行如果只有一台服务器用JVM内置的synchronized或者ReentrantLock就够了。但生产环境不可能只有一台服务器请求会通过负载均衡分发到不同机器每一台机器都有自己的JVM锁只能锁住本机的线程锁不住其他机器的线程。这个时候就需要一个“所有人都能看到的、公共的锁”Redis天然适合做这件事因为所有服务器都连同一个Redis。黑马点评第一版分布式锁是基于setnx命令实现的SET lock 1 NX EX 10。NX表示只有当key不存在时才设置成功EX表示设置10秒过期时间防止获取锁的线程中途宕机导致死锁。这个逻辑本身没问题但面试官一定会追问“如果业务执行时间超过了锁的过期时间怎么办”这就是分布式锁领域最容易翻车的点——锁过期释放但业务还没执行完另一个线程拿到了锁导致并发问题。解决思路有两种。第一种是给锁续期也就是开一个守护线程定时检查锁是否还持有如果业务没执行完就自动续期。Redisson框架的watchDog机制就是这个原理默认每10秒检查一次如果锁还在就续期到30秒。这是主流方案也是你可以在面试里展示自己了解分布式锁生态的机会。第二种是不要用Redis实现分布式锁直接用ZooKeeper的临时顺序节点通过Watch机制实现锁的自动释放可靠性和公平性都比Redis强但性能略逊、运维成本更高。还有一点必须提到如果你们是Spring Boot项目一般不需要自己手写锁直接用Redisson的RLock默认就带看门狗自动续期。黑马点评里为了教学是自己手写的但面试时主动提一句“我知道生产环境会用Redisson来避免续期问题”比傻乎乎背代码效果好得多。另外关于锁的可重入性setnx实现的简单锁是不可重入的。如果同一个线程在持有锁时又调用了一个也需要同一把锁的方法就会死锁。Redisson的锁是基于Redis的Hash结构实现的支持可重入。这些细节都应该提前整理到自己的面试笔记里因为真被问到的时候现场思考是来不及的。2.3 秒杀下单全链路从乐观锁到Lua脚本秒杀这一块是整个项目里技术密度最高的部分也是面试官最爱深挖的模块。它涉及四个核心问题超卖问题、一人一单问题、下单流程的原子性问题、以及高并发下的性能优化。超卖问题的根源在于数据库层面的查询-判断-扣减不是原子操作。比如库存还剩1件两个请求同时查到库存为1都判断可以扣减最终都执行了UPDATE stock SET count count - 1结果库存变成-1。黑马点评里最开始用的方案是乐观锁在更新时加上库存大于0的条件。UPDATE tb_seckill_voucher SET stock stock - 1 WHERE voucher_id #{voucherId} AND stock 0;这种写法利用数据库的行锁保证并发安全stock 0就是一个版本校验条件只要库存减到0后面的更新操作影响行数就是0事务回滚。这是最经典的乐观锁思路。面试时要把这个sql解释清楚说明影响行数为0时如何判断秒杀失败以及为什么数据库本身的行锁能够解决并发问题。一人一单问题解决的是同一个用户不能重复秒杀。黑马点评的判断逻辑是查询订单表里当前用户和商品ID是否存在记录如果存在就不允许再次下单。但这里有一个隐蔽的问题在高并发场景下两个请求可能同时通过“订单不存在”的判断接着同时插入订单导致一人多单。这个问题的解决办法在单体应用里可以对用户ID加synchronized锁来保证同一用户串行化但生产环境是集群就必须用前面说的分布式锁了。锁的粒度要细致userId.toString().intern()可以作为锁的key但intern在大量不同字符串时可能有性能问题所以更稳妥的是直接用Redis分布式锁key设计成lock:order:userId。Redis Lua脚本保证原子性是秒杀模块最亮的点。黑马点评的最终版本把判断库存是否充足、判断用户是否已下单、扣减库存、保存订单这些步骤全部封装进一个Lua脚本通过Redis执行。Lua脚本在Redis中是原子执行的要么全部成功要么全部失败不会出现中间状态。面试时如果能直接背出或默写出Lua脚本关键片段会特别加分-- 1. 判断库存是否充足 if (redis.call(exists, KEYS[1]) 0) then return -1; end if (tonumber(redis.call(get, KEYS[1])) 0) then return -2; end -- 2. 判断用户是否已下单 if (redis.call(sismember, KEYS[2], ARGV[1]) 1) then return -3; end -- 3. 扣库存 记录用户 redis.call(incrby, KEYS[1], -1); redis.call(sadd, KEYS[2], ARGV[1]); return 1;这个脚本执行完成后还需要把订单信息发送到消息队列异步保存到数据库。黑马点评里用的是Redis的Stream数据结构面试时如果被问到“为什么不用RabbitMQ”可以从这几个角度回答第一项目本身已经引入了Redis再额外引入RabbitMQ会增加部署和运维成本第二Redis Stream从Redis 5.0开始支持具备消费组、消息持久化、ACK机制足以应对秒杀这种异步削峰场景第三真正海量消息、需要复杂路由、可靠投递的场景才会考虑引入专业的MQ中间件。这个回答既说明你会权衡技术选型又给自己留了台阶。2.4 关注Feed流与附近商铺Redis数据结构的组合拳除了缓存和秒杀黑马点评还有两个值得讲的模块一个是关注Feed流另一个是附近商铺。这两个模块难度适中但很能体现你对Redis数据结构的熟练度。Feed流简单说就是“刷动态”。用户关注了很多人每个被关注的人发了笔记后关注者如何看到这些笔记黑马点评采用的是推模式也就是当作者发布笔记后系统把笔记ID写入每个粉丝的收件箱。收件箱在Redis里用ZSet存储score就是时间戳这样拉取动态时直接按时间倒序排列即可。# 发布笔记时向粉丝收件箱推送笔记ID ZADD feed:{followerId} {timestamp} {noteId} # 滚动分页查询收件箱 ZREVRANGEBYSCORE feed:{followerId} (maxScore minScore LIMIT 0 5这里有一个面试官特别喜欢的细节Feed流的滚动分页为什么不用Page分页因为Feed流的数据是实时变化的如果用户不停发笔记用LIMIT offset, size这种分页方式在翻页时offset会随着新数据的插入而偏移导致读到重复或漏掉内容。所以黑马点评用的是类似“上次查询的最小时间戳”作为游标持续往前翻。这个小知识点非常值得展开因为它把“Redis实战”和“系统设计”两个维度都拉满了。附近商铺模块就简单一些用的是Redis的GEO数据结构底层基于Sorted Set实现。添加商铺时执行GEOADD shopGeo {lng} {lat} {shopId}查询附近时执行GEOSEARCH shopGeo FROMLONLAT {lng} {lat} BYRADIUS 5000 m ASC。GEO本质上是把经纬度编码成一个52位的整数作为score所以可以用ZSet相关的命令去操作它。这个模块面试时不太容易深挖但你至少要知道GEO底层是ZSet以及为什么能支持范围查询。3. 面试怎么讲这个项目才能拿高分3.1 一句话版本的项目导流话术我辅导过很多同学他们最大的问题不是不会技术而是不会“讲项目”。面试官一天面五六个人每个人都说自己做过黑马点评你要是照着课程里的介绍从头背到尾面试官根本听不进去。你需要在30秒内用一个“痛点-方案-效果”模型把项目定调。比如我常用的开场是“我做的是一个高并发的本地生活类应用核心功能包括商铺查询和秒杀。这个项目最大的特点是大量使用Redis来解决高并发场景下的性能和数据一致性问题比如用缓存重建逻辑处理缓存击穿、用分布式锁Lua脚本保证秒杀不超卖、用Redis Stream做异步订单处理。整个项目在压测环境下秒杀接口的QPS从最初的几百提升到几千的量级。”这一段话看起来简单但效果好是因为它同时完成了三件事第一告诉面试官项目的业务背景和核心场景第二主动抛出关键词——缓存击穿、分布式锁、Lua脚本引导面试官往你准备过的方向问第三提到了压测数据暗示你做过性能验证而不是只会跟着视频敲代码。注意这里要如实陈述。如果没做过压测就说“通过接口模拟并发测试验证了结果”不要硬造一个QPS数字。面试官一旦追问“你怎么测的、并发数多少、Redis配置多少”数据说谎很容易被戳穿。3.2 面试官追问时最值得主动展开的三个话题除了被动回答问题你还需要准备两个“主动加分点”。这个技巧很多人不知道面试官问你一个开放性话题时你可以把话题引导到自己准备最充分的模块上。第一个推荐的主动展开话题是缓存击穿的互斥锁方案与Redisson看门狗的区别。你可以主动说“我项目里手写了一个互斥锁但在生产环境我更倾向于用Redisson因为它解决了锁自动续期的问题”。这句话能引出来的后续问题包括Redisson的watchDog实现原理、Redis分布式锁的红锁争论Redlock、ZooKeeper锁和Redis锁的对比、可重入锁的实现方式。这些都是你提前背好就能稳稳拿下的题。第二个推荐的主动展开话题是Stream消息队列与RabbitMQ的选型对比。这几乎是为黑马点评量身定做的高频追问因为课里用了Stream而很多公司实际用的是RabbitMQ或RocketMQ。你主动对比两者既能展示自己会ORM之外的工具又暴露出你了解技术特性的权衡。第三个是GEO的底层结构为什么是ZSet以及ZSet的跳表实现原理。这个问题可以把数据结构基础、Redis底层编码、业务应用串起来属于一个“低成本、高回报”的话题。3.3 容易被问挂的“死角”提前自查很多候选人死在项目本身之外的基础问题上。我整理几个黑马点评相关的高频死角你对照自查第一Redis的过期删除策略。黑马点评里设置了大量带过期时间的key面试官大概率会问“Redis怎么清理过期key”。你要能回答惰性删除每次访问时检查是否过期 定期删除每秒随机抽查部分key清理过期key以及内存淘汰机制LRU、LFU、volatile-lru、allkeys-lru的触发条件。第二ZSet的实现原理。Feed流和GEO都用了ZSet所以你要能画出跳表的结构说明为什么ZSet用跳表不用红黑树以及压缩列表ZipList到跳表的转换条件。第三Lua脚本为什么原子。要能说清楚Redis是单线程模型Lua脚本在执行期间不会被其他命令打断所以整个过程不会有并发穿插。这是整个秒杀方案成立的前提不能含混。第四MySQL和Redis的数据一致性。项目里大量数据先更新数据库再删除缓存面试官会问“如果数据库更新成功但缓存删除失败怎么办”。你要能说出延迟双删、消息队列补偿、设置合理过期时间兜底这几个层面的方案并且理解为什么用“删除缓存”而不是“更新缓存”。第五事务与Lua的对比。有人会问“为什么不用Redis事务MULTI/EXEC来保证原子性”。你要能说清楚Redis事务虽然能保证多个命令按顺序执行但不会回滚而且中间不能根据前一个命令的结果做条件判断而Lua脚本可以所以秒杀场景下Lua完胜。4. 实际面经复盘我整理的高频问题与踩坑记录4.1 高频面试问题速查表我在各个技术社区、面试群里收集了很多黑马点评相关的问题按照出现频率整理成下面这张表。你可以拿来自测如果能不看答案把每个问题讲清楚两分钟以上这个项目就算吃透了。问题考察点回答要点黑马点评里Redis解决了哪些业务痛点项目整体理解从缓存、分布式锁、消息队列、Feed流、GEO、UV统计六个方面回答缓存穿透、击穿、雪崩的区别你分别怎么处理缓存基础穿透用空值缓存布隆过滤器击穿用互斥锁逻辑过期雪崩用随机过期时间集群你的分布式锁是怎么实现的有什么问题分布式锁深入setnx 过期时间 - 可重入 - 自动续期 - 红锁争论秒杀怎么防止超卖并发与数据库乐观锁升级为Lua脚本原子操作怎么保证一人一单锁粒度与分布式用用户ID作为锁key做分布式锁Lua脚本为什么能保证原子性Redis单线程模型脚本执行期间不会被其他命令中断Redis Stream和RabbitMQ怎么选技术选型能力从项目成本、功能需求、可靠性和消息堆积能力四个维度对比Feed流为什么用推模式而不用拉模式系统设计推模式写入多但读取快拉模式存储省但查询慢粉丝量大时可考虑推拉结合GEO底层是什么数据结构Redis底层原理Sorted Set GeoHash编码登录Token为什么要存Redis和JWT比有什么优劣登录方案设计Redis可主动失效、可做服务端状态管理JWT无状态、无法主动踢人4.2 我踩过的坑和总结的临场技巧我在准备这个项目的时候印象最深的一个坑就是只背代码不背原理。刚开始模拟面试时面试官问我“为什么用setnx做锁而不是直接INCR”我愣了几秒因为我确实没想过这个问题。后来我才明白setnx自带的过期时间是原子设置的如果先SETNX再EXPIRE分两步执行第二步失败就会造成死锁。INCR虽然也能实现锁但判断“返回值为1才是成功”会有一个不太直观的语义而且同样要配合EXPIRE使用。这些细节其实才是面试官区分“背过”和“理解”的关键。另一个经验是回答项目问题时一定要学会“结构化表达”也就是先给结论、再给依据、最后补充细节。比如被问到“缓存击穿你如何解决”不要上来就背代码。你可以先说“我用的是互斥锁方案同时对比了逻辑过期方案最后选择了前者”然后再解释两者原理和优缺点。这样的回答框架会让面试官觉得你思路清晰而不是在背诵。最后一个建议是一定要自己做一次完整的项目源码阅读和笔记整理不要只看视频。看视频是“输入”面试是“输出”中间需要靠笔记和模拟问答来衔接。你可以在自己的GitHub上写一个《黑马点评面试复盘》的文档把每个模块画成图、把每段核心代码抄一遍、把每个可能的问题和答案写下来。这个整理的过程本身就会逼着你去思考很多之前忽略的细节。5. 项目还能怎么扩展给你三个简历加分方向黑马点评做得再好毕竟是个教学项目很多同学把课上的功能做完就停了。但面试官见的项目太多了纯原版的黑马点评很难让他们眼前一亮。我个人建议在基础版本之上至少加一个扩展点让项目带上你的个人思考。下面分享三个投入产出比比较高的扩展方向。第一个方向给Feed流做推拉结合模式。原版是清一色推模式作者发笔记后给所有粉丝推送。但你有10万粉丝的时候往10万个收件箱里各写一份数据就太浪费了。这时可以改成普通用户用推模式大V用户用拉模式普通用户刷动态时先查自己的收件箱推再查关注大V的最近笔记拉然后合并排序。这个扩展能引出“粉丝量分级”“读写分离权衡”“归并排序合并数据”等进阶话题非常能体现系统设计能力。第二个方向给秒杀模块做消息队列削峰把Redis Stream替换成一个真正的事前削峰方案。秒杀的核心问题是瞬时流量过高直接打到Redis和MySQL。你可以在秒杀开始前先把参与资格预扣在Redis里用户点秒杀时拿着一个令牌去执行Lua脚本扣减库存然后再把订单发送到RabbitMQ由消费者异步写库。这个扩展能引出“令牌桶限流”“消息不丢失”“消息幂等性”等话题都是面试官喜欢深挖的。第三个方向给查询接口做多级缓存。原版是Redis作为缓存你可以再加上本地缓存Caffeine形成Caffeine - Redis - MySQL三级缓存结构。同时要说清楚缓存一致性怎么保证——Caffeine会通过发送删除事件让其他节点的本地缓存失效Redis负责最终一致性。这个扩展能引出“JVM本地缓存与分布式缓存的对比”“缓存一致性方案”“缓存雪崩的兜底策略”等话题非常契合目前大厂高并发场景。我个人建议三个方向里挑一个做深做透就行了千万不要三个都写进简历否则面试官会觉得你只是堆砌名词。做一个、讲清楚一个比贪多嚼不烂强得多。最后再说一个我最近整理面经时的体会黑马点评这个项目本身不是目的它是你通往“Redis实战经验”的一根拐杖。面试官真正想看到的是你能否透过代码看到背后的设计思想和业务取舍。你把这个项目的代码删掉、把Redis和MySQL换成另一个中间件你还能不能讲清楚为什么这么做如果能那黑马点评对你来说就真的过关了。准备面试的时候不妨每学完一个模块就合上电脑问自己一句“如果不看任何资料我能把这个模块的来龙去脉讲给别人听吗”多这样练习几次面试场上你会比大多数候选人淡定得多。
返回列表