问题产生原因
在一些秒杀购物项目中,可能会遇到部署多个服务的情况,通常情况下我们会使用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";
}
}