为什么使用分布式Session
单体项目一般使用的是一台Tomcat服务器,当用户量多的时候会将系统部署到多台服务器上会使用到nginx作为代理,就会出现用户登录的问题,
原因:Nginx默认使用的是轮询的负载均衡策略,请求会按照一定的时间顺序发送到后端服务器上,比如刚开始使用的是Tomcat1服务器做的登录操作,此时用户登录的Session信息就会存放在Tomcat1服务器中,当登录成功后,切换到其他页面发起请求时,此时请求被Nginx分发到了Tomcat2服务器上,这是Tomcat2服务器上的Session中没有这个用户的登录信息,导致用户又要重新登录。用户体验不好。
解决方案
引入了分布式Session
- Session复制
优点
无需修改代码,只需要修改Tomcat配置
缺点
Session同步传输占用内网带宽
多台Tomcat同步性能指数级下降
Session占用内存,无法有效水平扩展 - 前端存储
优点
不占用服务端内存
缺点
存在安全风险
数据大小受cookie限制
占用外网带宽 - Session粘滞
优点
无需修改代码
服务端可以水平扩展
缺点
增加新机器,会重新Hash,导致重新登录
应用重启,需要重新登录 - 后端集中存储
优点
安全
容易水平扩展
缺点
增加复杂度
需要修改代码
Redis实现分布式Session
方法一:使用SpringSession实现
- 添加依赖
<!-- 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.添加配置
spring:
redis:
#超时时间
timeout: 10000ms
#服务器地址
host: 127.0.0.1
#服务器端口
port: 6379
#数据库
database: 0
lettuce:
pool:
#最大连接数,默认8
max-active: 1024
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接
max-idle: 200
#最小空闲连接
min-idle: 5
3.测试
方法二:将用户信息存入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.添加配置
spring:
redis:
#超时时间
timeout: 10000ms
#服务器地址
host: 127.0.0.1
#服务器端口
port: 6379
#数据库
database: 0
lettuce:
pool:
#最大连接数,默认8
max-active: 1024
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接
max-idle: 200
#最小空闲连接
min-idle: 5
3.添加Redis配置类
/**
* Redis的配置,主要完成Redis的序列化
* @author JiaShuai
* @date 2022/10/9 16:04
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
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(connectionFactory);
return redisTemplate;
}
}
此时会有一个问题,每次都要查询判断信息,所以进行优化,在用户信息到达Controller层之前进行判断,将判断好的数据传入,
/**
* @author JiaShuai
* @date 2022/10/9 16:34
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz == 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 ticket = CookieUtil.getCookieValue(request, "userTicket");
if (StringUtils.isEmpty(ticket)) {
return null;
}
return userService.getUserByCookie(ticket, request, response);
}
}
/**
* @author JiaShuai
* @date 2022/10/9 16:36
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
}
(1)Controller层
/**
* @author JiaShuai
* @date 2022/10/9 14:53
*/
@Controller
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private UserService userService;
/*跳转到商品列表页*/
@RequestMapping("/toList")
public String toList(Model model, User user){
// if (StringUtils.isEmpty(ticket)){
// return "login";
// }
User user=(User) session.getAttribute(ticket);
// User user = userService.getUserByCookie(ticket,request,response);
// if (null==user){
// return "login";
// }
model.addAttribute("user",user);
return "goodsList";
}
}
(2)Service层
/**
* <p>
* 服务实现类
* </p>
*
* @author jishuai
* @since 2022-10-08
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
/*登录功能*/
@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.LOGINVO_ERROR);
// }
// if (!ValidatorUtil.isMobile(mobile)){
// return RespBean.error(RespBeanEnum.MOBILE_ERROR);
// }
//根据手机号获取用户信息
User user = userMapper.selectById(mobile);
if (null==user){
throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
}
//判断密码是否正确
if (!MD5Util.formPassToDBPass(password,user.getSlat()).equals(user.getPasword())){
throw new GlobalException(RespBeanEnum.LOGINVO_ERROR);
}
//生成Cookie
String ticket= UUIDUtil.uuid();
//存放用户信息到session中
// request.getSession().setAttribute(ticket,user);
//将用户信息存放到Redis中
redisTemplate.opsForValue().set("user:"+ticket,user);
CookieUtil.setCookie(request,response,"userTicket",ticket);
return RespBean.success();
}
/*根据Cookie获取用户*/
@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;
}
}
总结:将用户信息存放在Redis中在Nginx分发时现在Redis中查询用户信息,再在Tomcat中处理。