分布式session问题,以及Spring + Redis解决分布式session问题

本文探讨了在秒杀项目中,如何通过配置Spring Boot和Redis解决Nginx负载均衡导致的session失效问题。通过将session存储在Redis中,实现会话共享,并详细介绍了配置步骤和关键代码实现,如自定义MVC和异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题产生原因

在一些秒杀购物项目中,可能会遇到部署多个服务的情况,通常情况下我们会使用Nginx进行负载均衡,但是由于Nginx的轮询策略,同一个请求可能会转发到不同的tomcat上。因此会造成session频繁失效的问题。
那么这里使用Redis第三方存储的方式解决分布式session的问题

方法的原理

在用户登陆之后,将session和用户信息统一保存在Redis中,然后在项目中配置自定义参数,每次请求的时候去Redis中获取session
Spring中内置的spring session 核心的原理其实也是使用Redis进行统一的存储的方式解决分布式session的问题

解决方案

分布式session问题其实有很多种方案的:

session复制

通过配置多台tomcat实现会话统一,共享session
优点:
无需修改代码,只需要修改tomcat配置
缺点:
session同步传输占用内网宽带
多台tomcat性能指数下降
session占内存,无法有效水平扩展

前端存储

优点:
不会占用服务器内存
缺点:
存在安全隐患
数据大小受cookie的限制
占用外网带宽

session粘滞

优点:
无需修改代码
服务器可以水平扩展
缺点:
增加新机器,会重新生成hash,导致重新登录
应用重启,需要重新登录

后端集中存储

优点:
安全
容易水平扩展
缺点:
增加复杂度
需要修改代码

<!-- Redis 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

去Application.yml文件中去配置Redis

  # Redis 配置
  redis:
    # 服务器地址
    host: 127.0.0.1
    # 端口
    port: 6379
    # 数据库
    database: 0
    # 超时时间
    timeout: 10000ms
    lettuce:
      # 连接池
      pool:
        # 连接数 默认 8
        max-active: 8
        # 最大连接阻塞等待时间 默认 -1
        max-wait: 10000ms
        # 最大空闲连接 默认 8
        max-idle: 200
        # 最小空闲连接 默认 8
        min-idle: 5

配置Redis

package com.an.seckill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
 * Redis配置类
 *
 * @author An
 * @date 2022/9/8 19:03
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // key 序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value 序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // hash 类型的 key 序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash 类型的 value 序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

}

在登录的实现类中添加相关的代码:

package com.an.seckill.service.impl;

import com.an.seckill.common.LoginVo;
import com.an.seckill.common.RespBean;
import com.an.seckill.common.RespBeanEnum;
import com.an.seckill.exception.GlobalException;
import com.an.seckill.mapper.UserMapper;
import com.an.seckill.pojo.User;
import com.an.seckill.service.IUserService;
import com.an.seckill.utils.CookieUtil;
import com.an.seckill.utils.MD5Util;
import com.an.seckill.utils.UUIDUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

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

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author An
 * @since 2022-08-31
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 用户登录
     *
     * @param loginVo  账号 密码
     * @param request
     * @param response
     * @return
     */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        // 获取手机号和密码
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();
        // 参数校验
        // 判断手机号和密码是否为空
        //if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
        //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        //}
         判断手机号格式是否正确
        //if (!ValidatorUtil.isMobile(mobile)) {
        //    return RespBean.error(RespBeanEnum.MOBILE_ERROR);
        //}
        // 查询数据库 -- 根据手机号获取用户
        User user = userMapper.selectById(mobile);
        if (null == user) {
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
            // 抛出异常 全局登录异常
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }
        // 判断密码是否正确
        if (!MD5Util.formPassToDBPass(password, user.getSalt()).equals(user.getPassword())) {
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }
        // 生成cookie
        String ticket = UUIDUtil.uuid();
        request.getSession().setAttribute(ticket, user);
        // 将 Cookie存入 Redis中
        redisTemplate.opsForValue().set("user:" + ticket, user);
        // request.getSession().setAttribute(ticket,user);
        // 将cookie存入的 前端的cookie中去
        CookieUtil.setCookie(request, response, "userTicket", ticket);
        return RespBean.success(ticket);
    }

    /**
     * 根据cookie 获取用户
     *
     * @param userTicket cookie值
     * @return
     */
    @Override
    public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {
        if (StringUtils.isEmpty(userTicket)) {
            return null;
        }
        User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
        if (user != null) {
            CookieUtil.setCookie(request, response, "userTicket", userTicket);
        }
        return user;
    }
}

紧接着去自定义MVC让每次请求接口的时候去Redis中获取user对象

package com.an.seckill.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * MVC配置类
 *
 * @author An
 * @date 2022/9/8 20:29
 */
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}

自定义用户参数

package com.an.seckill.config;

import com.an.seckill.pojo.User;
import com.an.seckill.service.IUserService;
import com.an.seckill.utils.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

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

/**
 * 自定义用户参数
 *
 * @author An
 * @date 2022/9/8 20:34
 */
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private IUserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> clazz = parameter.getParameterType();
        return clazz == User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        String ticket = CookieUtil.getCookieValue(request, "userTicket");
        if (StringUtils.isEmpty(ticket)) {
            return null;
        }
        return userService.getUserByCookie(ticket, request, response);
    }
}

自定义全局异常处理类

package com.an.seckill.exception;

import com.an.seckill.common.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 全局异常
 *
 * @author An
 * @date 2022/9/7 22:50
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
    private RespBeanEnum respBeanEnum;

}

package com.an.seckill.exception;

import com.an.seckill.common.RespBean;
import com.an.seckill.common.RespBeanEnum;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


/**
 * 全局异常处理类
 *
 * @author An
 * @date 2022/9/7 22:51
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public RespBean ExceptionHandler(Exception e) {
        if (e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return RespBean.error(ex.getRespBeanEnum());
        } else if (e instanceof BindException) {
            BindException ex = (BindException) e;
            RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
            respBean.setMessage("参数校验异常:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
            return respBean;
        }
        return RespBean.error(RespBeanEnum.ERROR);
    }
}

最后就可以不需要在接口中去做用户信息校验了

package com.an.seckill.controller;

import com.an.seckill.pojo.User;
import com.an.seckill.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;

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

/**
 * 商品
 *
 * @author An
 * @date 2022/9/8 0:27
 */
@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private IUserService userService;

    /**
     * 功能描述:跳转到商品列表页
     *
     * @param model
     * @return
     */
    @RequestMapping("/toList")
    public String toList(Model model, User user) {
        //if (StringUtils.isEmpty(ticket)) {
        //    return "login";
        //}
         通过前端存储的cookie获取user
        User user = (User) session.getAttribute(ticket);
         通过Redis中存储的cookie获取用户信息
        //User user = userService.getUserByCookie(ticket, request, response);
        //if (null == user) {
        //    return "login";
        //}
        model.addAttribute("user", user);
        return "goodsList";
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值