Session粘滞性问题->Redis实现session共享

文章目录


一、Session

 当用户首次访问时,服务器为其创建一个唯一的 Session 对象,并生成一个 SessionId 写回客户端(通常通过 Cookie)。此后,客户端每次请求都会自动带上这个 SessionId,服务器便可根据它找到对应用户的会话,从而实现"记住"用户状态。

1.Session 的存储位置

 在传统的 Java Web 应用中,Tomcat 默认把 Session 对象存放在 JVM 堆内存中。内部实际上是一个 ConcurrentHashMap,Key 为 SessionId,Value 为 HttpSession 对象,其中存储了用户相关的属性。

2.Session的生命周期

 Session 在以下情况会被销毁:

  • 超过设定的过期时间(默认 30 分钟,可通过 web.xml 或 server.xml 中的 session-timeout 配置)。
  • 调用 session.invalidate() 方法手动销毁。
  • Tomcat 重启或宕机------由于数据只存在内存,重启后全部丢失。

3.Session的粘滞性问题

 因为每个 Tomcat 实例都在自己的内存中维护 Session,一旦引入负载均衡和多个实例,就会出现经典的 Session 共享问题。

假设你有多台 Tomcat,前端使用 Nginx 轮询分发请求

  • 用户第一次请求 → Nginx → Tomcat 1 → 登录成功,Session 存在 Tomcat 1 的内存中
  • 用户第二次请求被分到 Tomcat 2,Tomcat 2 的内存里没有这个 Session,就会认为用户未登录。

这就是 Session 粘滞性问题。

二、解决方案:tomcat+redis+nginx 实现session共享

 业界主流的方案是使用 Redis 作为集中式 Session 存储器。所有 Tomcat 实例在需要读写 Session 时都去访问同一个 Redis,从而达到会话共享。

1.登录校验

 对于下面的验证码登录实践案例中,原本 session.setAttribute("code", code) 被替换为 Redis 写入。这样无论请求被 Nginx 分发到哪台机器,校验验证码时都从同一个 Redis 读取。

 此时Redis中存储的数据如下,code对应验证码校验,token存储对应的用户 UserDTO

java 复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
    //校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        //手机号不符合
        return Result.fail("手机号格式错误");
    }
    //手机号符合,生成验证码
    String code = RandomUtil.randomNumbers(6);
    /*//保存验证码到session
    session.setAttribute("code", code);*/
    //保存验证码到redis
    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
    //发送验证码
    log.debug("发送验证码成功,验证码:{}", code);
    //返回ok
    return Result.ok();
}
java 复制代码
public Result login(LoginFormDTO loginForm, HttpSession session) {
    //校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        //手机号不符合
        return Result.fail("手机号格式错误");
    }
    //从redis中获取验证码 校验验证码
    /*  Object cacheCode = session.getAttribute("code");*/
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        //不一致 报错
        return Result.fail("验证码错误");
    }
    //一致 根据手机号查询用户
    User user = baseMapper
            .selectOne(new LambdaQueryWrapper<User>()
                    .eq(User::getPhone, phone));
    //判断用户是否存在
    if (user == null) {
        //不存在 创建新用户
        user = createUserWithPhone(phone);
    }
    /*//保存用户信息到session
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));*/
    //生成token
    String token = UUID.randomUUID().toString(true);
    //userDTO转map
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>()
            , CopyOptions.create().setIgnoreNullValue(true)
                    .setFieldValueEditor(
                            (name, value) -> value.toString()
                    ));
    //保存用户信息到redis
    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, map);
    //设置过期时间
    stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
    return Result.ok(token);
}

2.拦截器设置

  • 第一个拦截器RefreshTokenInterceptor的作用:从请求头取 Token → 查 Redis → 得到用户数据 → 存入 ThreadLocal,刷新 Redis 中该 Token 的过期时间,请求结束后,清理 ThreadLocal。
  • 第二个拦截器LoginInterceptor的作用:检查 ThreadLocal 里有没有用户数据。有就放行,没有就返回 401

ThreadLocal相当于备份了Redis中的UserDTO数据,对于该次请求(该次线程)ThreadLocal中的数据存在,方便进行显示,不用再去Redis中查看了
两个拦截器为了解决Redis中token的过期时间问题,如果只设置一个拦截器,用户每次访问需要UserDTO登录校验的界面,会刷新token过期时间,但是如果访问商家页面这种不需要登录校验,此时token的过期时间就没有刷新

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登陆拦截器
        registry
                .addInterceptor(new LoginInterceptor())
                .excludePathPatterns("/user/code"
                        , "/user/login"
                        , "/blog/hot"
                        , "/shop/**"
                        , "/shop-type/**"
                        , "/upload/**"
                        , "/voucher/**"
                )
                .order(1);
        //Token续命拦截器
        registry
                .addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .addPathPatterns("/**")
                .order(0);
    }
}
java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取用户
        if (UserHolder.getUser() == null) {
            //不存在用户 拦截
            response.setStatus(401);
            return false;
        }
        //存在用户放行
        return true;
    }
}
java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {
    private final StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从请求头中获取token
        String token = request.getHeader("authorization");
        if (StringUtils.isEmpty(token)) {
            //不存在token
            return true;
        }
        //从redis中获取用户
        Map<Object, Object> userMap =
                stringRedisTemplate.opsForHash()
                        .entries(RedisConstants.LOGIN_USER_KEY + token);
        //用户不存在
        if (userMap.isEmpty()) {
            return true;
        }
        //hash转UserDTO存入ThreadLocal
        UserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false));
        //token续命
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
相关推荐
睡不醒男孩0308231 小时前
第二篇:深入探索开源数据库高可用:构建基于CLup的PostgreSQL生产级高可用与读写分离架构
数据库·postgresql·开源·clup
Micro麦可乐3 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
码农阿豪3 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端
鼎讯信通4 小时前
风电光缆运维提质增效:G-4000A 光缆故障追踪仪破解风场巡检难题
运维·网络·数据库
三十..4 小时前
MySQL 从入门到高可用架构实战精要
运维·数据库·mysql
cfm_29145 小时前
Redis五大基本数据结构底层了解
数据结构·数据库·redis
真实的菜5 小时前
Redis 从入门到精通(十二):典型业务场景实战 —— 排行榜、限流器、秒杀系统、Session 共享
数据库·redis·python
你想考研啊5 小时前
mysql数据库导出导入
数据库·mysql·oracle
十年编程老舅6 小时前
Linux DRM:底层逻辑与实践架构
数据库·mysql
The Sheep 20237 小时前
Vue复习
linux·服务器·数据库