首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Redis分布式缓存,原来还可以这么用!企业级源码!java程序员必备!

Redis分布式缓存,原来还可以这么用!企业级源码!java程序员必备!

作者头像
卡卡罗特AI
发布2026-09-10 08:43:47
发布2026-09-10 08:43:47
420
举报

大家好,我是程序员卡卡罗特。之前我们聊过 Redis 在分布式锁、限流这些场景中的应用。

今天想和大家分享另一个高频使用场景——「分布式缓存」,并且结合用户登录这个场景去讲解。

想象一下,你们公司有个日活百万的 App,用户登入成功后,会获取一个token,后续调接口与后端交互,都是通过在请求头里面加上token来获取的。

业界,标准做法其实特简单:登录成功后,把用户状态(Token+核心信息)一把梭塞进Redis。后面每个请求都带着Token来,服务端直接找Redis验货,毫秒级响应,又快又稳。

✿一、具体业务流程

1

发令牌:用户输入账号密码登录成功,服务端当场生成一个唯一Token

2

存Redis:把这个Token和对应的用户核心信息塞进Redis,设个过期时间(我们是7天)

3

客户端保管:Token返回给App,App自己存本地(LocalStorage或者啥都行)

4

携令牌访问:App后续所有接口请求,都在Header里带上这个Token

5

拦截器校验:服务端拦截器拿到Token,去Redis里查。查到就放行,查不到直接401

✿二、核心代码

我们用的是Spring Boot + RedisTemplate,为了直观,Token用UUID生成。 真实项目里可以考虑JWT,但注销那套还是得靠Redis黑名单,各有各的坑。

1、登录成功,生成 Token 并写缓存

代码语言:javascript
复制
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 认证服务 - 负责发"通行证"
 */
@Service
public class AuthService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private UserService userService;

    public LoginResult login(String phone, String password) {
        // 1. 验证用户身份
        User user = userService.findByPhone(phone);
        if (user == null) {
            throw new RuntimeException("用户不存在");
        }

        // 这里有个坑:密码比对一定要用BCrypt,别明文存,别自己写加密,血的教训
        if (!user.getPassword().equals(encryptPassword(password))) {
            throw new RuntimeException("密码错误");
        }

        // 2. 生成Token,前缀加个APP_,方便运维在Redis里一眼认出
        String token = "APP_" + UUID.randomUUID().toString().replace("-", "");

        // 3. 构建缓存对象,只存最核心信息,越轻量越好
        LoginCacheUser cacheUser = new LoginCacheUser();
        cacheUser.setUserId(user.getId());
        cacheUser.setPhone(user.getPhone());
        cacheUser.setNickname(user.getNickname());

        // 4. 存Redis,key要规范,方便后续清理和管理
        // 我们的格式:app:login:token:{token},90天过期,具体的天数可以配置到Apollo
        String redisKey = "app:login:token:" + token;
        redisTemplate.opsForValue().set(redisKey, cacheUser, 90, TimeUnit.DAYS);

        // 5. 返回给前端
        LoginResult result = new LoginResult();
        result.setToken(token);
        result.setUserInfo(new UserVo(user));
        return result;
    }
}

2、拦截器逻辑

App 端发请求时,会在 Header 中带上 Token,比如:X-Auth-Token: APP_xxxxxxx

代码语言:javascript
复制
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用户上下文拦截器
 * 从请求头里抠出Token,解析后塞到ThreadLocal,供业务代码直接用
 */
public class AppUserContextInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(AppUserContextInterceptor.class);

    private static final String TOKEN_HEADER = "X-Auth-Token";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            // 1. 从Header拿Token
            String token = request.getHeader(TOKEN_HEADER);
            if (token == null || token.trim().isEmpty()) {
                LOGGER.debug("请求头里没有Token,可能是个匿名访问");
                // 注意这里:不是所有接口都要登录,所以没Token不能直接打回去,得让后续逻辑判断
                return true;
            }

            // 2. 去Redis查用户信息
            // 这里我们封装了个方法,真实场景建议加本地缓存Caffeine,减少Redis压力
            LoginCacheUser cacheUser = getUserFromRedis(token);
            if (cacheUser == null) {
                LOGGER.warn("Token在Redis里找不到,可能过期了: {}", token);
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }

            // 3. 塞进ThreadLocal,后续业务代码直接拿
            AppUserContext.setCurrentUser(cacheUser);
            LOGGER.info("请求 {} 已设置用户上下文: {}", request.getRequestURI(), cacheUser.getUserId());
            return true;
        } catch (Exception e) {
            LOGGER.error("解析Token出错了", e);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 4. 请求结束后必须清理ThreadLocal,不然线程池复用会串数据
        // 这个bug我们排查了三天,线程池里上一个请求的userId跑到下一个请求里,当场报警
        AppUserContext.clear();
    }

    private LoginCacheUser getUserFromRedis(String token) {
        String redisKey = "app:login:token:" + token;
        return (LoginCacheUser) redisTemplate.opsForValue().get(redisKey);
    }
}

注意:血泪教训afterCompletion里一定要清ThreadLocal

我们线上曾经出现过用户A的信息串到用户B的请求里,原因是用了线程池,线程复用导致ThreadLocal没清理。后来改成TransmittableThreadLocal

3、AppUserContext存储上下文

为了支持线程池传递上下文,我们用了阿里开源的TransmittableThreadLocal普通ThreadLocal在异步场景下会丢数据,这个坑不少人都踩过。

1. 引入 Maven 依赖
代码语言:javascript
复制
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>transmittable-thread-local</artifactId>
  <version>2.14.2</version>
</dependency>

2. 实现 AppUserContext
代码语言:javascript
复制
import com.alibaba.ttl.TransmittableThreadLocal;
import org.springframework.util.Assert;

/**
 * 基于TTL的用户上下文
 * 支持线程池、@Async等异步场景
 */
public class AppUserContext {

    private static final TransmittableThreadLocal<LoginCacheUser> CURRENT_USER =
        new TransmittableThreadLocal<>();

    private AppUserContext() {}

    public static void setCurrentUser(LoginCacheUser user) {
        Assert.notNull(user, "用户信息不能为null");
        CURRENT_USER.set(user);
    }

    public static LoginCacheUser getCurrentUser() {
        LoginCacheUser user = CURRENT_USER.get();
        if (user == null) {
            throw new BusinessException("用户上下文不存在,请检查登录状态");
        }
        return user;
    }

    public static Long getCurrentUserId() {
        return getCurrentUser().getUserId();
    }

    public static String getCurrentUserPhone() {
        return getCurrentUser().getPhone();
    }

    // 安全获取,不抛异常
    public static Long getCurrentUserIdSafely() {
        LoginCacheUser user = CURRENT_USER.get();
        return user != null ? user.getUserId() : null;
    }

    public static void clear() {
        CURRENT_USER.remove();
    }

    public static boolean hasUser() {
        return CURRENT_USER.get() != null;
    }
}

4、配置拦截器

代码语言:javascript
复制
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private AppUserContextInterceptor appUserContextInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(appUserContextInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/auth/login") // 登录接口自己验自己,别拦截
                .excludePathPatterns("/api/auth/register")
                .excludePathPatterns("/api/public/**"); // 公共接口也要放开
    }
}

5、后去用户登入信息

有了AppUserContext后,我们可以在Controller、Service层非常优雅的获取用户登入信息。

代码语言:javascript
复制
@RestController
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/profile")
    public ApiResult<UserProfile> getProfile() {
        // 这行代码写起来真爽,不用每个接口都传userId
        Long userId = AppUserContext.getCurrentUserId();

        UserProfile profile = userService.getUserProfile(userId);
        return ApiResult.success(profile);
    }
}

@RestController
@RequestMapping("/api/order")
public class OrderController {

    @GetMapping("/list")
    public ApiResult<List<Order>> getOrderList() {
        // 订单、收藏、评论...所有需要用户的接口都这么拿
        Long currentUserId = AppUserContext.getCurrentUserId();
        List<Order> orders = orderService.findOrdersByUserId(currentUserId);
        return ApiResult.success(orders);
    }
}

这套方案我们在线上跑了两年,日活从几万干到几百万,Redis稳如老狗

刚开始也踩了不少坑,比如ThreadLocal没清理、key命名不规范、序列化方式不当等等。但修完这些bug后,整个登录模块基本没出过问题。

我是程序员卡卡罗特,每天分享后端干货知识!如果觉得对你有用,点个赞+个关注在走呗🥳~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • ✿一、具体业务流程
  • ✿二、核心代码
    • 1、登录成功,生成 Token 并写缓存
    • 2、拦截器逻辑
    • 3、AppUserContext存储上下文
      • 1. 引入 Maven 依赖
      • 2. 实现 AppUserContext
    • 4、配置拦截器
    • 5、后去用户登入信息
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档