Redis替代Session:短信登录模块的实战解析与避坑指南 📅 发布时间:2026/9/11 8:07:33 👁 浏览次数: 1. 为什么短信登录是黑马点评第一个要啃的硬骨头黑马点评这个项目但凡认真刷过的同学应该都有体会前面几集还在讲Redis基础突然就上手一个短信验证码登录模块。很多人在这卡住不是因为代码量大而是因为这个模块的每一步都是“看着简单做起来处处是坑”而且它把Redis的核心价值体现得最直接——换掉Session那一套用Redis做共享登录态后面做缓存、做分布式锁、做秒杀时才不会一脸懵。先把这个业务说透。短信登录本质上解决三个问题验证码怎么发、验证码怎么存、登录状态怎么保持。大部分初学者能搞定第一个第二个就有人拿Session硬扛第三个更是一堆人做了个假拦截器就交差。但面试官和项目评审盯着看的恰恰是后两个。我用一句话概括这个模块的定位它是整个黑马点评项目的“地基”你在这里对Redis数据结构、过期策略、拦截器机制的理解深度直接决定后面评论点赞、关注推送、优惠券秒杀这些模块你能写多深。所以这篇文章我按自己的复盘思路来拆不是把代码贴一遍就算完而是把每个设计点背后的取舍、踩过的坑、面试常问的细节都捋清楚。2. 从Session到Redis短信验证码的存储方案为什么必须换2.1 Session方案的问题到底出在哪先看常规写法很多人一开始写的短信登录长这样PostMapping(/code) public Result sendCode(RequestParam(phone) String phone, HttpSession session) { String code RandomUtil.randomNumbers(6); session.setAttribute(code, code); // 调用短信服务商发送验证码 return Result.ok(); }这代码单机跑一点毛病没有但做项目如果只是“能跑”那后面全是窟窿。Session本身存服务端内存默认30分钟过期Tomcat默认配置看起来也没问题。但你把应用部署到多台服务器上试试用户在A机器登录验证码存进了A机器的Session下次请求被负载均衡转发到B机器B机器没有这个Session验证码校验直接失败用户一脸懵。解决方式无非两种Session粘滞同一个用户的请求固定打到同一台机器或者Session共享用Spring Session把Session存到Redis但这两个都算“打补丁”。更麻烦的是Session还有一堆隐性问题内存占用随用户量线性增长、服务重启Session丢失、跨端浏览器和App无法共享登录态。所以项目里用Redis替代Session不是炫技是真的比Session适合这个场景。2.2 Redis方案的Key设计和数据结构选型换Redis之后核心代码就变成这样PostMapping(/code) public Result sendCode(RequestParam(phone) String phone) { String code RandomUtil.randomNumbers(6); stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY phone, code, 5, TimeUnit.MINUTES); // 调用短信服务商发送验证码 return Result.ok(); }这里有两个细节值得深入说。第一个是Key的设计。我见过不少同学直接把phone当Key这么做的问题是如果哪天业务扩展同一个手机号既可能用来收验证码登录又可能用来收验证码注册、改密码那这几个场景的验证码就会互相覆盖。所以规范的写法是加业务前缀比如登录验证码的Key就是LOGIN_CODE_KEY phone在常量类里定义成login:code:最终Redis里的Key长这样login:code:13800001234。这样同一个手机号在不同业务场景下的验证码互不干扰排查问题也方便——你打开Redis可视化工具一眼就能分辨这个Key是干嘛用的。第二个是过期时间。5分钟是行业里比较通用的验证码有效期短了用户来不及输入尤其是不小心关掉了短信App再切回来可能已经过期长了增加验证码被暴力破解的风险。有些同学为了省事直接不设过期时间这是大忌Key会永久留在Redis里随着用户量增长Redis内存迟早爆掉。那验证码为什么不存成Hash而是直接用String因为验证码场景本质就是“一个Key对应一个Value”没有结构化需求String是Redis里最轻量、读写性能最高的数据结构。新手容易走进一个误区总觉得要用复杂的数据结构才能体现水平。其实Redis编程的第一准则恰恰是用最合适的数据结构而不是最复杂的数据结构。2.3 校验验证码时的原子性问题登录接口的实现一般长这样PostMapping(/login) public Result login(RequestBody LoginFormDTO loginForm) { String phone loginForm.getPhone(); String code loginForm.getCode(); String cacheCode stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY phone); if (cacheCode null || !cacheCode.equals(code)) { return Result.fail(验证码错误); } // 验证通过删除验证码 stringRedisTemplate.delete(LOGIN_CODE_KEY phone); // 创建用户、生成token...省略 }这里有个隐藏的并发问题用户拿到验证码之后在极短时间内连续点了两次登录按钮两个请求同时进入校验逻辑。如果校验和删除不是原子的可能出现两个请求都校验通过、都创建了用户、都返回了token的情况。虽然实际影响不大后面token是一样的登录态但面试官抓住这个点问“怎么优化”时你得能说出思路用Lua脚本包住“比较并删除”两步操作或者在校验通过后立刻删除验证码Redis单线程GET和DEL之间其实已经天然有序只是这两步之间隔着Java代码并发时会有空隙。这个问题属于“什么时候值得做什么时候不值得做”的一类。真实项目里我更倾向于登录成功后删除验证码是必须的防止同一个验证码被反复使用至于校验和删除之间那一瞬间的并发窗口用Lua脚本解决也不算复杂细节写在后面章节。3. 登录成功后的Token机制为什么不用Cookie直通3.1 Token的生成与存储方案对比验证码校验通过后项目里要把用户信息保存下来生成一个Token返回给前端。黑马点评里用的方案是随机生成一个UUID作为Token把用户信息转成JSON存进RedisKey是login:token: token同时设置30分钟的过期时间。String token UUID.randomUUID().toString(true); UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class); String json JSONUtil.toJsonStr(userDTO); stringRedisTemplate.opsForValue().set(LOGIN_TOKEN_KEY token, json, 30, TimeUnit.MINUTES); return Result.ok(token);有人会问这不就是在Redis里模拟Session吗对本质上就是“自研Session”但区别在于跨端共享Session绑定服务端内存和CookieApp端根本拿不到CookieToken是纯字符串App端存本地请求时放Header里即可。天然支持分布式Token对应的用户状态存在Redis这个公共存储里任何一台服务器都能校验。可控性更强Session的过期管理由容器说了算你可以给Token定制任意过期策略后面还会说怎么实现“登录后长时间无操作才过期”这种需求。关键点是Token本身的随机性。UUID能直接用但有一点要注意UUID生成的字符串是带横线的有些前端团队不喜欢而且UUID是一种基于时间戳MAC地址的生成方式理论上有被预测的风险对登录态这种敏感场景不算最优。换成IdUtil.fastSimpleUUID()Hutool提供去掉横线的简化版UUID或者用Redis自增随机盐拼一个也行。实战里我用Hutool的IdUtil多一点够用且没额外依赖。3.2 用户信息为什么用UserDTO而不是完整User对象这个是黑马点评里一个特别细节、但面试时会被追问的设计。项目里存进Redis的不是整个User对象而是只保留id、nickName、icon三个字段的UserDTO。原因有两点第一User对象里有密码字段密文存储但能少暴露就少暴露有手机号、邮箱这些个人敏感信息。这些信息一旦Redis被脱库或者被人拿到就是严重的用户隐私泄露。即使Redis设置了密码、做了内网隔离工程上的原则也是最小化存储敏感数据。第二UserDTO体积小一个完整的User可能有几十个字段序列化成JSON字符串后动辄几百字节甚至几KB每个在线用户存一份百万在线用户就是几百MB甚至几GB的Redis内存占用。只有三个字段的UserDTO顶多一两百字节量级差距非常明显。3.3 微服务场景下的演进方向黑马点评是单体项目所以Token直接存在同一个Redis里就行。但如果这套登录逻辑要迁移到微服务架构核心思路依然不变独立一个认证服务负责发Token其他业务服务只负责校验Token用户状态统一存在这个Redis集群。区别只是从“拿Redis当Session存”变成了“标准的Token-based认证”这就是这个模块为后续架构演进留下的空间你可以把这个写进简历的“项目扩展性”里。4. 拦截器与刷新机制登录拦截的真正难点在后面4.1 第一版拦截器的缺陷很多同学跟着视频敲到这里就会觉得登录校验不就是在拦截器里判断有没有Token吗但黑马点评这层的设计比看上去复杂因为要同时解决两个问题没登录的请求直接拦截和登录用户的登录态自动续期。先看基础版拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(user); if (user null) { response.setStatus(401); return false; } return true; } }这版拦截器的问题特别明显只要用户操作一次Token就永远在30分钟内有效吗不对Token过期时间是从生成那一刻算起30分钟一到就失效不管你中间操作了多少次。这就出现一个很差的用户体验用户刷了15分钟点评切出去回了个消息再回到App点刷新Token过期了被踢回登录页。要解决这个问题就得实现“每次访问都自动续期”的效果。这就牵扯出黑马点评里那个经典的二级拦截器结构。4.2 二级拦截器结构刷新拦截器 登录拦截器项目里的做法是注册两个拦截器。第一个叫RefreshTokenInterceptor它的职责只有两件事如果用户带了Token且Redis里有对应数据就刷新这个Token的过期时间把它重新设为30分钟并把用户信息存到ThreadLocal里如果用户没带Token或者Token已经失效直接放行不做拦截。第二个才是LoginInterceptor它的职责是判断ThreadLocal里有没有用户信息没有就返回401。两个拦截器执行顺序是Refresh先执行Login后执行。这样设计的好处是请求链路中任何需要登录的接口在LoginInterceptor这一层就会拦住非登录用户而已经登录的用户每次请求都会顺带续期一次Token永远不会自然过期。// 核心逻辑每次请求都刷新过期时间 public class RefreshTokenInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(authorization); if (StrUtil.isNotBlank(token)) { String key RedisConstants.LOGIN_TOKEN_KEY token; MapObject, Object userMap stringRedisTemplate.opsForHash().entries(key); if (!userMap.isEmpty()) { // 续期 stringRedisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); // 存进ThreadLocal UserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false)); } } return true; } }注意一个细节这个版本用的是Hash结构存用户信息userMap而不是String这种写法在视频教程里有出现实际用的时候要注意取出来的是MapObject, Object转成MapString, String再填充DTO否则Hutool的fillBeanWithMap会碰见泛型转换问题。这是我当时折腾过一阵子的点写在这里帮你避坑。4.3 ThreadLocal保存用户信息的一个高频面试细节拦截器获取到用户后不能直接塞进request里传给Controller吗能但ThreadLocal是更干净的方案。为什么因为HttpServletRequest本质是Servlet容器在请求链路上传递的参数你要用它传值每个用到用户信息的地方都得把request对象传进去Controller层的方法签名被迫全带上HttpServletRequest参数看着非常啰嗦。ThreadLocal本质是线程上下文在同一个请求线程内任何地方Controller、Service、甚至别的方法里都能通过静态方法拿到用户。这里面试官特别爱问的点是ThreadLocal会有内存泄漏风险吗回答思路分两个层面原理层面ThreadLocalMap的Key是ThreadLocal对象本身弱引用Value是强引用。如果这个线程是Tomcat的线程池线程线程不销毁ThreadLocal和Value的引用链就一直在GC回收不了。所以用完之后必须remove()。工程层面黑马点评里UserHolder提供了remove()方法在拦截器的afterCompletion里调用这是标准写法但很多人敲代码时容易漏。我见过有人只写preHandle和postHandle偏偏把afterCompletion漏了线上跑着跑着就出现“这个用户的数据莫名出现在另一个用户登录后的页面上”——这就是典型的ThreadLocal内存泄漏或串数据问题。这个点你在简历上可以用一句话体现深度“登录状态使用ThreadLocal保存并在拦截器afterCompletion阶段remove防止线程池复用导致的数据串线。”4.4 拦截器配置里的路径坑路径配置大概是这个项目里最容易被忽视又最容易出问题的地方。如果你把/user/me、/shop/**这些接口全拦了那你自己的登录接口/user/code和/user/login都会被拦住等于自己把自己锁在门外。另外黑马点评里的/shop/**、/shop-type/**这些浏览类接口是不需要登录的但/follow/**、/blog/**这些涉及个人数据的接口需要登录。你配置拦截路径时得想清楚哪些接口是游客也能看的哪些必须登录。我见过有些同学图省事直接把拦截路径配成/**导致首页都没法访问又回来Debug半天。5. 验证码发送环节真实生产环境不会这么简单5.1 为什么模拟发送在真实项目中不够视频教程里发验证码用的是log.info(发送验证码成功{}, code)控制台打印就算发送成功。这针对学习项目没问题但你要能讲清楚真实项目里短信发送是怎么接的。真实的验证码链路长这样业务后端调用短信服务商阿里云短信、腾讯云短信、容联云等的HTTP接口服务商再通过运营商将短信下发到用户的手机。这里面牵扯几个工程问题防刷同一个手机号1分钟内不能重复发送一天内的发送次数也不能无限。不然被恶意脚本刷爆你的短信账单直接爆掉。模板审核短信内容不能随意写需要服务商审核通过后按模板ID发送。比如模板内容一般是“您的验证码为${code}5分钟内有效请勿泄露”。异步发送短信发送接口耗时可能几百毫秒到几秒不能卡在用户的登录请求里同步等要丢进消息队列异步发或者用线程池异步处理提高接口响应速度。防刷逻辑补充下发送前先检查Redis里有没有login:code:limit: phone这个Key有就拒绝发送没有就放行并设置60秒过期。短信验证码的Key本身也做了5分钟过期所以同一个手机号理论上每5分钟能收到一条新验证码——要控制这个频率就得有一层独立的限流Key。我实际做的时候这两个Key是同时存在的一个管发送频率一个管验证码有效性。5.2 验证码存储的安全诉求验证码存Redis里明晃晃地放着表面看没啥大问题但严谨一点的做法是存的是验证码的加盐哈希。这样即使Redis被脱库攻击者也拿不到明文验证码。注意这不是教学项目会深究的但你答出来就是在告诉面试官你考虑过安全问题。加盐哈希在Java里就是BCrypt或者DigestUtils.sha256Hex(code salt)实现成本很低一行代码的事但体现了设计思维的差距。5.3 短信服务商选型与对接注意点黑马点评对接的纬度上你不用真的买一个短信服务商的套餐学生项目也没必要花钱。但你需要知道这几件事面试时能说出来接通一个短信服务商通常需要申请AccessKey/AppKey、配置签名比如“黑马点评”这四个字就是签名、配置模板。HTTP接口的入参一般是手机号、模板Code、模板参数JSON格式的替换值。服务商的SDK一般提供了封装但你自己要设计好回调通知、发送失败重试这些机制。另外开发环境怎么模拟常见做法是把发送逻辑抽成接口本地用一个MockSmsSender实现生产环境切换成阿里云的AliyunSmsSender。面试时能提到“我用策略模式封装了短信发送接口方便替换厂商”这就是一个加分项。6. 完整登录流程串讲一次请求的前世今生把上面的细节串起来一个完整的短信登录流程长这样用户输入手机号点击“获取验证码”。后端接收请求先查防刷Key通过后生成6位随机数字写入Redislogin:code: phone过期时间5分钟再调用短信服务商或模拟发送。用户收到验证码输入到前端表单里提交登录。后端拿着手机号和验证码从Redis取出真实验证码比较不匹配返回错误匹配则删除这条验证码。根据手机号查数据库查到了就复用User记录查不到则创建新用户昵称默认就是手机号头像给一个默认图。生成随机Token把用户的关键信息id、昵称、头像转成JSON或Hash写入RedisKey是login:token: token过期30分钟把Token返回给前端。前端拿到Token之后存储在本地小程序是storageApp是SharedPreferences浏览器是localStorage之后所有需要登录的请求在Header里带上authorization: token。后端有两个拦截器按序处理刷新拦截器校验Token存在就续期并将用户写入ThreadLocal登录拦截器判断ThreadLocal里有没有用户没有就返回401有就放行。请求处理完后刷新拦截器在afterCompletion里调用UserHolder.remove()清空ThreadLocal。这里面数据库创建用户的细节值得多看一眼用save()还是先query()再save()黑马点评里是直接save()因为手机号通常加了唯一索引重复提交会导致数据库异常。但真实项目里更推荐的做法是先用手机号查一遍查不到再创建。为什么因为用户可能之前用这个手机号下过单只是因为短信验证码Key过期、用户重新走了一遍登录流程这时如果无脑插入就会产生幽灵用户甚至触发唯一索引异常。用IdUtil生成用户主键ID雪花ID也是值得写的点雪花ID是趋势递增的分布式ID相比数据库自增主键它不依赖数据库生成速度快分布式环境下不冲突。这个项目的用户量在单机数据库其实没到需要分布式ID的程度但你用了雪花ID说明你接触过分布式系统设计的思路这个在回答“为什么不用自增主键”时可以说清楚。7. 开发过程最容易“莫名其妙”挂掉的几个点7.1 验证码校验后Redis里的值取出来是空的排查思路查看有没有把验证码的Key和业务上其他Key写重复比如登录验证码和注册验证码共用了同一个Key导致覆盖。查看设置的过期时间单位TimeUnit写错成HOURS5小时过期用户说“我收完验证码去干了件事回来发现登录不上”这既是Bug也是体验问题。查看序列化器RedisTemplate默认的JdkSerializationRedisSerializer会把字符串序列化成一堆反人类前缀\xAC\xED\x00\x05t\x00...你GET的时候取到的值带前后缀和你存的纯数字字符串总对不上。解决方式是用StringRedisTemplate这个类的Key和Value都按String序列化是最适合这种场景的。7.2 拦截器设置了但登录接口还是被拦多半是路径匹配问题。SpringMVC的拦截路径和请求路径必须精确匹配如果你配置的排除路径是/user/login而Controller里映射的却是/user/login/多了个斜杠就会不匹配。另外静态资源的放行也要配好否则前端页面加载不出来用户以为登录坏了其实只是样式文件被拦截了。7.3 HttpServletRequest的Header里取不到Token常见原因前端代码里把Header名字取名为Authorization后端取的却是authorization。HTTP Header大小写不敏感理论上问题不大但如果你用了Nginx转发时有特殊配置大小写可能真会有影响。后端统一用小写前端也统一用小写最省事。前端没有在请求拦截器里注入Header。如果前端是uniapp小程序你自己写的uni.request封装里如果没有把Token拼进去后端永远拿不到。7.4 登录状态一刷新就丢检查刷新拦截器的续期逻辑有没有生效——最典型的错误是你直接让stringRedisTemplate.expire()在每次请求里都跑但忘了Key的正确前缀。比如生成Token时用的Key是login:token: token续期时却写成了login:token token少了冒号那你每次续期的都是另一个不存在的Key登录态当然一到30分钟就断。这种Bug藏在代码里特别隐蔽排查时把Redis里的Key列表打出来看一下就明白了。7.5 用户信息在Controller里取不到这个一般是ThreadLocal没传对。注意两点UserHolder必须是静态方法否则你每次要调用的地方都得先new一个拦截器的preHandle执行顺序是在Controller前但SpringMVC有一些特殊的处理器比如用ModelAttribute时可能提前初始化拦截器里的数据会产生未知顺序问题最直接的排错方法是在拦截器和Controller入口分别打一条日志看看执行顺序。8. 缓存淘汰与异常情况的兜底设计8.1 验证码的缓存淘汰策略Redis的过期策略是惰性删除定期删除组合。也就是说你给Key设了5分钟过期不意味着5分钟到了它就立刻消失可能是下次访问时发现过期了才删。所以代码里get之后判断null是标准做法不能指望“设了过期时间就不管了”。另外一个细微之处是Redis的过期时间精度是秒你设5, TimeUnit.MINUTES实际是300秒如果你在前面写的是login:code: phone、后面写的是LOGIN_CODE_KEY phone这种字符串拼接的时候一定要检查常量是否包含冒号少了分隔符Redis里会出现一批语义混淆的Key后期维护特别痛苦。8.2 短信服务商不响应、验证码发不出去怎么办生产环境里这个Case真的很常见。对接短信服务商时它的接口可能因为网络抖动、欠费、模板审核不通过等原因返回失败这时候你要做的不是把错误原样抛给用户而是做好失败降级返回一个用户可理解的提示比如“短信发送太频繁请稍后再试”同时在日志里记录错误详情方便排查。另外发送失败的手机号可以在Redis里记录一个失败次数连续失败超过N次就暂时禁止这个手机号发送防止短信服务商因为大量失败请求把你限流了。8.3 登录失败的错误码设计黑马点评给的返回结构是Result里面有个success字段和msg字段。但真实项目里建议对校验失败、Token过期、无权限这类错误定义统一的错误码。比如错误码含义200成功401登录状态失效需要重新登录403已登录但没有权限操作500服务器内部错误600验证码错误或过期前端拿到错误码之后可以分门别类跳转或弹窗。这个点跟短信登录的关系很大——你会发现自己在写登录模块时顺手就把错误码规范定好了之后的评论、点赞模块都能复用这套Result结构项目整体风格会统一很多。9. 黑马点评短信登录模块的面试问答弹药库面试官问这个项目的登录模块一般会从浅到深追问我把自己被问过的问题和参考答法整理出来你可以直接背。问题一为什么用Redis存验证码和登录态而不是Session分三点答分布式环境下Session共享困难需要额外引入Spring Session或摆弄负载均衡策略Session存在服务器内存里无法控制精确的过期和续期策略Token机制天然适合App端和跨端共享。再加上Redis本身支持过期时间存取性能高作为共享存储非常合适。问题二Token续期怎么实现的会有什么并发问题吗答用两级拦截器刷新拦截器在登录拦截器之前执行每次请求都会把Token对应Key的过期时间重置。高并发下多线程同时访问时续期操作本身是幂等的不会产生覆盖问题但如果需要精确控制可以把续期操作封装到Lua脚本里执行。问题三验证码在Redis里存的是明文安全吗先承认工程上的边界验证码有效期短、单次有效泄露风险可控。再讲改进方案存验证码的加盐哈希校验时对用户输入做同样的哈希再比较同时配合全链路HTTPS、Redis访问控制、内网部署等措施降低泄露概率。问题四用户信息在请求线程里怎么传递用ThreadLocal并在第二个拦截器的afterCompletion里调用remove清理。顺手再扩展一句ThreadLocal在异步场景下无法传递需要额外处理比如transmittable-thread-local但当前项目是同步处理请求所以没问题。这一句说出来会让面试官觉得你有真实线上经验因为很多人只会背ThreadLocal的原理。问题五用户表的主键为什么用雪花ID不用自增主键数据库自增主键在单库单表下挺好但一旦数据量上来要做分库分表自增主键就会冲突。雪花ID不依赖数据库在分布式环境下各节点自行生成ID也不会冲突并且趋势递增对数据库索引友好。另外雪花ID是纯数字比UUID长度短索引性能更好。10. 从登录模块延伸出来的几个进阶改造点我自己在做复习时又额外加了几个“增强包”。如果你时间充裕强烈建议加进项目里这些是拉开简历差距的内容。10.1 把验证码发送改成异步现在的入口方法是先写Redis再发短信如果短信接口耗时较长用户的登录请求就跟着被拖慢。改成线程池异步发短信接口只负责生成验证码和存Redis立刻返回“发送成功”。这样能明显提升接口响应速度也是真实项目的常规操作。10.2 做一层简单的接口限流针对短信发送接口可以做基于Redis滑动窗口或计数器的限流比如一个IP一分钟最多发10次一个手机号一天最多发20次。防止别人用你的短信通道刷量这是上线后最现实的攻击角度之一。黑马点评里没怎么展开但你知道怎么做就是加分项。10.3 用布隆过滤器或查询兜底做手机号注册判断用户如果不存在需要注册新账号。万一恶意请求连着一个不存在的手机号来注册每次都会触发一次数据库插入。要在接口层做一层兜底判断手机号格式是否合法、是否在黑名单里、是否真的需要一个新老用户逻辑分开处理。10.4 把登录模块抽成通用组件如果是真实项目登录逻辑不可能只给一个入口用可能还有“小程序登录”“管理后台登录”等变体。你可以把“验证码发送”和“校验手机号验证码”的公共逻辑抽到SmsLoginService里Controller层只是薄薄一层参数接收这种设计在维护多端登录时会非常清爽。黑马点评项目里Controller也基本只做参数接收这个意识你在复习时可以留意面试时能说出来就是经验。10.5 压测和日志追踪虽然教学项目不会要求你压测但如果你能在讲方案时顺手提一句“登录接口用压测工具测过TPS大概在xx瓶颈出现在验证码的Redis读写上优化Redis连接池后又提升了一些”面试官的印象会完全不一样。这个数字不用很大关键是体现了你有“性能意识”。最后说点实际的短信登录这个模块在视频教程里可能只是开篇几集但它把你后续在Redis里用到的大部分思路都过了一遍数据结构选型、过期策略、缓存续期、并发兜底、安全考虑。我刷第一遍的时候觉得这就是“存个验证码校验一下存个Token”这么简单刷到第二遍开始理解拦截器为什么要分两层等到自己动手从零写一遍才发现线程池异步、防刷限流、异常兜底、统一错误码这些细节全是坑每一个都能单独拿出来做一篇文章。如果你正准备面试建议别只抱着源码看找一天时间把这段代码全部手写一遍尤其是两个拦截器的执行顺序和ThreadLocal的清理时机写错一遍比看三遍印象都深。而如果你是还没开始做这个项目、想先了解它在学什么那我告诉你短信登录就是整个黑马点评的钥匙拿不到这把钥匙后面的Redis缓存、分布式锁、秒杀、点赞排行榜你都进不了门。