使用Redis解决:集群的Session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题。
- 问题背景
- 无状态HTTP协议:HTTP协议本身是无状态的,服务器无法直接识别不同请求是否来自同一用户。
- Session的作用:通过Session(通常存储在服务器内存中)跟踪用户状态(如登录信息、购物车数据等)。
- 集群环境的问题:当用户请求被负载均衡分配到不同服务器时,每个服务器的Session数据是独立的。若用户第二次请求被路由到另一台服务器,该服务器可能没有对应的Session数据,导致用户状态丢失。
- 问题示例
假设用户访问一个由3台服务器组成的集群:- 用户首次访问服务器A,登录成功后,Session(含用户ID)存储在A的内存中。
- 下一次请求被负载均衡分配到服务器B,但B的内存中没有该用户的Session。
- 结果:用户被迫重新登录,体验中断。
- 核心挑战
- 数据一致性:多个服务器需要共享或同步Session数据。
- 可用性:Session存储服务需高可用,避免单点故障。
- 性能:频繁的Session读写需低延迟,不影响用户体验。
- 常见解决方案
- 方案1:Session复制(Replication)
- 原理:将Session复制到集群中所有服务器。
- 优点:无需外部依赖,简单易实现。
- 缺点:
- 内存和带宽开销大(尤其节点多时)。
- 数据一致性问题(如网络分区时)。
- 方案2:集中式存储(推荐)
- 原理:使用独立存储(如Redis、Memcached、数据库)保存Session,所有服务器共享访问。
- 优点:
- 解耦Session与服务器,扩展性强。
- 支持高可用(如Redis集群)。
- 缺点:引入额外组件,增加系统复杂度。
- 方案3:粘性会话(Sticky Session)
- 原理:负载均衡器将同一用户的请求始终路由到同一台服务器。
- 优点:天然保证Session一致性。
- 缺点:
- 负载不均衡(某些服务器可能过载)。
- 服务器故障时,其上的Session永久丢失。
- 方案4:无状态设计(如JWT)
- 原理:将用户状态直接存储在客户端(如Token中),服务器无需保存Session。
- 优点:彻底解决共享问题,天然支持分布式。
- 缺点:Token体积较大,且需处理加密和安全性。
- 方案1:Session复制(Replication)
- 实际应用场景
- 电商/社交平台:常用集中式存储(如Redis)确保高并发下的Session一致性。
- 微服务架构:通过JWT等无状态方案简化服务间通信。
- 传统Java EE集群:Tomcat可通过
Redis Session Manager
插件实现共享。
- 最佳实践建议
- 优先选择集中式存储(如Redis),兼顾性能和扩展性。
- 设置合理的Session过期时间,减少存储压力。
- 启用存储的高可用模式(如Redis Sentinel或Cluster)。
- 结合HTTPS和加密,防止Token或Session被窃取。
文章目录
- 使用Redis解决:集群的Session共享问题
- 一、基于Session实现的用户登录
- 二、使用Redis替代Session,解决集群的Session共享问题
- 1.UserServiceImpl业务层实现
- 前置条件,注入 StringRedisTemplate
- 发送短信验证码
- 短信验证码登录、注册
- 2.LoginInterceptor登录拦截器
- 校验登录状态
- 3.添加登录拦截器到WebMvcConfigurer
- 三、优化登录拦截器
- 1.引入刷新token拦截器:RefreshTokenInterceptor
- 2.重构LoginInterceptor登录拦截器逻辑
- 3. 添加刷新token拦截器到WebMvcConfigurer配置中
一、基于Session实现的用户登录
传送门:基于Session实现用户登录
二、使用Redis替代Session,解决集群的Session共享问题
将原存储到session的地方,替换为存储到redis中。
1.UserServiceImpl业务层实现
前置条件,注入 StringRedisTemplate
@Resourceprivate StringRedisTemplate stringRedisTemplate;
发送短信验证码
@Overridepublic Result sendCode(String phone) {// 1. 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5.发送验证码log.debug("发送短信验证码成功,验证码:{}", code);return Result.ok();}
短信验证码登录、注册
@Overridepublic Result login(LoginFormDTO loginForm) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 2.从redis获取验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 3.不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = lambdaQuery().eq(User::getPhone, phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到redis中// 7.1 随机生成token(不带划线),作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2 将User对象转化为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() // 数据拷贝选项.setIgnoreNullValue(true) // 忽略null值.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()) // 字段值编辑器);// 7.3 存储String tokenKey = RedisConstants.LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4 设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);return Result.ok(token);}private User createUserWithPhone(String phone) {// 1.创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));// 2.保存用户save(user);return user;}
2.LoginInterceptor登录拦截器
校验登录状态
/*** LoginInterceptor 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;/*** 未加入Spring IOC容器管理,使用构造方法初始化 stringRedisTemplate*/public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}// 2.基于token获取redis中的用户String tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);// 3.判断用户是否存在if (userMap.isEmpty()) {// 4.不存在,拦截response.setStatus(401);return false;}// 5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
3.添加登录拦截器到WebMvcConfigurer
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/code", "/user/login", "/othoer/**");}
}
三、优化登录拦截器
为解决访问未拦截接口时,不刷新token导致用户登录过期的问题,引入RefreshTokenInterceptor
1.引入刷新token拦截器:RefreshTokenInterceptor
// 关键包
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;/*** RefreshTokenInterceptor 刷新Token拦截器*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;/*** 未加入Spring IOC容器管理,使用构造方法初始化 stringRedisTemplate*/public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// 放行return true;}// 2.基于token获取redis中的用户String tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);// 3.判断用户是否存在if (userMap.isEmpty()) {// 放行return true;}// 5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
2.重构LoginInterceptor登录拦截器逻辑
// 关键包
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.concurrent.TimeUnit;/*** LoginInterceptor 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判断是否需要拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null) {// 没有,需要拦截,设置状态码response.setStatus(401);// 拦截return false;}// 有用户则放行return true;}}
3. 添加刷新token拦截器到WebMvcConfigurer配置中
// 关键包
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code", "/user/login", "/other/**").order(1);// 刷新token拦截器,order=0优先加载registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}