秒杀系统2-Redis解决分布式Session问题

前言

如果操作在一台Tomcat上,是没有问题的,但是当我们部署多台系统,配合Nginx的时候会出现用户登录的问题。

原因

由于Nginx 使用负载均衡策略,将请求分发到后端,
也就是说,在Tomcat 1 登录后,用户信息存在Tomcat1的Session里,下次请求又到了Tomcat2上,这时Tomcat2上Session里还没有用户信息,就要去登录。

在这里插入图片描述

解决

redis 安装教程自学

Redis实现分布式Session

方法1、使用SpringSession实现 (这个方法跳转不了页面)

1、添加依赖:

        <!-- spring data 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>
        <!-- spring-session 依赖 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

2、配置 Redis

spring:
  # thymeleaf关闭缓存
  thymeleaf:
    cache: false

  # MySql数据源配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/secKill?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

    # 最快的连接池
    hikari:
      # 连接池的名称
      pool-name: DateHikariCP
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大时间 默认600000(10分钟)
      idle-timeout: 1800000
      # 最大连接数 默认10
      maximum-pool-size: 10
      # 从连接池返回的连接自动提交
      auto-commit: true
      # 连接最大存活时间  0代表永久存活,默认 1800000(分钟)
      max-lifetime: 1800000
      # 连接超时时间 默认30000(30)
      connection-timeout: 30000
      # 测试连接是否可以的查询语句
      connection-test-query: SELECT 1
  # Redis配置
  redis:
    host: 127.0.0.1
    port: 6379
    # 使用的数据库 0
    database: 0
    # 连接超时时间
    timeout: 10000ms
    lettuce:
      # 连接池的配置
      pool:
        # 最大连接数 8
        max-active: 8
        # 最大连接的阻塞时间
        max-wait: 10000ms
        # 最大空闲连接
        max-idle: 200
        # 最小空闲连接
        min-idle: 5


# mybatis-plus 配置
mybatis-plus:
  # 配置 Mapper.xml的映射文件路径
  mapper-locations: classpath*:/mapper/*.xml

# mybatis SQL 打印(接口方法在的包,不是 mapper.xml所在的包)
logging:
  level:
    com.example.seckill.mapper: DEBUG


这个时候 就OK 了,登录后,Session就会存进 Redis - 0(库) -spring Session 中
在这里插入图片描述

此时 注释掉我们代码中的

//        /**
//         * 校验用户成功后生成 cookie,将cookie 与用户存进 session中
//         */
//        // 使用UUID生成 cookie
//        String cookie = UUIDUtil.uuid();
//        // 将 cookie 和用户存进 session 中
//        request.getSession().setAttribute(cookie,user);
//        // 设置 cookie
//        CookieUtil.setCookie(request,response,"userCookie",cookie);

再次登录 发现也是会存进Redis中。

方法2、将用户信息存进redis中

1、添加依赖

<!-- spring data 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>

2、redis 的配置 和方法1一样
3、Redsi配置类

package com.example.seckill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


/**
 * Redis 配置
 */
@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类型value序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        //注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

4、json工具类

这个很重要,如果没有的话 从redis中获取的user信息匹配不对,跳转不了 商品页面

package com.example.seckill.util;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.List;

/**
 * Json工具类
 *
 * @author zhoubin
 * @since 1.0.0
 */
public class JsonUtil {
    private static ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 将对象转换成json字符串
     *
     * @param obj
     * @return
     */
    public static String object2JsonStr(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            //打印异常信息
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将字符串转换为对象
     *
     * @param <T> 泛型
     */
    public static <T> T jsonStr2Object(String jsonStr, Class<T> clazz) {
        try {
            return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz);
        } catch (JsonParseException e) {
            e.printStackTrace();
        } catch (JsonMappingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json数据转换成pojo对象list
     * <p>Title: jsonToList</p>
     * <p>Description: </p>
     *
     * @param jsonStr
     * @param beanType
     * @return
     */
    public static <T> List<T> jsonToList(String jsonStr, Class<T> beanType) {
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = objectMapper.readValue(jsonStr, javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

5、修改 UserServiceImpl代码

在验证用户后,将user 存进redis
并添加通过cookie 获取user的方法

package com.example.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.seckill.common.RespBean;
import com.example.seckill.common.RespBeanEnum;
import com.example.seckill.controller.parm.LoginRequestParam;
import com.example.seckill.exception.GlobalException;
import com.example.seckill.mapper.UserMapper;
import com.example.seckill.pojo.User;
import com.example.seckill.service.IUserService;
import com.example.seckill.util.CookieUtil;
import com.example.seckill.util.JsonUtil;
import com.example.seckill.util.MD5Util;
import com.example.seckill.util.UUIDUtil;
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;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author jobob
 * @since 2022-06-13
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Autowired(required = false)
    UserMapper userMapper;

    @Autowired
    private RedisTemplate redisTemplate;


    @Override
    public RespBean doLogin(LoginRequestParam param, HttpServletRequest request, HttpServletResponse response) {
        String password = param.getPassword();
        String mobile = param.getMobile();

        User user = userMapper.selectById(mobile);
        if (null == user){
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }

        if (!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }

        /**
         * 校验用户成功后生成 cookie,将cookie 与用户存进 session中
         */
        // 使用UUID生成 cookie
        String cookie = UUIDUtil.uuid();
        // 将 cookie 和用户存进 session 中
//        request.getSession().setAttribute(cookie,user);
        redisTemplate.opsForValue().set("user:" + cookie, JsonUtil.object2JsonStr(user));

        // 设置 cookie
        CookieUtil.setCookie(request,response,"userCookie",cookie);
        return RespBean.success(cookie);
    }

    @Override
    public User getUserByCookie(String cookie, HttpServletRequest request, HttpServletResponse response) {
        if (StringUtils.isEmpty(cookie)){
            return null;
        }
        String userJson = (String) redisTemplate.opsForValue().get("user:" + cookie);
        User user = JsonUtil.jsonStr2Object(userJson, User.class);
        if (null != user){
            // 设置 cookie
            CookieUtil.setCookie(request,response,"userCookie",cookie);
        }
        return user;
    }
}


6、商品Controller

package com.example.seckill.controller;

import com.example.seckill.common.RespBean;
import com.example.seckill.controller.parm.LoginRequestParam;
import com.example.seckill.pojo.User;
import com.example.seckill.service.IGoodsService;
import com.example.seckill.service.IUserService;
import lombok.extern.slf4j.Slf4j;
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;


@RequestMapping("/goods")
@Controller
@Slf4j
public class GoodsController {

    @Autowired
    IGoodsService goodsService;

    @Autowired
    IUserService userService;

    /**
     * 跳转商品页
     * @param
     * @param model
     * @param cookie
     * @return
     */
    @RequestMapping("/toList")
    public String toList(HttpServletRequest request, HttpServletResponse response, Model model, @CookieValue("userCookie") String cookie){

        if (StringUtils.isEmpty(cookie)){ //如果 cookie为 空 跳转到 登录页面
            return "login";

        }
        // 从session 中获取用户
//        User user = (User) session.getAttribute(cookie);
        User user = userService.getUserByCookie(cookie, request, response);

        if (null == user){ // 如果用户信息为空 跳转登录
            return "login";
        }
        // 将用户信息 传到前端页面
        model.addAttribute("user",user);
        return "goodsList";

    }

}


以上避免每个接口都要去完成 根据Cookie获取User信息,避免代码冗余,特做优化:
1、UserArgumentResolvers类

package com.example.seckill.config;

import com.example.seckill.pojo.User;
import com.example.seckill.service.IUserService;
import com.example.seckill.util.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;

/**
 * 自定义用户参数
 */
@Component
public class UserArgumentResolvers implements HandlerMethodArgumentResolver {

    @Autowired
    IUserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> parameterType = parameter.getParameterType();


        return parameterType == User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

        String cookie = "486ceb27176a485f85e3c3d1f4f35e77";
//        String cookie = CookieUtil.getCookieValue(request, "userCookie");
        if (StringUtils.isEmpty(cookie)){ //如果 cookie为 空 跳转到 登录页面
            return null;

        }
        return userService.getUserByCookie(cookie, request, response);

    }
}

2、

package com.example.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的配置类
 *
 */
@Configuration
//@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

3、goodController

package com.example.seckill.controller;

import com.example.seckill.common.RespBean;
import com.example.seckill.controller.parm.LoginRequestParam;
import com.example.seckill.pojo.User;
import com.example.seckill.service.IGoodsService;
import com.example.seckill.service.IUserService;
import lombok.extern.slf4j.Slf4j;
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;


@RequestMapping("/goods")
@Controller
@Slf4j
public class GoodsController {

    @Autowired
    IGoodsService goodsService;

    @Autowired
    IUserService userService;

    /**
     * 跳转商品页
     * @param
     * @param model
     * @param
     * @return
     */
    @RequestMapping("/toList")
    public String toList(Model model,User user){
        // 将用户信息 传到前端页面
        model.addAttribute("user",user);
        return "goodsList";

    }

}


问题:这里我遇到一个问题,每次生成Cookie存进Redis是正常的,但是登录成功后调用其他接口 通过CookieUtil 获取的Cookie并不是本次登录生成的Cookie 就很烦~ 我对这块不太了解 所以在保证redis中存在这个cookie ,将cookie写死了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值