1. 先聊项目本身的定位:黑马点评到底练了什么
黑马点评这个项目,几乎可以说是Java后端学习路径上绕不开的一个名字。很多人第一次接触它,是冲着“仿大众点评”这个名头去的,但真正把它做完做完复盘的,反而能意识到一件事:它表面上是个业务项目,本质上是一堂非常系统的Redis实战课。
先给它一个准确的定义。黑马点评是一个前后端分离的移动端订餐/点评类项目,后端用Spring Boot + MyBatis-Plus + MySQL + Redis构建,前端是配套的移动端H5界面。项目的业务模块覆盖了短信验证码登录、商户查询、优惠券秒杀、点赞、关注、签到统计、附近商户、UV统计等日常消费场景。这些模块单看都不复杂,但放到一起,就构成了一套完整的“Redis在互联网业务中怎么用”的答案。
很多人在学Redis时都会有一个困惑:背了一堆命令,知道String、Hash、List、Set、ZSet、Geo、HyperLogLog这些数据类型,但真正写业务时,想不到该用哪个、为什么用。黑马点评解决的就是这个“能想到”的问题——它把每一个数据结构都放到了具体的业务场景里,让抽象的概念有了落地的抓手。
对于正在准备实习或者校招的人来说,这个项目的价值尤其明显。因为面试官不会问“Redis的String能存什么”,而是会问“你在项目里用Redis做了什么、为什么这么设计、遇到并发问题怎么解决”。黑马点评几乎每一个模块都对应着面试中的高频考点:缓存穿透、缓存击穿、缓存雪崩、超卖、分布式锁、消息队列、数据一致性问题。把这些点吃透,远比堆砌十个简单项目更有说服力。
也正因为如此,市面上关于黑马点评的教程、笔记、代码仓库非常多。但大部分人只是跟着视频敲了一遍,代码能跑,问到底层原理却说不出所以然。这正是这篇总结想解决的问题——不按视频目录重讲流程,而是站在复盘的视角,把项目的业务链路、技术选型、出现问题时的排查思路,以及面试官追问时该怎么答,一条一条掰开讲清楚。
这个项目的代码量不算大,但业务场景异常真实。你在学校写课程设计时,后台管理系统的增删改查占了八成代码,而黑马点评恰恰相反,它把重点放在了一线互联网业务才会遇到的并发、缓存、一致性问题上。换句话说,它练的不是“会写代码”,而是“会做设计”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 业务主链路拆解:从验证码登录到下单,每一步的Redis都在干嘛
2.1 验证码登录与Session共享问题
黑马点评的登录模块是一个经典的“短信验证码登录”流程:用户输入手机号,点击获取验证码,后端生成6位随机数字保存起来,用户填入验证码后核验通过,服务端保存登录状态。
这里就引出了第一个关键设计:验证码存哪里,登录状态存哪里。
很多初学者第一反应是存Session,因为Servlet规范里就是这么教的。但在分布式场景下,Session默认存在单台Tomcat的内存里,一旦服务被负载均衡转发到另一台机器,Session就丢了。这个问题的标准答案有几种:Session粘滞、Session复制、把Session抽离到Redis等中间件中。黑马点评用的是第三种,也是最符合生产实践的一种。
具体做法是把验证码存到Redis中,key为login:code:手机号,value为验证码,并设置过期时间。登录成功后,再以token为key,用户对象为value,存入Redis,把token返回给前端,之后每次请求都携带这个token,后端通过拦截器校验并刷新有效期。这个方案实际上就是把“服务端Session”换成了“定制化的Redis Session”。
这里有一个很容易被忽略的细节:为什么要用手机号做key的一部分,而不是直接用验证码做key?因为验证码是同一个值,在不同用户之间可能重复(6位数字随机,碰撞概率很高),而手机号是唯一的。把手机号编入key,能够天然避免不同用户之间的验证码覆盖问题。另外,验证码的过期时间设置为5分钟,是为了兼顾安全性和用户体验——太短用户来不及输,太长又容易被暴力破解。
登录模块虽然代码量不大,但把它和“为什么不用Session”结合来回答,面试基本就过关了。
2.2 商户查询与首页数据
登录进来之后,用户看到的是首页的商户列表和商户详情页。这个模块为后面的缓存方案提供了完整的业务背景。
商户信息在数据库里是tb_shop这种表,如果每个用户每次打开首页都直接查数据库,流量一大数据库就扛不住了。所以这里的标准做法是“先查Redis,没命中再查数据库,然后回填Redis”。
黑马点评的代码里封装了一个缓存工具类,这个工具类非常值得学习。它抽象出了几个方法:根据key查询并缓存、设置逻辑过期、删除缓存。把这些方法抽取出来之后,后面的秒杀、点赞等模块也能复用,而不是每写一个功能就复制一份缓存代码。
从教学项目的角度看,这个工具类其实也是“代码复用”思想的一个很好的范本。很多人写CRUD的时候,功能越加代码越乱,原因就是没有做这种抽取。黑马点评通过一个CacheClient类,把缓存读写、重建缓存、空值处理、逻辑过期全部收拢起来,体验一次之后,后面写业务时自然会考虑“我这块能不能也抽个公共方法”。
2.3 从浏览到下单:完整的业务闭环
用户浏览商户详情,看到优惠券信息,点击“抢购”,然后填单、下单、支付,这是一条完整的主链路。在秒杀场景下,这条路会比普通购买多出很多步骤:先判断是否有库存、是否已经下过单、是否在秒杀时间段内,然后扣减库存、创建订单。
黑马点评的秒杀模块是这个项目的高潮部分。最初版本使用的是JVM内存进行库存判断,只能支持单机;进阶版本则引入了Redis的分布式锁和Lua脚本进行原子操作。为什么会有这个演进过程,我在下文专门展开。
在整个主链路中,Redis承担了四个角色:缓存(商户信息)、计数器(库存)、分布式锁(并发控制)、消息队列(异步下单)。这些都是面试中非常典型的考察点。
现在回头再看黑马点评的设计,你会发现它并没有用什么高深框架,只是在每个环节选择了合理的Redis数据结构,配合几个核心设计模式,把一条真实业务链路串了起来。这对学习者的启示是:与其追求新框架,不如把基础设施吃透,然后想清楚在业务里该怎么用。
3. 商户缓存与数据库一致性:穿透、击穿、雪崩的落地方案
3.1 预设问题:缓存方案不是“加个Redis”就完事
缓存是提高读性能最直接的手段,但加了缓存就会引入新问题:缓存击穿、缓存穿透、缓存雪崩、缓存与数据库数据不一致。
黑马点评的商户详情缓存模块,正是围绕这三个经典问题展开的。我见过很多人把解决方案当作八股文背,但在项目里具体怎么用,一上场就倒下。这里我用黑马点评的实际代码逻辑,把三个问题逐个还原一遍。
先明确“击穿、穿透、雪崩”这三个词的区别:
- 穿透:查一个根本不存在的数据,Redis和数据库都没有,请求直接打到数据库。
- 击穿:某个热点key过期的一瞬间,大量请求同时打到数据库。
- 雪崩:大量key在同一时间过期,或者Redis宕机,导致海量请求打到数据库。
这三个问题虽然名字像,但应对方案完全不同。黑马点评分别用了“缓存空值”、“互斥锁/逻辑过期”、“随机过期时间”来处理。
3.2 缓存穿透:空值缓存与布隆过滤器
缓存穿透最常见的场景是恶意请求故意访问不存在的id,比如id为负数或者极大的数字。如果代码里只做“先查缓存,缓存没有就查数据库”,那这种请求会直接打穿数据库。
黑马点评的处理方案是:当数据库查询结果为空时,依然把这个空值写入Redis,并设置一个较短的过期时间(比如2分钟)。这样同一个不存在的id在短时间内再次请求时,命中缓存直接返回,不会再透传到数据库。
这个方案有一个前提,就是key的生成规则要一致。如果在不同接口中key命名混乱,缓存就形同虚设。黑马点评的key规则是cache:shop: + id,这个前缀规则中cache代表“缓存数据”,shop代表“商户模块”,id是具体主键。推而广之,你自己写项目时也应该定义一套统一的key规范,否则后面排查问题会非常痛苦。
空值缓存有一个小坑:如果恶意请求构造了大量随机不存在的id,那么Redis里会堆积大量空值key,占用内存。这时候可以考虑配合布隆过滤器,在缓存之前先用布隆过滤器判断id是否存在,不存在直接返回,不落Redis。黑马点评主代码中并没有实现布隆过滤器,但面试时可以提它作为一个扩展点。
3.3 缓存击穿:互斥锁方案和逻辑过期方案的取舍
缓存击穿是这三个问题里最考设计能力的。黑马点评给出了两种解法,对应两套代码实现,非常值得对比着看。
方案一:互斥锁。当缓存过期时,线程A在重建缓存之前先获取一把分布式锁,获取成功后才去查数据库并回填缓存,其他线程发现锁被占用,就短暂休眠后重试。这个方案保证了同一时刻只有一个线程在重建缓存,但代价是“缓存过期瞬间会有一小段阻塞等待”,牺牲了少量可用性,换来了强一致性。
方案二:逻辑过期。缓存永不设置物理过期时间,而是在value里存储一个逻辑过期时间戳。查询时如果发现逻辑过期,就获取锁并另起一个线程去重建缓存,当前线程先返回旧数据。这个方案读多写少的情况下体验最好,因为用户几乎感知不到延迟,但存在短暂的数据不一致窗口期,而且实现复杂度更高。
我记得当时看黑马点评代码时,印象最深的是互斥锁方案的锁粒度。很多人容易犯的错误是直接给“整个商户查询方法”加锁,这样会导致不同id的商户查询互相阻塞,并发能力大打折扣。正确的做法是锁的key要带商户id,比如lock:shop: + id,这样锁的粒度能精确到某个商户,它过期了只影响这一个商户的查询,其他商户完全不受影响。
这两个方案没有绝对的好坏。如果业务允许短暂的脏读,逻辑过期更优;如果业务要求强一致,互斥锁更稳。面试官问“你选哪个”时,最好的回答是结合业务场景来选,而不是直接说“我选互斥锁”。
3.4 缓存雪崩:过期时间加随机值
缓存雪崩通常是因为大量key设了同一个过期时间,在某个时间点集体失效。黑马点评的解决方案非常简单粗暴:设置过期时间时,在基础过期时间上再加上一个随机值,比如baseTime + Random.nextInt(300)。这样一来,原本会在同一秒过期的key,被分散到几分钟内逐步过期,数据库压力就被削峰了。
这个方案代码量只有一行,但背后的思想是“错峰”。同类思想还可以用在定时任务上,比如多个实例同时跑定时任务时,给每个实例加一个随机延迟,避免所有实例同时冲击数据库。
除了给过期时间加随机值,缓存雪崩还有另一个解法是“多级缓存”,比如用本地缓存(Caffeine)挡一层,Redis挡一层,数据库最后兜底。黑马点评项目本身没有引入本地缓存,但在高并发业务中,这也是常见方案。
3.5 缓存与数据库的一致性:先更新数据库还是先删缓存
这是面试中一个必问的问题。黑马点评的商户模块采用了“更新数据库后删除缓存”的策略。为什么不是更新缓存?因为直接更新缓存的成本更高:如果缓存数据是经过复杂计算的(比如商品详情包含多个表的数据),那每次更新数据库都要把计算结果同步到缓存,而实际业务中这个缓存可能根本不会被频繁读取,白白浪费时间。
“先更新数据库再删除缓存”有一个隐患:在极端情况下,线程A更新数据库,线程B读取缓存得到旧值,线程A再删除缓存,线程B把旧值写回缓存,最后导致缓存和数据库不一致。这个问题的根本原因是两个操作之间不是原子的。
削弱这个问题的常见套路是延迟双删:删除缓存后,等待几百毫秒再次删除一次;或者使用消息队列异步删除。实际开发中,很多业务能接受几秒钟的短暂不一致,所以“先更新数据库再删缓存”本身是工程上很常用的方案。面试时,要能把代价和风险讲出来。
黑马点评没有深入做“延迟双删”这类高级方案,但只要你能在复盘时把这些补上,项目深度就明显上了一个档次。
4. 秒杀“超卖”问题:从CAS乐观锁到分布式锁再到Lua脚本
4.1 最经典的超卖Bug是怎么产生的
秒杀模块是黑马点评里最有含金量的部分,它把并发问题完整地暴露在了你面前。
先还原一个真实的超卖场景。优惠券表里库存是100,但用户同时来了200个请求。如果把扣库存逻辑写成:
- 判断库存大于0
- 执行
update coupon set stock = stock - 1 where id = ...
在高并发下,两个线程可能同时通过了第1步的判断,都认为库存还有,然后分别执行扣减,最终库存变成负数。这就是典型的“不安全的读改写”流程。
黑马点评的第一版解决方案是乐观锁。具体做法是:在update语句的where条件中加上stock > 0,让数据库自己判断库存是否足够。MySQL的update是行级原子操作,当多个线程同时执行更新时,只有一个线程能真正修改成功,其他线程的更新会失败或影响行数为0,从而避免超卖。
4.2 用乐观锁还是悲观锁
乐观锁最简单,也是黑马点评初期版本的方案。但要注意,乐观锁并非银弹。在秒杀这种热点场景下,如果大量请求同时更新同一行,会有大量线程在数据库层被阻塞或重试,数据库压力会非常大。
悲观锁,也就是select ... for update,能保证同一时间只有一个线程更新库存,但会让其他线程排队等待,吞吐量下降。如果秒杀人数不多、数据库还能扛,悲观锁也可以接受;一旦并发量上万,数据库行锁竞争就会成为瓶颈。
互联网大厂更常用的做法是“把库存预扣到Redis里”:先将库存加载到Redis,使用DECR命令原子扣减,库存扣完就返回失败。因为Redis是单线程模型,所有命令的执行都是串行的,所以DECR天然不会超卖。然后,再通过消息队列异步处理真正的下单和数据库写入。
4.3 分布式锁的正确姿势
提到Redis扣库存,很多人第一反应是“用分布式锁”。
分布式锁本质上是让多个进程抢占同一个Redis key,抢到了就执行临界区代码,执行完释放。黑马点评的分布式锁演进有两条线:一条是手写SETNX锁,另一条是使用Redisson。
手写SETNX锁时,最容易踩的坑有两个:
第一个坑是忘记设置过期时间。如果线程获取锁后,执行过程发生异常没有释放锁,锁就会永远存在,其他线程全部卡死。所以必须在获取锁时用SET key value NX EX seconds同时设置过期时间,保证锁一定能在超时后自动释放。
第二个坑是误删别人持有的锁。线程A持锁执行中,锁因为超时自动释放了,线程B拿到锁开始执行,此时线程A执行完,直接调用DEL释放锁,结果把线程B的锁释放了。正确做法是:删除锁之前,先比较value是否是自己写入的唯一标识(比如UUID),是才删。这个过程必须用Lua脚本保证原子性,否则“判断+删除”两步之间依然会被插入其他操作。
Redisson解决了上面这些复杂问题。它提供了可重入锁、自动续期机制,通过看门狗线程保证锁在业务执行期间不会提前过期。在实际项目中,直接用Redisson比手写锁靠谱得多,因为99%的手写锁都会有边界条件问题。
黑马点评在进阶版本中引入了Redisson,但它在教学时也保留了一套手写SETNX锁的代码。复盘的顺序建议是:先看手写锁,理解锁的本质,再看Redisson,理解工程化的细节,这样面试时能讲得更深入。
4.4 Lua脚本:把“多个Redis命令”打包成原子操作
有了分布式锁之后,秒杀流程就变成:先抢锁,再查询库存,再扣库存,再创建订单,再释放锁。但抢锁和业务操作是两段代码,如果抢锁成功后的业务逻辑里出现异常,锁还没释放,就回到了死锁问题。
更优雅的方案是使用Lua脚本,把“判断库存是否充足 + 扣减库存 + 创建订单”这几步打包成一个脚本,交给Redis以原子方式执行。因为Redis执行Lua脚本时不会插入其他命令,所以这个脚本里的所有操作要么全部成功,要么全部不执行。
黑马点评实际项目中用到了一个很关键的优化:不直接在Java代码里反复调用Redis,而是把逻辑写成一段Lua,由redis.call()执行。这里涉及的命令包括SISMEMBER判断用户是否已买过、GET获取库存、DECR扣减库存,等等。一个Lua脚本执行完,返回一个状态码,Java层根据状态码做后续处理。
为什么能用Lua替代分布式锁?因为Lua脚本的原子性天然避免了并发竞争,不需要显式加锁。这比“锁+业务”的组合更简单、更快,也是当前互联网技术栈中抗秒杀流量的主流方案。
面试时如果能把这条演进链路讲清楚——从有超卖风险的原始逻辑,到乐观锁,到分布式锁,再到Lua脚本——就已经秒杀了95%只会背“什么是Redis”的候选人。
5. 点赞、关注、签到、附近商户:Redis数据结构的实战对照
5.1 点赞功能为什么用Set,而不是用数据库count
用户可以对商户点赞,也可以取消点赞。一个用户只能点赞一次,重复点赞应该取消。这个业务如果做成数据库表,就是一张tb_like表,每次点赞往表里插一条记录,取消点赞删除记录。在并发不高的时候,这样没问题,但一旦点赞量很大,数据库会扛不住。
黑马点评的优化是“把点赞数据放进Redis,不直接操作数据库”。用到的数据结构是Set。Set的特点是无序且成员唯一,天然适合做“用户是否点赞过”的判断。对某个商户点赞,就是SADD like:shop:商户id 用户id;取消点赞,就是SREM;判断用户是否点赞,就是SISMEMBER member;查询点赞总数,就是SCARD。
这个思路的本质是“读写分离”:读点赞状态、点赞数直接走Redis,数据库只保留最终的数据快照,或者干脆异步入库。
5.2 点赞排行榜为什么用Sorted Set
如果需求从“只看点赞数”升级成“按点赞时间展示最新点赞用户”,Set就不够用了,因为Set没有顺序概念,无法按时间倒序。
解决办法是改用Sorted Set(ZSet),把score设置成点赞时间戳。每次点赞执行ZADD like:shop:商户id 时间戳 用户id,展示最新点赞时执行ZREVRANGE,就能按时间倒序拿到用户列表。这个改动量很小,但把数据结构的特性卡得很准。
黑马点评在这个模块里还用到了ZSCORE、ZRANGEBYSCORE等命令。面试时被问“ZSet底层是什么结构”时,能答出“跳表”以及“跳表查询的时间复杂度是O(logN)”会是一个亮点。
5.3 关注与共同关注:Set的相交运算
关注功能的本质也是Set。每个用户维护一个follow:用户id的Set,里面存的是该用户关注的博主的id。那么两个用户的共同关注,就是两个Set的SINTER交集运算。这个用法很直观,但看到黑马点评用它来实现“共同关注”时,很多人会有一个顿悟感:原来一个集合运算就能解决一个业务查询。
这里还能延伸一个思路:如果还要记录“我关注的人是否也关注了我”,也就是粉丝关系中常见的“回关”判断,同样可以用SISMEMBER判断对方Set里是否包含自己。一条命令搞定。
5.4 附近商户:GEO与GeoHash
基于位置的服务,比如“查找附近3公里内的商户”,在传统SQL里需要计算每个商户与用户之间的距离,再进行排序。商户量大之后,SQL的性能会非常差。
Redis的GEO类型,底层依赖Sorted Set + GeoHash编码,可以把经纬度转换成一维字符串作为score,支持按距离范围查询商户。黑马点评的实现思路是:将商户的经纬度写入GEO中,查询时使用GEOSEARCH或GEORADIUS,传入用户的经纬度和范围,就能直接返回附近的商户。
这个模块的实际操作很直观,但在写项目时要注意两点:一是GEO的key要按城市或区域拆分,因为一个key里塞全国所有商户,数据量和查询效率都会有问题;二是GEO的坐标系统精度有限,如果需要极高精度还是需要引入专业的地理位置中间件。
5.5 签到与UV统计:BitMap和HyperLogLog
签到功能的传统设计是建一张签到表,用户每天一行。一年下来,一个用户就有365行数据,用户量一大,表会非常大。
黑马点评用了BitMap来优化。BitMap可以理解成二进制位数组,每个位只有0和1两种状态。把一年365天编码成365个bit,用户签到的天数,就是把对应的bit置为1。一个月全勤与否,可以通过对连续bit做BITFIELD或GET运算判断,而不需要查数据库。
BitMap的一个月签到统计在面试里非常常见。它能在极小的内存占用下完成海量签到数据的存储和统计,是一个能体现“你懂数据结构”的亮点。
UV统计对应的是HyperLogLog。HyperLogLog的核心价值是:只占约12KB内存,就能统计上亿级别的去重用户数,误差约0.81%。这个误差率对“统计今日访问人数”这种需求完全够用,因为产品经理通常关心的是量级而不是精确值。
黑马点评用HyperLogLog统计页面UV的逻辑是:每次用户访问页面时执行PFADD uv:page:日期 userId,查询时执行PFCOUNT拿到去重用户数。相比直接在数据库里COUNT(DISTINCT user_id),性能和开销完全是两个量级。
6. 部署、调试经验:黑马点评常见问题和排查思路
6.1 环境问题:Redis版本不一致引发的血案
黑马点评的视频和资料发布过多个版本,不同版本依赖的Redis版本也不同。早期教程里如果用了GET、SETEX这类老命令还好,但到了附近商户模块,如果Redis版本低于6.2,GEOSEARCH命令可能无法使用,只能用老版的GEORADIUS替代。
我当时在本地跑项目时,就遇到过Redis 5.x下GEO查询报错的问题。排查链路是这样的:先看异常日志,发现是Redis命令解析错误,再用redis-cli手动执行同一条命令,确认是命令不支持,最后查资料发现GEOSEARCH是在Redis 6.2才正式引入的。解决方式是升级Redis,或修改代码兼容旧命令。
这类问题的排查思路,本身就是一种能力。遇到Redis命令异常时,不要只盯代码,先用redis-cli复现命令,能少走很多弯路。
6.2 JSON序列化带来的反序列化异常
黑马点评中用户信息、商户信息等对象都要在Redis中存储。如果直接用JDK序列化,会出现一串ser之类的二进制内容,不方便排查。项目里通常会改用JSON序列化器。
但改成JSON序列化器后,有一个经典问题:反序列化时,如果对象的泛型信息丢失,Redis拿回来的数据可能被还原成LinkedHashMap而不是目标类型。比如你存的时候是User对象,取出来强转成User类型,就报ClassCastException。
解决方式是使用带类型信息的序列化方案,比如Jackson时指定TypeReference,或者在Spring Data Redis的RedisTemplate中配置GenericJackson2JsonRedisSerializer,让它把类型信息也写入JSON中。
这个坑对新手非常不友好,因为它报错的信息往往很长,且指向的不是你业务代码里写错的那一行。排查时可以用Redis Desktop Manager先查看一下Redis里实际存储的value长什么样,常常一眼就能发现问题。
6.3 分布式锁没释放导致的死锁
如果你自己实现过SETNX锁,那么大概率遇到过一个现象:发一次请求,后面所有请求全部超时。原因就是上一个请求获取锁后发生了异常,没有走到释放锁的代码。
排查这个问题时,第一步看Redis里的锁key是否还存在;第二步看业务请求日志,找到最后一个成功的请求,看它停在哪一行;第三步看那一段代码里有没有可能抛异常、有没有finally块。
修复方案很简单,把锁释放写到finally里就永远不会漏。但别忘了释放前要校验value是否是自己线程写入的那一个。这套逻辑写对一次,后面所有需要并发控制的代码都能复用。
6.4 秒杀压测数据不一致
用JMeter并发测试秒杀时,可能发现数据库里的订单数超过了实际成功的响应数。这个现象很容易让人误以为“下单逻辑有bug”。其实大概率是异步流程导致的:秒杀接口返回成功后,订单通过消息队列异步创建,响应成功并不代表订单已经落库,两者之间存在时间差。
验证方法是:等几秒再查数据库,或者看消息队列的消费日志。如果消息队列积压了,订单就会延迟出现。这不算bug,而是异步设计的正常现象,但如果在简历里把这个流程写清楚,面试官会认为你考虑了数据链路的一致性。
6.5 前端联调时的跨域与拦截器问题
黑马点评是前后端分离的项目,本地联调时前端请求后端接口,会遇到跨域问题,配置CorsFilter之后基本能解决。
还有一个容易被忽视的坑是拦截器。黑马点评的登录拦截器会拦截所有需要登录的接口,如果前端请求没有在header中携带token,返回结果是“未登录”。有时候前端明明登录了,接口还是报未登录,很可能是token的key名不一致。前端传的是authorization,后端取的是token,差一个字符就导致登录态失效。这种问题查起来很费时间,建议前后端统一约定好header字段名。
7. 面试与简历视角:项目亮点、高频提问与后续扩展
7.1 简历上怎么描述黑马点评
很多人的简历上写的是“实现了验证码登录、商户查询、优惠券秒杀等功能”。这句话太单薄了,面试官完全无法判断你做了多深。
一个更好的写法是突出“问题与解法”。比如:
- 使用Redis缓存商户信息,设计缓存空值与逻辑过期策略,解决缓存穿透、击穿问题。
- 使用Redis分布式锁与Lua脚本实现优惠券秒杀,将库存判断和扣减操作原子化,解决了超卖问题,并支撑单机压测不低于XXX QPS。
- 使用Set与Sorted Set实现点赞和点赞排行榜,避免数据库读写压力。
- 使用Geo、BitMap、HyperLogLog分别实现附近商户搜索、用户签到统计、UV统计。
要注意,简历上写的每一个点,都要经得起追问。你写了QPS,就要说清楚压测环境、并发线程数、链路瓶颈在哪里;你写了解决超卖,就要把从加锁到Lua的演进过程讲清楚。
7.2 面试官高频提问Top 10
- 你们项目的缓存更新策略是什么?为什么先更新数据库再删缓存?
- 缓存穿透和缓存击穿的区别是什么?分别怎么解决?
- 分布式锁的key和value怎么设计?为什么要设置过期时间?
- Redisson的看门狗机制了解吗?
- Lua脚本为什么能保证原子性?
- Redis的过期删除策略是什么?内存淘汰策略有哪些?
- Set和ZSet的底层结构分别是什么?
- HyperLogLog的误差原理是什么?
- 消息队列引入后,订单和库存的一致性问题怎么解决?
- 如果Redis挂了怎么办?怎么保证可用性?
这里想提醒一点:不要背答案。面试官听完你背的答案,会换一个场景再问,比如“如果这个商户的访问量突然高了一百倍,你原来的方案哪里最先扛不住”。这些问题才是真正拉开差距的地方。
7.3 项目后续还可以怎么扩展
黑马点评本身是一个教学项目,如果你只做到课程内的功能,项目深度是有限的。要想在简历上更有竞争力,可以考虑以下几个扩展方向:
一是引入消息队列。秒杀成功后不直接创建订单,而是发送一条消息到RabbitMQ或RocketMQ,由消费者异步落库。这样能把数据库压力削峰,秒杀接口的响应时间也能显著降低。
二是引入读写分离。当业务量增长到一定程度,单库单表扛不住,可以在MySQL层面做主从复制,Redis缓存未命中时查询从库,写操作走主库。
三是引入本地缓存。在查询商户详情时,先用Caffeine查本地内存,再查Redis,最后查数据库。多级缓存能把Redis的请求压力进一步降低,但需要处理本地缓存一致性问题。
这些扩展不需要全部实现,但写进文档或者在面试中作为“后续规划”提出来,会让人觉得你不只掌握了现有代码,还有全局架构意识。
7.4 复盘清单
最后分享一个我个人的复盘习惯。每次做完一个项目,不要急着开新项目,先花时间回答这三个问题:
- 这个项目解决了什么真实的业务问题?
- 核心设计里有哪些“为什么”是我现在能解释清楚的?
- 如果流量增加一百倍,系统哪里会先崩,我会怎么改?
能把这三个问题写下来,项目才算真正吸收成自己的东西。黑马点评最大的价值,恰恰在于它提供了一个足够丰富的业务场景,逼着你去回答这些“为什么”。这份总结,也是我在反复回答这些问题的过程中沉淀下来的,希望对正在刷这个项目的人有帮助。
