最近在整理黑马点评项目的技术笔记第一篇想从登录聊起。做点评类应用登录绝对不只是验一下手机号那么简单越往后做越能发现登录态是整条业务链的地基。最早我用Session存用户信息单机跑得很顺结果架构一改成多实例部署用户明明登录了刷新一下就被踢出去浏览器里的SessionId在另一台机器上完全对不上。后来把登录态迁移到Redis用Token作为客户端凭证服务端只认Redis里的Key整个链路才算彻底稳定下来。这篇分享的不是什么高深理论而是我在黑马点评项目里落地Redis登录时完整的设计思路、代码实现和踩坑记录。无论你是刚学Spring Boot还是准备把老项目的Session登录换成Redis方案内容都能给你一个可以直接照搬的参考。我会把为什么用Token、验证码怎么存、怎么让登录态自动续期、两个拦截器怎么配合这些细节全部拆开讲明白。1. 为什么登录要从Session换成Redis瓶颈与选型1.1 传统Session方案到底卡在哪Session方案的运作机制不复杂用户登录成功后服务端把用户信息放在HttpSession对象里再生成一个JSESSIONID通过Cookie写回浏览器。之后每次请求浏览器自动带上Cookie服务端根据JSESSIONID在内存里找到对应的Session数据就认为这个用户是登录状态。这对单机应用来说够用但项目一旦变成多实例部署麻烦立刻就来了。想象一下你有两台后端服务器前面挂着一个负载均衡。用户第一次请求被分到了A机器Session存在A的内存里第二次请求被分到了B机器B机器上根本没有这个Session服务端自然认为用户未登录于是把请求重定向到登录页。用户感觉就是莫名其妙被踢下线。当时第一反应是配置粘滞会话让同一个用户的请求都固定分到一台服务器但这方案在扩容、缩容、重启的时候都会出问题比如A机器宕机了原来粘在A上的用户全部需要重新登录。更隐蔽的问题是Session放在JVM内存里的成本。用户量上来之后大量未过期的Session堆积在堆内存里GC压力越来越大时不时触发FullGC导致接口抖动。再加上移动端App对Cookie的支持没有Web端那么友好前后端分离项目里为了跨域带Cookie还得配置withCredentials前端漏一个参数登录就失效排查起来非常烦躁。Session其实是一种把状态绑死在单台服务器内存的方案在分布式和前后端分离的背景下它的维护成本已经高过了它带来的简单性。1.2 Redis加Token模式带来的核心变化Redis登录的核心思想是把登录态从服务端内存挪到独立缓存服务里。客户端登录成功后服务端生成一个不透明的Token字符串通常直接用UUID客户端保存这个Token之后每次请求把它放在请求头里比如Authorization: xxxx服务端只需要拿Token去Redis里查对应的用户信息查到就是已登录查不到就是未登录。为什么用Redis而不是数据库第一是速度Redis基于内存读写是微秒级数据库查一次用户信息怎么也得几毫秒第二是过期时间Redis可以对Key设置TTL登录态天然有生命周期不需要人为写定时任务清理第三是共享性多个后端实例连接同一个Redis无论请求落到哪台机器拿到的数据都一样分布式会话问题直接消失。Token本身是一个随机字符串不包含任何用户信息所以不需要像JWT那样担心签名算法泄露或密钥管理问题。服务端想踢人直接把Redis里对应的Key删掉就行非常干净。另外Redis的过期策略配合活跃续期可以做到用户30分钟内只要有操作就一直保持登录超过30分钟没操作就自动过期安全性和体验平衡得比较舒服。1.3 什么场景下值得用这套方案Redis登录不一定适合所有项目。纯单体、单实例、服务端渲染的小项目Session依然是最简洁的选择没必要为了技术而技术。但如果你做的项目满足下面任何一个特征Redis方案就值得认真考虑前后端分离前端可能是Web、小程序、App多端共用接口后端需要水平扩展多实例部署是常态产品有强制下线踢人后台封禁这类需求或者单纯想统一管理登录态的过期时间。黑马点评这个项目正好踩中了这些点。它既要被Web端访问又要被移动端调用接口的登录校验如果靠Cookie跨域问题会很麻烦。换用Token之后客户端只需要在本地存一个字符串请求时塞进Header服务端用拦截器统一处理所有端的行为完全一致。整体设计思路拆成三块验证码登录负责身份确认Redis存储负责登录态保存双拦截器负责续期和权限校验。三块各管一件事出了问题也好定位。2. 登录链路的设计验证码、用户存储与Key规划2.1 验证码发送与校验的完整流程真实项目中短信验证码是通过短信服务商发送的但在本地开发和测试阶段我们不会真的去调短信接口而是把验证码打印到日志里方便自己联调。我在黑马点评项目里把发送验证码封装成一个独立接口生产环境可以无缝替换成真实短信服务商的实现开发环境只打日志代码结构不脏切换也不费劲。发送验证码的核心逻辑是先校验手机号格式再生成6位随机验证码存入Redis并设置5分钟过期最后在日志里输出。一个简化版的Controller代码是这样的PostMapping(/code) public Result sendCode(RequestParam(phone) String phone) { // 1. 校验手机号格式 if (RegexUtils.isPhoneInvalid(phone)) { return Result.fail(手机号格式错误); } // 2. 防刷同一手机号60秒内只能发送一次 if (!stringRedisTemplate.opsForValue() .setIfAbsent(login:sms: phone, 1, 60, TimeUnit.SECONDS)) { return Result.fail(发送太频繁请稍后再试); } // 3. 生成6位验证码 String code String.format(%06d, new Random().nextInt(1000000)); // 4. 存储到Rediskey为 login:code:手机号TTL为5分钟 stringRedisTemplate.opsForValue() .set(login:code: phone, code, 5, TimeUnit.MINUTES); // 5. 开发环境打印验证码生产环境换成短信服务商调用 log.debug(手机号 {} 的验证码是{}, phone, code); return Result.ok(); }这里有个细节同一手机号60秒内只能发一次我用的是setIfAbsent加过期时间。Redis的单线程命令执行保证了这个操作是原子的多个请求同时进来只有一个能成功写入Key并返回true其它请求全部被拒绝不需要额外加分布式锁。这个防刷逻辑在分布式环境下也能正常工作是Redis自带命令带来的好处。校验验证码时一定要把手机号做trim之后再拼Key验证码也要trim之后再比较。验证码是一次性的校验成功后要立刻把login:code:这个Key删掉否则同一个验证码可以被反复使用丢失安全性。存储和比较都通过StringRedisTemplate完成不要用RedisTemplate默认的JDK序列化方式否则Key和Value都会变成带序列化头的一堆乱码没法直观排查。2.2 登录成功后用户信息怎么存Redis登录接口内部的处理顺序是校验手机号格式校验验证码验证码通过后根据手机号查数据库用户表。如果用户不存在直接创建一个新用户这对应的是首次登录即注册的产品策略体验上最顺滑不需要让用户单独走一遍注册流程。用户信息查出来以后我不会直接把整个User对象塞进Redis而是封装成一个UserDTO只保留id、phone、nickName、icon这几个展示和标识用的字段。密码、角色、权限这类敏感信息不进Redis这是基本的安全习惯。很多人觉得既然是内部服务就无所谓但Redis一旦被误操作或者被其他服务共享敏感信息暴露出去就是事故。数据结构上我选择Redis的Hash而不是直接存JSON字符串。原因有几点一个用户信息对应一个Token KeyHash内部可以有多个Field比如id、phone、nickName想改昵称时只需要HSet tokenKey nickName 新值不需要把整个对象读出来再覆盖写读取时entries()返回Map转回Java对象也很方便。存储代码大致是String token UUID.randomUUID().toString(true); UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class); MapString, Object userMap BeanUtil.beanToMap(userDTO); // 过滤掉null字段避免Redis Hash写入空值 userMap.entrySet().removeIf(e - e.getValue() null); String tokenKey login:token: token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES); return Result.ok(token);关于BeanUtil.beanToMap我用的是Hutool工具类它会默认把值为null的字段也放进Map。Redis Hash的field对应value如果是nullStringRedisTemplate会抛异常所以必须过滤。这个坑我在第一次集成时踩过日志里报了一个很奇怪的错误查了半天才发现是某个实体的字段没赋值。以后凡是涉及Bean转Map再写Redis的操作我都会条件反射地检查null过滤。2.3 Key命名规范与过期策略的取舍Redis里的Key如果乱起名排查问题的时候会非常痛苦。黑马点评项目里我统一采用业务:功能:唯一标识的格式。验证码Key是login:code:手机号登录Token Key是login:token:UUID防刷Key是login:sms:手机号。冒号在Redis里不会影响性能只是作为字符串的一部分但在命令行、可视化工具里可以按前缀过滤非常方便。过期策略上验证码设置5分钟登录Token设置30分钟。这个时长不是拍脑袋定的验证码太短用户容易超时太长又会增大被暴力猜解的风险登录Token 30分钟看似短但是配合活跃续期之后用户只要一直在操作就永远不会掉线只有连续30分钟没有请求时才会被强制下线。这个逻辑对绝大多数场景都合适。另外需要区分一个概念Redis的过期时间是挂在Key上的不是挂在Key内部的某个Field上。我用的结构是一个Token对应一个Hash只要你对这个Token Key设置过TTL整个Hash都会在到期后自动消失不需要对每个Field单独设置。这一点经常有人搞混以为Hash内部的每条Field都要单独设置过期时间其实没有这个能力。所有登录相关的Key都必须显式设置TTL。如果Redis配置了allkeys-lru这类内存淘汰策略内存吃紧时那些没有设置TTL的Key会先被淘汰虽然能勉强兜底但淘汰掉登录态会让用户被无故踢下线。我的习惯是在每次写入Redis前问一句这个Key需要活多久没有答案的Key不要往Redis里塞。3. 双拦截器实现登录续期与权限校验3.1 为什么拆分两个拦截器很多人写登录校验都会用一个拦截器搞定在preHandle里判断用户是否登录没有登录就返回401登录了才放行。这个思路本身没问题但一旦遇到活跃续期和部分接口允许匿名访问的需求单拦截器就会写得很别扭。举个例子刷新验证码的接口、首页商户列表的接口本来就允许不登录访问。如果只有一个登录拦截器你拦截/**那这些匿名接口怎么办要么你手动把它们排除掉但排除之后这些请求就不经过任何拦截器了Token的续期逻辑也执行不了。如果一个匿名用户浏览首页浏览了半小时其实没有影响但如果一个已登录用户请求的是排除路径他的Token就不会被续期等他再访问需要登录的接口时可能Token正好过期又要被踢下线。所以我在项目中拆了两个拦截器。第一个叫RefreshTokenInterceptor拦截所有请求主要职责是只要请求头里带着Token且Token有效就刷新过期时间并把用户信息写入ThreadLocal如果没带Token或者Token无效也不拦截直接放行。第二个叫LoginInterceptor只拦截那些需要登录的接口在preHandle里从ThreadLocal取用户信息取不到就返回401。两个职责解耦代码清爽不会出现既要放行又要续期的纠缠。3.2 Token滑动刷新机制的前置实现Token固定30分钟过期如果不刷新用户在第29分钟发了一个请求第31分钟再操作就会被强制下线哪怕他刚刚还在活跃。所以要设计一个滑动过期时间每次用户发生请求时只要Token有效就把过期时间从当前时间重新延长30分钟。实现位置放在RefreshTokenInterceptor的preHandle里代码长这样Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 跨域预检请求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StrUtil.isBlank(token)) { return true; } String tokenKey login:token: token; MapObject, Object userMap stringRedisTemplate.opsForHash().entries(tokenKey); if (userMap.isEmpty()) { return true; } // 刷新过期时间实现活跃续期 stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES); // 用户信息写入ThreadLocal UserDTO userDTO BeanUtil.fillBeanWithMap( userMap, new UserDTO(), false); UserContext.saveUser(userDTO); return true; }这里我特意处理了OPTIONS请求。当前端用跨域请求带自定义Header时浏览器会先发送一个OPTIONS预检这个请求没有业务语义会被拦截器拦到如果不放行就会报CORS错误。这个细节在开发联调时非常常见我放在拦截器的最前面处理了。刷新过期时间为什么放在preHandle而不是afterCompletion因为preHandle是请求进入Controller之前的第一个环节执行越早越能保证续期动作在业务代码之前完成。如果放在afterCompletion遇到Controller执行时间特别长、超过剩余过期时间的情况等业务跑完Token已经过期了再执行expire就晚了。刷新的动作要在Token还没过期但马上要过期的时刻抢先完成这也是滑动续期最关键的点。3.3 ThreadLocal线程上下文的正确写法LoginInterceptor的实现非常简洁Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (UserContext.getUser() null) { response.setStatus(401); return false; } return true; }能看到这套设计的价值因为RefreshTokenInterceptor已经提前把用户信息查好并放进了ThreadLocalLoginInterceptor只需要判断当前线程里有没有用户即可。Controller里需要用户ID的接口直接调用UserContext.getUser().getId()不用再根据Token去Redis重新查一遍省掉了大量重复IO和序列化逻辑。ThreadLocal有个非常大的坑请求结束后必须清理。Spring Boot默认使用Tomcat的线程池线程会被复用如果上一次请求把用户信息塞进ThreadLocal没有及时移除下一次请求如果因为异常或者拦截器配置变更没有走到刷新区就可能读到上一个用户的登录信息造成数据串号。为了统一管理我封装了一个用户上下文工具类public class UserContext { private static final ThreadLocalUserDTO tl new ThreadLocal(); public static void saveUser(UserDTO user) { tl.set(user); } public static UserDTO getUser() { return tl.get(); } public static void removeUser() { tl.remove(); } }然后在拦截器的afterCompletion方法里调用UserContext.removeUser()。注意用remove()而不是set(null)因为set(null)不会真正清理ThreadLocal内部的Entry线程复用后仍然可能读到残留引用。这是我一个从老代码里带过来的坏习惯后来排查一个诡异的用户串号问题才发现根因在这里。4. 实操落地依赖、接口、拦截器配置4.1 环境准备与Redis依赖配置黑马点评项目使用Spring Boot引入Redis主要需要两个依赖Spring Data Redis和连接池实现。在pom.xml中dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency第二个依赖不是必选的但Spring Boot的Redis默认使用Lettuce连接池没有commons-pool2的话连接池相关配置不会生效。本地开发时我喜欢用Docker直接起一个干净Redisdocker pull redis:7 docker run -d --name redis-local -p 6379:6379 redis:7不需要挂载数据卷因为登录态是缓存Redis重启之后全部丢失也符合设计预期。application.yml中需要配置连接参数spring: redis: host: localhost port: 6379 password: database: 0 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0这里特别提醒一点业务代码里统一使用StringRedisTemplate不要直接用RedisTemplate。因为默认的RedisTemplate会使用JDK序列化器存字符串时会在前面加上一堆序列化头Redis可视化工具里看到的全是\xAC\xED\x00\x05t...这种乱码。StringRedisTemplate的Key和Value都是纯字符串看起来直观排查问题也方便。4.2 验证码接口的完整实现验证码接口刚才已经给出了核心片段这里补全Service层的完整逻辑。发送验证码时我会把手机号格式校验、防刷、缓存写入、日志打印都放在Service方法里Controller只负责参数接收和结果返回。下面是简化版实现public Result sendCode(String phone) { // 1. 手机号非空、格式校验 if (StrUtil.isBlank(phone) || !phone.matches(^1\\d{10}$)) { return Result.fail(手机号格式错误); } // 2. 同一手机号60秒内不允许重复发送 String sendKey login:sms: phone; Boolean isFirst stringRedisTemplate.opsForValue() .setIfAbsent(sendKey, 1, 60, TimeUnit.SECONDS); if (Boolean.FALSE.equals(isFirst)) { return Result.fail(发送太频繁请稍后再试); } // 3. 生成6位随机验证码 String code String.format(%06d, new Random().nextInt(1000000)); // 4. 验证码存入Redis5分钟过期 String codeKey login:code: phone; stringRedisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES); // 5. 开发环境打印验证码 log.debug(验证码{}, code); return Result.ok(); }注意一个容易踩坑的地方new Random().nextInt(1000000)可能生成不足6位数的值但String.format(%06d, ...)会自动补前导零所以最终一定是6位字符串。验证码比对时前端传过来的是字符串类型Redis取出来的也是字符串类型两边都是6位字符串相等判断没问题。如果前端做了一层去掉前导零的处理那就对不上了所以明确约定验证码始终按字符串处理。4.3 登录接口与Token生成的完整实现登录接口是整个流程的核心我把完整Service方法写出来public Result login(String phone, String code) { // 1. 校验手机号 if (StrUtil.isBlank(phone) || !phone.matches(^1\\d{10}$)) { return Result.fail(手机号格式错误); } // 2. 校验验证码 String codeKey login:code: phone; String cacheCode stringRedisTemplate.opsForValue().get(codeKey); if (StrUtil.isBlank(cacheCode) || !cacheCode.equals(code.trim())) { return Result.fail(验证码错误或已过期); } // 3. 验证码一次性校验通过后立即删除 stringRedisTemplate.delete(codeKey); // 4. 根据手机号查询用户 User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getPhone, phone)); // 5. 用户不存在注册一个新用户 if (user null) { user createUserWithPhone(phone); } // 6. 生成Token并把用户信息写入Redis UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class); MapString, Object userMap BeanUtil.beanToMap(userDTO); userMap.entrySet().removeIf(e - e.getValue() null); String token UUID.randomUUID().toString(true); String tokenKey login:token: token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES); // 7. 返回Token给前端 return Result.ok(token); }这段代码里我还原了实际操作中的几个选择。Token用了UUID.randomUUID().toString(true)其中true表示去掉中间的横杠得到一个纯随机字符串放在请求头里比较干净前端处理起来不容易出错。验证码校验成功之后立刻删除这是必需的不然同一个验证码可以在过期时间内反复使用。用户首次登录时自动注册对应的createUserWithPhone方法只需要插入一条手机号记录并带上默认昵称和头像保证后续流程能正常走下去。登录成功后Redis里存的用户信息是Hash过期时间统一30分钟。这里需要说明一点BeanUtil.beanToMap会把Long类型的ID转成字符串存储读取时也会以字符串形式拿到再通过工具类填充回Long字段是可以的不会存在类型转换问题。但如果存之前直接用了JSON序列化整个对象读取时就有可能出现Integer和Long不匹配的坑所以Hash结构更稳。4.4 拦截器注册的路径与顺序细节拦截器实现完之后必须在WebMvcConfigurer里注册并设置拦截范围和顺序。我项目中的配置如下Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private RefreshTokenInterceptor refreshTokenInterceptor; Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(refreshTokenInterceptor) .addPathPatterns(/**) .order(0); registry.addInterceptor(loginInterceptor) .addPathPatterns(/user/**, /blog/**, /follow/**, /shop/**) .excludePathPatterns(/user/login, /user/code) .order(1); } }这里的两个细节需要特别强调。第一RefreshTokenInterceptor的order必须是0LoginInterceptor的order必须是1数字小的先执行。如果顺序反过来LoginInterceptor先执行时用户信息还没被放进ThreadLocal所有需要登录的接口都会被误判为未登录。第二LoginInterceptor拦截了/user/**但必须把/user/login和/user/code排除掉否则登录接口自己都过不了登录校验前端会陷入请求登录接口被拦的死循环。excludePathPatterns也可以写成/shop/**等匿名接口路径但我不建议把所有匿名路径都排除掉因为RefreshTokenInterceptor拦截/**已经处理了匿名请求的放行和Token续期LoginInterceptor只需要精确拦那些需要登录的路径即可。配置完成后可以做一个快速验证不带Token访问需要登录的接口返回401带上Token访问返回正常业务数据。5. 踩坑实录排查思路与技巧整理5.1 验证码校验不通的定位方法验证码接口写好后我遇到过一堆验证码明明输对了却提示错误的问题。最常见的原因就是Key不一致。发送验证码时写的是login:code:${phone}校验时写成了login:code:${phone.trim()}手机号本身没问题但多了一个空格Redis里就查不到同一个Key了。解决的办法是养成习惯所有从请求参数里拿到的手机号第一时间就trim()后面所有拼接Key的地方都用处理后的变量。第二个常见原因是Key没有设置正确过期时间。比如验证码设置的是5分钟测试环境放着不动超过5分钟再输入Redis里的Key已经自动消失前端拿到的验证码自然校验失败。这个属于预期行为但在联调时很容易让人误以为代码有bug。我通常在排查时先用Redis可视化工具看Key是否存在、剩余TTL是多少一眼就能定位。如果你用redis-cli GET看到的值是一串乱码那多半是序列化器不对。StringRedisTemplate存出来的是可读的字符串如果用RedisTemplate默认的JdkSerializationRedisSerializerKey和Value都带着序列化头显示为\xAC\xED开头的十六进制排查难度瞬间拉满。所以我在项目中全局统一用StringRedisTemplate避免这个问题。5.2 Hash存取的用户信息序列化问题用户信息写入Redis Hash时最容易踩的坑是null字段。BeanUtil.beanToMap默认会把值为null的字段也放进Map但StringRedisTemplate在写入Hash时value传null会直接抛异常或者写入一个空值导致读取时类型不一致。我在第一次实现时以为工具类会自动跳过空字段结果启动后登录一直报错排查日志才发现是某个DTO字段没有赋值。读取方向的坑也有。opsForHash().entries(tokenKey)返回的是MapObject,Object不是MapString,String。虽然大多数情况下field和value都是String但Java泛型不会自动帮你转直接用工具类填充Bean时可能出现类型不匹配。我的处理方式是手动转为MapString,StringMapString, String userMap new HashMap(); MapObject, Object entries stringRedisTemplate.opsForHash().entries(tokenKey); entries.forEach((k, v) - userMap.put(k.toString(), v.toString()));这样后续无论填充Bean还是从Map里取字段类型都是确定的。经验是从Redis取出任何数据都先想清楚它的真实类型再决定要不要转换不要指望框架帮你自动处理。5.3 拦截器放行规则与跨域预检拦截器放行规则写错最典型的症状是登录接口也被登录拦截器拦了。如果你配置了addPathPatterns(/user/**)但没排除/user/login那前端请求登录接口时拦截器发现ThreadLocal里没有用户直接返回401登录永远不可能成功。排查时可以先看路径配置再确认excludePathPatterns里的路径和Controller里的RequestMapping是否完全一致。跨域预检是另一个高频问题。前后端分离项目里前端axios带自定义Header请求时浏览器会先发一个OPTIONS请求做跨越预检。这个OPTIONS请求会被Spring MVC拦截器拦截到如果拦截器直接返回401前端控制台会报CORS错误而且这个错误很难和拦截器联想到一起。我的做法是在RefreshTokenInterceptor最前面加一个判断OPTIONS请求直接放行同时在后端的跨域配置里允许Authorization这个Header。5.4 续期、并发与强制下线的边界并发场景下Token续期还有一个容易被忽视的时序问题。假设用户同时发出两个请求两个请求都带着同一个TokenRefreshTokenInterceptor会分别执行两次expire结果是把过期时间往后推了30分钟这个没问题。但如果用户在某一个请求里主动注销服务端删除了Token Key另一个并发请求可能刚好在删除前执行了expire就会出现注销后Token还能再活30分钟的隐患。这种边界很极端但处理方案其实也简单注销接口删除Token后如果业务上严格要求可以再配合黑名单机制。强制下线在Redis方案里实现起来很舒服后台管理员封禁用户时直接找到该用户的Token Key并delete用户下一次请求时发现Redis里没有对应数据登录状态立即失效。这比Session方案需要遍历所有实例的Session要简单很多。如果后续业务要支持一个账号只能在一台设备登录还可以在登录时把旧Token找到并删除同一时间只保留最新Token。5.5 简单压测数据与测试脚本思路登录链路接入Redis后性能主要取决于Redis单机能力和网络开销。我自己用Jmeter简单压过一轮大致场景是1000个线程每个线程先调用登录接口获取Token再带着Token请求一个需要登录的业务接口循环10次。在我本地开发机上Token校验接口的P99能够稳定在20毫秒左右绝大多数请求都在10毫秒以内。这个数据不算标准基准但可以说明一个问题验证码登录和Token校验两条链路不会成为系统瓶颈。如果你也想做类似压测最简单的方式是Jmeter脚本里添加一个HTTP请求先用正则表达式提取登录响应里的Token再放到下一个HTTP请求的Header里。要注意的是造数据时不要所有线程共用同一个Token那样会集中在同一个Redis Key上测出来的结果不真实。无论是压测还是日常开发每个用户独立的Token才能模拟出真实业务场景。最后再分享一个我个人维护这套登录态时的小习惯所有Key前缀、过期时长都抽成常量放在一个专门的RedisKeyUtil类里而不是散落在Service和拦截器里。后面要调整登录时长只需要改一处排查线上问题也只需要看常量就能知道哪个Key是干什么用的。登录模块虽然看起来简单但它是所有业务请求的第一道关卡把这里的细节理清楚后面做点赞、关注、评论这些功能时会省非常多的事。
阅读完成 · 觉得有帮助?