
1. 项目整体认知黑马点评到底在解决什么问题1.1 项目背景与业务模块黑马点评这个项目本质是一个模拟大众点评/美团 hybrid 模式的商户点评类应用。它看起来是一个单体SpringBoot项目但里面塞进了非常多的经典业务场景短信验证码登录、商户查询缓存、优惠券秒杀、好友关注Feed流、附近商户搜索、UV统计、签到打卡等等。为什么这套项目在Java后端求职圈里这么火核心原因就一个它在单体应用的壳子里把Redis的典型应用场景基本上全部串了一遍。你去看很多培训机构的项目要么是电商CRUD要么是管理后台Redis顶多拿来存个登录态、做个缓存问深一点就答不上来了。而黑马点评的设计者很明显是对着Redis官方文档的使用场景缓存、分布式锁、计数器、发布订阅、Stream、Geo、HyperLogLog、BitMap一个不落地做了映射。所以准备这个项目的过程本质上就是把Redis知识体系过了一遍。业务模块主要分为这么几块用户模块短信验证码登录、用户信息维护、签到统计。商户模块商户查询、缓存更新、类型查询、附近商户搜索。优惠券模块秒杀券下单、库存扣减、一人一单限制、订单超时关单。关注模块用户关注、共同关注、关注推送Feed流。数据统计模块UV统计、签到次数统计。你把这些模块拆开看每一个都是一个面试题。商户模块对应缓存三兄弟问题穿透、击穿、雪崩优惠券模块对应分布式锁、Lua脚本、消息队列附近商户对应Geo签到和UV统计对应BitMap和HyperLogLog。所以这个项目不是让你背几个接口怎么调而是要你把整个Redis知识树通过业务串起来。1.2 技术栈与选型逻辑黑马点评的技术栈本身不算新Spring Boot 2.x、MyBatis-Plus、MySQL、Redis、RabbitMQ扩展部分但选型很讲究。我的建议是你在准备面试介绍项目时不要只报菜名要讲清楚每个组件在这个项目里承担什么角色。举个例子Redis在项目里的角色就不是单一的存登录token对应的用户对象利用TTL实现自动过期。存商户数据利用空值或布隆过滤器解决穿透。存秒杀库存利用Redis单线程特性配合Lua保证原子扣减。存关注推送的收件箱利用ZSet按时间排序。存地理坐标利用Geo结构实现附近商户。存签到记录利用BitMap按位标记。存UV数据利用HyperLogLog做近似统计。MySQL负责的是业务主数据的持久化订单表、店铺表、用户表这些。RabbitMQ在扩展环节用于异步重建缓存、处理秒杀订单的异步下单。这套选型背后其实有一条主线能用Redis内存特性解决的就不打到MySQL能O(1)或O(logN)解决的就不全表扫。你在面试时要能主动把这条主线讲出来这是项目体现“设计感”的关键。2. Redis在项目里的五个核心战场2.1 短信登录与Session改造从单体Session到Redis追踪黑马点评的登录模块设计得很有代表性。传统的单体项目里登录状态一般放Session靠Cookie里的JSessionId关联。但这个方案在分布式部署下天然有问题请求被负载均衡到不同机器Session不共享就得引入Spring Session做Session同步方案重且不优雅。黑马点评的做法是验证码和登录用户信息都直接存Redis用自定义Token代替SessionId返回给前端。具体链路我在后面第3章细讲这里先讲设计思路的核心逻辑——把登录态从“服务器内存”搬到“独立缓存层”。这么做有三个直接收益天然支持水平扩容不需要额外的Session共享组件。Redis的TTL机制天然适合登录过期场景可以做到“最后一次操作后30分钟过期”比Session有效期管理灵活得多。可以方便地存储更多用户画像数据后续扩展权限、风控都方便。面试中容易被追问的一个点是为什么用Hash结构存用户信息而不是直接存JSON字符串这个我当时也纠结过。用String存JSON读写都简单但要修改某个字段就得整个读出再写回。用Hash结构每个字段独立存储修改昵称、签名这类操作可以只更新单个字段。而且在内存占用上Hash在字段少的时候用了ziplist编码比JSON字符串更省内存。所以黑马点评里用户对象用Hash存储这个细节面试官问了就是加分项。2.2 商户缓存穿透、击穿、雪崩的三重防线缓存这块是整个项目里面试密度最高的区域没有之一。黑马点评的商户查询做了一个典型的Cache Aside Pattern也就是先读缓存读不到再读DB然后回填缓存。这个模式本身不难难的是三种异常情况怎么防。穿透查询一个不存在的id每次都打到DB。黑马点评的做法是缓存空值就是即使DB查询结果为null也往Redis里写一个空值TTL设短一些比如2到5分钟。这样后续相同请求直接命中空值不会打到DB。要穿透必须换着id打那就要上布隆过滤器做前置过滤但布隆过滤器的缺点是存在误判率而且删除不方便所以项目中主要用缓存空值兜底。击穿某个热点key突然过期大量请求同时打到DB。黑马点评的做法是互斥锁就是在缓存未命中时先尝试获取分布式锁拿到锁的线程查DB回填缓存其他线程等锁释放后再查缓存。还有一种方式叫逻辑过期就是不给key设TTL而是在value里存一个过期时间字段每次读取时主动判断发现逻辑过期就尝试获取锁并另起线程重建缓存读线程先返回旧数据。两种方案各有取舍互斥锁实现简单、数据一致性好但存在等待时间逻辑过期性能好、无等待但实现复杂且短暂返回脏数据。面试时建议说清楚自己选哪种、为什么以及两种方案的对比。雪崩大量key同时失效导致DB压力骤增。黑马点评的思路是给TTL加一个随机因子比如基础上加1到5分钟的随机值避免同一批key在同一时刻集体过期。另外还能做多级缓存、Redis集群高可用但那属于架构层面的扩展话题了。2.3 优惠券秒杀从超卖到分布式锁再到Lua脚本秒杀是整个项目里技术深度最深、面试官最爱深挖的一块。从最初的超卖问题开始到最终用Lua脚本一把梭整个演进链路本身就是一套完整的面试素材。超卖问题高并发下扣减库存先查库存再更新库存这一查一改之间库存就被别人扣掉了最后出现超卖。第一个方案是乐观锁用版本号或库存本身作为版本条件update时带上stock 0条件影响行数为0就说明没抢到。这个方案能解决超卖但如果抢购失败用户那边体验很生硬没有重试机制。一人一单问题秒杀券通常限制每人只能买一单。实现思路是下单前先判断用户是否已存在订单但这个判断和插入订单之间如果没有原子性保障并发下还是会存在一个人抢多单。黑马点评的演进思路是先加synchronized锁只锁单机然后升级为Redis分布式锁用set lock uuid ex nx保证集群环境下的互斥。如果是基于Redisson实现的分布式锁还有看门狗自动续期避免业务执行时间过长导致锁过期。但分布式锁只解决了查询和下单判断这一段逻辑的互斥库存扣减的原子性还得靠Lua脚本。最终方案是用Lua脚本把“判断库存是否充足、扣减库存、判断是否已下单、写入订单”这几个步骤合并为一个原子操作Redis单线程执行Lua天然保证了这段逻辑不会被并发穿插。这是秒杀链路里最核心的一个设计点后面第3章我会把完整脚本结构贴出来。2.4 附近商户与签到统计Geo、BitMap与HyperLogLog的实战应用这几个数据结构属于“知道的人少、用了就出彩”的加分项。附近商户用的是Redis的Geo结构。实现思路是先把所有商户的经纬度通过GEOADD写入Redis查询时用GEOSEARCH根据用户坐标和半径搜索附近商户。更关键的一个细节是分页问题传统分页用LIMIT offset count但Geo搜索没有直接的分页能力。黑马点评的解决思路是先把搜出来的所有商户id取出来再按商户类型做分组每组各自排序和分页。这个点面试时可以主动提展示你考虑过真实业务场景里的工程细节。签到统计用的是BitMap。每个用户一年签到状态用一个365位的位图表示签到当天把对应位设为1通过BITFIELD命令批量操作计算连续签到天数时按位从后往前数数到0为止。这个方案的优越性在于365天只需要46字节即使每天签到一次十年也就几百字节内存开销几乎可以忽略。UV统计用的是HyperLogLog。UV和PV不一样UV需要去重如果直接存用户id集合再大的内存都不够。HyperLogLog用概率算法标准误差0.81%换来的是每个key固定占用12KB不管多少用户来访问内存都是常量级。做活动页面的独立访客统计时这个结构比用Set存用户id划算得多。3. 关键功能实现拆解面试官最爱问的几个点3.1 短信验证码登录的完整链路这一块我认为值得完整梳理一遍因为它的完整度能体现一个人对“登录”这件事的工程化理解深度。整个登录流程分三步用户输入手机号点击获取验证码后端生成6位随机码存Rediskey用login:code:{phone}TTL设为5分钟同时通过短信服务商发送给用户。用户输入验证码后端从Redis取出比对一致则继续不一致直接返回“验证码错误”。比对通过后从MySQL查用户查不到就自动注册一个新用户。然后生成一个随机Token比如UUID或更随机的字符串把用户对象以Hash结构写入Rediskey为login:token:{token}TTL设30分钟。前端后续请求带着这个Token来访问。这里有个很容易被忽略但非常加分的细节Token怎么保证不可预测很多初学者会用UUID拼接一些固定字符串安全性其实不够。我在实操中更推荐用SecureRandom生成随机字节再Base64编码为URL安全的字符串或者用UUID去掉横杠加上一个随机盐值。黑马点评原版用的是UUID但面试时你如果能主动说“UUID虽然够用但可预测性方面我做了改进”面试官会很吃这一套。另一个细节是刷新TTL。每次请求都带着Token后端应该把这个Token对应的key重新设置过期时间这样用户连续操作就不会被中途踢下线。这相当于一个滑动过期策略业务上用户体验更好。注意这里要用EXPIRE命令重新设置而不是重新写入整个key。3.2 缓存一致性怎么保证的缓存和数据库的一致性是面试重灾区。黑马点评里的场景主要是商户信息的更新面试考察的是一个很经典的问题先更新数据库还是先删缓存先更新DB再删缓存可能的问题是更新DB成功、删缓存失败缓存里还是旧数据。先删缓存再更新DB可能的问题是删完缓存后、更新DB前有请求把旧数据重新写入缓存导致缓存长期是脏数据。更细一点说标题里的答案应该是先更新DB再删缓存并配合延迟双删或消息队列补偿。黑马点评里采用了先更新DB后删缓存的方案同时在扩展部分引入了RabbitMQ更新DB成功后发送消息到队列消费者删除对应缓存。如果删除失败消息重试最终保证一致性。我自己在阅读这块源码时还发现一个细节原版项目在更新商铺时用的接口是updateById更新完直接del缓存。但实际把缓存删了之后如果紧接着大量请求进来会发生缓存击穿。所以更稳的写法是更新DB前先主动把缓存删除而不是更新后删或者在删除后短暂地加一个互斥重建的过渡逻辑。面到这一层项目深度就比较明显了。3.3 秒杀流程的原子性保证秒杀下单的核心问题用一句话说就是多个操作之间只要存在非原子窗口就有并发风险。黑马点评的最终方案是把判断、扣减、下单三个动作通过Lua脚本合并为一次Redis调用。一段典型的秒杀Lua脚本逻辑是这样的-- 判断库存是否充足 if tonumber(redis.call(get, stockKey)) 0 then return 1 end -- 判断用户是否已下单 if redis.call(sismember, orderKey, userId) 1 then return 2 end -- 扣减库存 redis.call(incrby, stockKey, -1) -- 记录用户已下单 redis.call(sadd, orderKey, userId) return 0这个脚本的核心思想是把判断逻辑和写操作全部交给Redis单线程执行。Redis本身是单线程处理命令所以一段Lua脚本在执行期间不会被其他命令穿插原子性天然成立。调用方拿到返回值后0表示成功1表示库存不足2表示重复下单。真正的订单落库可以异步处理进一步降低数据库压力。面试官如果追到线程模型这个层面你要能说清楚为什么Redis单线程还能这么快答案核心在内存操作、IO多路复用、避免上下文切换和锁竞争。但也要补充一点Lua脚本虽然原子但执行时间长了会阻塞Redis服务所以脚本里绝对不要写耗时的操作比如循环上万次更不要在里面写redis.call(keys, *)这类O(N)命令。3.4 Feed流推送的收件箱模型关注模块里有一个比较容易被面试官追问的设计关注推送的Feed流到底是推还是拉黑马点评里用的是推模式的变种叫“收件箱模型”。用户在发布内容时把内容推送给自己的每个粉丝粉丝查看Feed流时直接从自己的收件箱里读。这里的收件箱就是Redis里的ZSetscore是发布时间戳value是内容id。粉丝翻页时用ZREVRANGEBYSCORE按时间倒序取出需要的contentId列表再去Redis或DB里查询内容详情并封装。推模式解决的是“读扩散”问题每个用户读自己收件箱时间复杂度从需要查询所有关注对象的动态再聚合变成一次ZSet范围查找性能非常好。坏处是写放大一个大V有几百万粉丝发一条动态就要推送几百万次粉丝少的内容创作者会浪费资源。所以实际项目中更多用“推拉结合”大V的内容走拉模式普通用户走推模式。黑马点评原版没有做这个区分但面试时主动提这个优化会显得你有架构意识。我当时就是把“收件箱按粉丝数量分级”作为一个扩展点讲给面试官的反馈很不错。4. 面试怎么讲黑马点评项目从自我介绍到深挖应对4.1 一分钟项目介绍模板面试官让你介绍项目时切忌上来就背功能列表。我整理了一个可以直接套用的模板你按照这个逻辑讲面试官基本能快速抓到重点我做过一个商户点评类项目整体是SpringBoot单体架构核心是围绕Redis解决高并发场景的性能和数据一致性问题。项目包含商户查询、优惠券秒杀、关注Feed流、附近商户搜索等模块。我在其中主要负责两块一是商户缓存体系的设计解决了缓存穿透、击穿、雪崩问题二是优惠券秒杀功能的实现通过RedisLua脚本保证了库存扣减和一人一单的原子性结合异步下单削峰。另外我还用Geo实现了附近商户用BitMap和HyperLogLog做了签到和UV统计。这个介绍的核心逻辑是功能 技术难点 解决方案 成果。每一句话都在给面试官递问题素材他有兴趣自然会往下追问。4.2 高频面试问答实录我在准备这个项目时梳理了大概二十多个面试官高频追问挑几个最典型的分享一下问你们这个项目并发量有多大这是最尴尬的问题因为我们自己练的项目并没有真实流量。我的建议是诚实说明项目是单机演练但设计上考虑了分布式扩展并进行过压测。你可以在本地用JMeter或wrk压一下秒杀接口记录下QPS和响应时间数据面试时拿真实数据说话。我当时压测的结果大概是一个单机RedisLua脚本的方案QPS能到2000多比纯DB方案高了一个数量级这个数据是可以讲的。问为什么用Redis存验证码不用Session从分布式部署、自动过期、数据可控性和可扩展性四个维度展开同时补充Session方案在集群模式下需要引入额外组件做同步。答案看2.1的内容就够了。问缓存空值解决了穿透那如果有人恶意用随机id刷呢这是追问。方案是加布隆过滤器做前置拦截把所有存在的商户id预加载到布隆过滤器里请求来了先用布隆过滤器判断id是否存在存在才继续查缓存。但布隆过滤器有误判率和删除问题所以我会搭配空值缓存做兜底以及加接口限流。问分布式锁超时了怎么办这是深水区问题。你如果用的是Redis的set nx ex锁超时释放会导致业务还没执行完锁被别人拿走了。更好的方案是Redisson的看门狗默认每10秒续期一次避免锁在业务执行期间提前过期。另外Redis分布式锁还有一个主从切换导致锁丢失的问题如果要更强的可靠性得用RedLock但RedLock本身也有争议。面到这一层说明面试官在压力测试你只要表现出“知道这个问题的存在且分析过不同方案的取舍”就算过关了。4.3 简历写法和项目复盘建议简历写法这块很多人的误区是把项目模块全部列上去导致篇幅又乱又长。我的建议是精选两到三个有技术亮点的模块每个模块的要点控制在三行以内。比如秒杀模块这么写负责优惠券秒杀功能的开发基于RedisLua脚本将库存判断、扣减、一人一单校验合并为原子操作解决并发超卖问题压测下QPS约2000引入Stream消息队列实现异步下单峰值流量削峰。这行突出了三件事场景、技术方案、量化结果。比“优化了秒杀功能性能”这种空话有说服力得多。另外强烈建议在准备项目时自己搭一个最小复现环境把核心流程完整走一遍。有没有亲手写过Lua脚本、有没有实际压测过、有没有排查过一个Redis连接池耗尽的问题面试时问两三轮就能分辨出来。纸上谈兵的项目经验在深挖两轮之后一定会露馅。5. 面经复盘我踩过的坑和最后的实操建议5.1 准备这个项目时最容易踩的坑第一个坑是只背概念不写代码。Redis的各类数据结构和命令看视频时觉得都懂面试官一问ZREVRANGEBYSCORE的语法和参数当场卡壳的人我见过太多了。我的建议是准备项目时把核心命令全部敲一遍至少把登录、缓存重建、秒杀脚本、Feed流这四个场景的代码亲手写到能默写的程度。第二个坑是简历写了但讲不深。很多人把Redis分布式锁写进简历面试官问“这个锁在异常情况下怎么释放的”答不上来。简历上出现的每一个技术名词都必须准备好一个至少能讲三分钟的对应故事否则宁可不写。第三个坑是不知道主动引导话题。面试是一个有来有回的对话你完全可以通过介绍项目的顺序来引导面试官问你准备好的内容。我当时的策略是不管被问到什么都会想办法把话题绕回“秒杀这个场景里我是怎么一步步优化设计”的因为这块我最熟、最有信心。这道题聊得越深被问其他薄弱环节的概率就越小。5.2 项目还能往哪些方向扩展黑马点评本身是一个很完整的练手项目但如果你想让它从“培训机构项目”变成“有个人思考的项目”我建议在这个基础上做三个方向的扩展第一个是接口幂等与防重。秒杀场景下用户快速点击两次或者前端重试会导致同一用户重复创建订单。用Redis的SETNX配合业务唯一流水号可以在入口就拦截重复请求比Lua里的sismember又多做了一层保护。第二个是热点key的探测与本地缓存。秒杀开始时有几个商品一定是热点所有请求都会打到Redis单key上。可以用hotkey探测工具或自己加一层Caffeine本地缓存热点请求在应用内存就处理一部分进一步降低Redis压力。第三个是全链路压测与监控。把秒杀接口跑在JMeter下面同时监控Redis的命中率、慢查询、连接池状态和GC情况。面试时如果你能说“我通过压测发现Redis连接池默认配置不够用调大了maxTotal之后QPS提升了30%”这就是比任何背模板都更真实、更打动面试官的素材。5.3 针对不同方向的个性化建议准备这个项目时我认为最核心的心态是不要为了“背住答案”而准备而是要把自己代入到“这个系统真实运行在线上”的场景里思考每个决策。如果你面试的是高级岗位还需要能说清Redis单线程模型下Lua脚本为什么不能写O(N)命令、Redis主从模式下分布式锁为什么可能失效、消息队列异步下单后订单状态怎么保证最终一致这些都需要结合项目细节去推演。根据我自己复盘面试的经验最后再分享一个小技巧面试前把你准备的所有项目知识画成一张纸的脑图从模块到技术点每个技术点标上“能讲几分钟、关联哪些面试题”然后照着脑图自问自答一遍。到了面试现场你会发现系统性地复述三到五遍之后这些内容已经是你的肌肉记忆了。黑马点评这个项目的价值不在于它本身多高深而在于它把后端面试里最常考的Redis、并发、缓存、消息队列问题全部串到了一个完整业务里好好吃透它你的项目面基本就稳了。