分布式Session会话实现方案
一、什么是分布式 Session?
分布式 Session 是一种在微服务架构中,保证用户会话(Session)数据能够在多个独立的、无状态的服务实例之间共享和保持一致的机制。以下是传统单体架构中的 Session与微服务架构中的 Session之间的对比:
- 
传统单体架构中的 Session: - 单体架构 Session:应用部署在一台服务器上。用户登录后,服务器会在本地内存中创建一个 Session 对象,存储用户ID、用户名等数据,并给浏览器返回一个唯一的 Session ID(通常保存在 Cookie 中)。浏览器后续的每次请求都会带上这个 Session ID,服务器根据它找到对应的 Session 数据。一切都很简单,因为数据就在本地。
 
- 
微服务架构中的 Session: - 服务是无状态的:会有很多个微服务(用户服务、订单服务、商品服务……),它们可以独立部署、扩缩容。
- 请求是分布式的:用户的一个请求(比如“查看我的订单”)可能会先经过网关,然后被路由到订单服务的实例 A,而这个请求又需要调用用户服务的实例 B 来验证用户信息。
 
二、为什么需要分布式 Session?
在传统的单体应用架构中,Session 通常由应用服务器的内存(如 Tomcat 的 Session)管理。用户的多次请求都会路由到同一台服务器,可以轻松地存取 Session 数据。
然而,在微服务架构下,服务被拆分为多个独立的、可水平扩展的实例。这就带来了问题:
- 无状态负载均衡:用户的两次请求可能会被路由到不同的服务实例上。
- 内存隔离:每个服务实例都有自己的内存空间。如果 Session 存储在实例 A 的内存中,那么实例 B 无法读取到该 Session。
因此,就需要一种机制,让所有服务实例都能访问到同一个 Session,这就是分布式 Session。
三、主流分布式 Session 实现方案
3.1 Session同步
- 原理:当一个服务实例的 Session 发生变化时,它会将这个 Session 数据同步到集群中的所有其他服务实例。
- 优点:
- 任何实例都拥有全量的 Session 数据,请求可以路由到任意实例。
 
- 缺点:
- 网络开销巨大:随着服务实例数量的增加,同步产生的网络流量会呈指数级增长,严重消耗带宽和性能。
- 内存消耗高:每个实例都存储全量的 Session 数据,浪费内存。
- 耦合性高:实例间存在强耦合,不利于扩展。
 
3.2 Session粘滞
- 原理:在负载均衡器(如 Nginx、网关)上配置规则,将同一个用户的所有请求都固定地路由到同一个后端服务实例。这样,这个用户的 Session 就始终只存在于那个实例的内存中,就像回到了单体架构。
- 优点:
- 实现简单,无需修改业务代码。
- 避免了 Session 的跨实例共享问题。
 
- 缺点:
- 缺乏容错性:如果某个实例宕机,那么路由到该实例的所有用户 Session 都将丢失。
- 不利于水平扩展:在扩缩容(增加或减少实例)时,哈希结果会变化,导致大量用户的 Session 失效,需要重新登录。
- 负载可能不均衡:无法根据实例的实时负载进行动态调整。
 
3.3 服务端集中存储
- 原理:这是最常用、最经典的方案。将 Session 数据从服务本地内存中剥离出来,集中存储在一个外部化的、高可用的数据存储中心。所有微服务实例都从这个中心读写 Session 数据。
- 常用存储介质:
- Redis(首选):基于内存,性能极高;支持数据持久化;提供丰富的数据结构和过期机制。
- Memcached:同样是高性能内存缓存,但数据结构较简单。
- 数据库(如 MySQL):不推荐用于生产环境,因为性能瓶颈明显,但易于实现和理解。
- MongoDB:文档型数据库,Schema 灵活。
 
- 工作流程:
- 用户登录后,服务端生成一个全局唯一的 Session ID。
- 将 Session 数据序列化后存入 Redis(Key 是 Session ID,Value 是 Session 数据)。
- 将 Session ID 通过 Cookie 返回给客户端。
- 客户端后续请求携带此 Session ID。
- 任何微服务实例收到请求后,都使用这个 Session ID 去 Redis 中查询完整的 Session 数据。
 
- 优点:
- 真正实现了服务的无状态化,扩展性强。
- 数据持久化,即使所有服务重启,Session 也不会丢失(取决于 Redis 配置)。
- 高性能,Redis 的读写速度极快。
 
- 缺点:
- 引入了外部依赖,架构变复杂,需要保证 Redis 集群的高可用。
- 多了一次网络 IO,有微小的延迟。
 
3.4 基于 Token(现代趋势)
- 原理:完全废除服务器端的 Session 存储。用户登录后,服务器根据用户信息生成一个签名的 Token(如 JWT - JSON Web Token),然后将这个 Token 返回给客户端。客户端在后续请求中(通常在 HTTP Header 的 Authorization 字段)携带此 Token。服务器只需验证 Token 的签名有效性即可信任其中的用户信息。
- 优点:
- 完全无状态:服务端不需要存储任何会话数据,扩展性达到极致。
- 适合跨域:非常适合现代的前后端分离、跨域访问以及单点登录(SSO)场景。
- 灵活性高:Token 可以被任何拥有秘钥的服务验证,不局限于生成它的服务。
 
- 缺点:
- Token 一旦签发,在有效期内无法主动使其失效(除非使用额外的黑名单机制,但这又引入了状态)。
- Token 包含信息较多:由于用户信息都在 Token 里,它通常比一个 Session ID 大,会增加每次请求的带宽开销。
- 安全性考虑:需要妥善保管签名秘钥,并防范 XSS 和 CSRF 攻击。
 
四、典型实现案例
4.1 基于 Spring Boot + Redis
4.1.1 项目依赖:
<!-- Spring Boot Starter Data Redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Session 使用 Redis 作为数据源 -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
4.1.2 配置文件 application.yml:
spring:session:store-type: redis # 指定存储类型为 Redistimeout: 30m     # Session 过期时间,默认30分钟redis:host: localhostport: 6379# 如果有密码,在此配置 password: your-password
4.1.3 登录控制器(设置 Session):
@RestController
@RequestMapping("/auth")
public class AuthController {@PostMapping("/login")public ResponseEntity<String> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) {// 1. 验证用户名密码 (伪代码)User user = userService.authenticate(request.getUsername(), request.getPassword());if (user == null) {return ResponseEntity.badRequest().body("Login failed");}// 2. 获取当前 HttpSession,Spring Session 会自动创建或获取HttpSession session = httpRequest.getSession(true); // true 表示不存在则创建// 3. 将用户信息存入 Sessionsession.setAttribute("CURRENT_USER", user);// 也可以存其他任何需要跨请求共享的数据session.setAttribute("LOGIN_TIME", Instant.now());return ResponseEntity.ok("Login successful");}
}
4.1.4 业务控制器(读取 Session):
@RestController
@RequestMapping("/api")
public class BusinessController {@GetMapping("/profile")public ResponseEntity<User> getProfile(HttpServletRequest httpRequest) {// 1. 获取当前 Session (如果不存在,getSession(false) 返回 null)HttpSession session = httpRequest.getSession(false);if (session != null) {// 2. 从 Session 中获取用户信息User currentUser = (User) session.getAttribute("CURRENT_USER");if (currentUser != null) {// 3. 执行业务逻辑...return ResponseEntity.ok(currentUser);}}// 4. 未登录或 Session 过期return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();}@PostMapping("/logout")public ResponseEntity<String> logout(HttpServletRequest request) {// 使当前 Session 失效,Spring Session 会自动从 Redis 中删除它HttpSession session = request.getSession(false);if (session != null) {session.invalidate();}return ResponseEntity.ok("Logged out");}
}
4.2 基于 Spring Boot + JWT
4.2.1 添加依赖
<dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- JWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><!-- Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>
4.2.2 配置文件
# application.yml
spring:redis:host: localhostport: 6379password: database: 0timeout: 2000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0jwt:secret: mySecretKeyForJWTTokenGenerationInSpringBootApplication2024expiration: 7200000  # 2小时header: Authorizationprefix: "Bearer "
4.2.3 创建JWT工具类
// JwtTokenUtil.java
@Component
public class JwtTokenUtil implements Serializable {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;@Value("${jwt.prefix}")private String tokenPrefix;// 生成 tokenpublic String generateToken(UserSession userSession) {Map<String, Object> claims = new HashMap<>();claims.put("sessionId", userSession.getSessionId());claims.put("userId", userSession.getUserId());claims.put("username", userSession.getUsername());claims.put("roles", userSession.getRoles());return Jwts.builder().setClaims(claims).setSubject(userSession.getUsername()).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}// 从 token 中获取用户名public String getUsernameFromToken(String token) {return getAllClaimsFromToken(token).getSubject();}// 从 token 中获取 sessionIdpublic String getSessionIdFromToken(String token) {return getAllClaimsFromToken(token).get("sessionId", String.class);}// 获取过期时间public Date getExpirationDateFromToken(String token) {return getAllClaimsFromToken(token).getExpiration();}// 获取所有声明private Claims getAllClaimsFromToken(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}// 验证 token 是否过期private Boolean isTokenExpired(String token) {final Date expiration = getExpirationDateFromToken(token);return expiration.before(new Date());}// 验证 tokenpublic Boolean validateToken(String token, UserSession userSession) {final String username = getUsernameFromToken(token);return (username.equals(userSession.getUsername()) && !isTokenExpired(token));}// 刷新 tokenpublic String refreshToken(String token) {Claims claims = getAllClaimsFromToken(token);claims.setIssuedAt(new Date());claims.setExpiration(new Date(System.currentTimeMillis() + expiration));return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();}// 提取 token(去掉前缀)public String extractToken(String header) {if (header != null && header.startsWith(tokenPrefix)) {return header.substring(tokenPrefix.length());}return null;}
}
4.2.4 Session 实体类
// UserSession.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserSession implements Serializable {private String sessionId;private Long userId;private String username;private List<String> roles;private Date loginTime;private Date lastAccessTime;private String token;public UserSession(Long userId, String username, List<String> roles) {this.sessionId = UUID.randomUUID().toString();this.userId = userId;this.username = username;this.roles = roles;this.loginTime = new Date();this.lastAccessTime = new Date();}
}
4.2.5 Redis Session 存储服务
// RedisSessionService.java
@Service
public class RedisSessionService {private static final String SESSION_KEY_PREFIX = "session:";private static final long SESSION_TIMEOUT = 7200L; // 2小时@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 保存 sessionpublic void saveSession(UserSession userSession) {String key = SESSION_KEY_PREFIX + userSession.getSessionId();redisTemplate.opsForValue().set(key, userSession, Duration.ofSeconds(SESSION_TIMEOUT));}// 获取 sessionpublic UserSession getSession(String sessionId) {String key = SESSION_KEY_PREFIX + sessionId;UserSession session = (UserSession) redisTemplate.opsForValue().get(key);if (session != null) {// 更新最后访问时间并延长过期时间session.setLastAccessTime(new Date());saveSession(session);}return session;}// 删除 sessionpublic void deleteSession(String sessionId) {String key = SESSION_KEY_PREFIX + sessionId;redisTemplate.delete(key);}// 检查 session 是否存在public boolean sessionExists(String sessionId) {String key = SESSION_KEY_PREFIX + sessionId;return Boolean.TRUE.equals(redisTemplate.hasKey(key));}// 延长 session 过期时间public void extendSession(String sessionId) {String key = SESSION_KEY_PREFIX + sessionId;redisTemplate.expire(key, Duration.ofSeconds(SESSION_TIMEOUT));}
}
4.2.6 JWT 认证过滤器
// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate RedisSessionService redisSessionService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {String token = resolveToken(request);if (token != null) {try {String sessionId = jwtTokenUtil.getSessionIdFromToken(token);UserSession userSession = redisSessionService.getSession(sessionId);if (userSession != null && jwtTokenUtil.validateToken(token, userSession)) {// 创建认证对象UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userSession, null, getAuthorities(userSession.getRoles()));authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 设置到 SecurityContextSecurityContextHolder.getContext().setAuthentication(authentication);// 更新 session 最后访问时间redisSessionService.extendSession(sessionId);}} catch (Exception e) {logger.warn("JWT token 验证失败: " + e.getMessage());}}chain.doFilter(request, response);}private String resolveToken(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}
}
4.2.7 Security 配置
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/api/auth/**").permitAll().antMatchers("/api/public/**").permitAll().antMatchers("/api/admin/**").hasRole("ADMIN").anyRequest().authenticated().and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
4.2.8 Redis 配置
// RedisConfig.java
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);serializer.setObjectMapper(mapper);template.setValueSerializer(serializer);template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;}
}
4.2.9 认证控制器
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate RedisSessionService redisSessionService;@Autowiredprivate PasswordEncoder passwordEncoder;// 模拟用户数据private Map<String, User> userDatabase = new HashMap<>();@PostConstructpublic void init() {// 初始化测试用户userDatabase.put("admin", new User(1L, "admin", passwordEncoder.encode("admin123"), Arrays.asList("ROLE_ADMIN", "ROLE_USER")));userDatabase.put("user", new User(2L, "user", passwordEncoder.encode("user123"), Arrays.asList("ROLE_USER")));}@PostMapping("/login")public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {User user = userDatabase.get(loginRequest.getUsername());if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("用户名或密码错误"));}// 创建 sessionUserSession userSession = new UserSession(user.getId(), user.getUsername(), user.getRoles());// 生成 tokenString token = jwtTokenUtil.generateToken(userSession);userSession.setToken(token);// 保存 session 到 RedisredisSessionService.saveSession(userSession);LoginResponse response = new LoginResponse(token,"Bearer",userSession.getSessionId(),new UserInfo(user.getId(), user.getUsername(), user.getRoles()));return ResponseEntity.ok(ApiResponse.success("登录成功", response));}@PostMapping("/logout")public ResponseEntity<?> logout(HttpServletRequest request) {String token = jwtTokenUtil.extractToken(request.getHeader("Authorization"));if (token != null) {try {String sessionId = jwtTokenUtil.getSessionIdFromToken(token);redisSessionService.deleteSession(sessionId);} catch (Exception e) {// token 无效,忽略}}SecurityContextHolder.clearContext();return ResponseEntity.ok(ApiResponse.success("登出成功"));}@PostMapping("/refresh-token")public ResponseEntity<?> refreshToken(HttpServletRequest request) {String token = jwtTokenUtil.extractToken(request.getHeader("Authorization"));if (token == null) {return ResponseEntity.badRequest().body(ApiResponse.error("Token 不存在"));}try {String sessionId = jwtTokenUtil.getSessionIdFromToken(token);UserSession userSession = redisSessionService.getSession(sessionId);if (userSession == null) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("Session 已过期"));}String newToken = jwtTokenUtil.refreshToken(token);userSession.setToken(newToken);redisSessionService.saveSession(userSession);return ResponseEntity.ok(ApiResponse.success("Token 刷新成功", new RefreshTokenResponse(newToken)));} catch (Exception e) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("Token 刷新失败"));}}@GetMapping("/me")public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal UserSession userSession) {if (userSession == null) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error("未登录"));}UserInfo userInfo = new UserInfo(userSession.getUserId(),userSession.getUsername(),userSession.getRoles());return ResponseEntity.ok(ApiResponse.success("获取成功", userInfo));}
}
