分布式Session会话实现方案

一、什么是分布式 Session?

分布式 Session 是一种在微服务架构中,保证用户会话(Session)数据能够在多个独立的、无状态的服务实例之间共享和保持一致的机制。以下是传统单体架构中的 Session与微服务架构中的 Session之间的对比:

  • 传统单体架构中的 Session:

    • 单体架构 Session:应用部署在一台服务器上。用户登录后,服务器会在本地内存中创建一个 Session 对象,存储用户ID、用户名等数据,并给浏览器返回一个唯一的 Session ID(通常保存在 Cookie 中)。浏览器后续的每次请求都会带上这个 Session ID,服务器根据它找到对应的 Session 数据。一切都很简单,因为数据就在本地。
  • 微服务架构中的 Session:

    • 服务是无状态的:会有很多个微服务(用户服务、订单服务、商品服务......),它们可以独立部署、扩缩容。
    • 请求是分布式的:用户的一个请求(比如"查看我的订单")可能会先经过网关,然后被路由到订单服务的实例 A,而这个请求又需要调用用户服务的实例 B 来验证用户信息。

二、为什么需要分布式 Session?

在传统的单体应用架构中,Session 通常由应用服务器的内存(如 Tomcat 的 Session)管理。用户的多次请求都会路由到同一台服务器,可以轻松地存取 Session 数据。

然而,在微服务架构下,服务被拆分为多个独立的、可水平扩展的实例。这就带来了问题:

  1. 无状态负载均衡:用户的两次请求可能会被路由到不同的服务实例上。
  2. 内存隔离:每个服务实例都有自己的内存空间。如果 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 灵活。
  • 工作流程:
    1. 用户登录后,服务端生成一个全局唯一的 Session ID。
    2. 将 Session 数据序列化后存入 Redis(Key 是 Session ID,Value 是 Session 数据)。
    3. 将 Session ID 通过 Cookie 返回给客户端。
    4. 客户端后续请求携带此 Session ID。
    5. 任何微服务实例收到请求后,都使用这个 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 项目依赖:
xml 复制代码
<!-- 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:
yaml 复制代码
spring:
  session:
    store-type: redis # 指定存储类型为 Redis
    timeout: 30m     # Session 过期时间,默认30分钟
  redis:
    host: localhost
    port: 6379
    # 如果有密码,在此配置 password: your-password
4.1.3 登录控制器(设置 Session):
java 复制代码
@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. 将用户信息存入 Session
        session.setAttribute("CURRENT_USER", user);
        // 也可以存其他任何需要跨请求共享的数据
        session.setAttribute("LOGIN_TIME", Instant.now());

        return ResponseEntity.ok("Login successful");
    }
}
4.1.4 业务控制器(读取 Session):
java 复制代码
@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 添加依赖
xml 复制代码
<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 配置文件
yaml 复制代码
# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

jwt:
  secret: mySecretKeyForJWTTokenGenerationInSpringBootApplication2024
  expiration: 7200000  # 2小时
  header: Authorization
  prefix: "Bearer "
4.2.3 创建JWT工具类
java 复制代码
// 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;
    
    // 生成 token
    public 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 中获取 sessionId
    public 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());
    }
    
    // 验证 token
    public Boolean validateToken(String token, UserSession userSession) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userSession.getUsername()) && !isTokenExpired(token));
    }
    
    // 刷新 token
    public 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 实体类
java 复制代码
// 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 存储服务
java 复制代码
// RedisSessionService.java
@Service
public class RedisSessionService {
    
    private static final String SESSION_KEY_PREFIX = "session:";
    private static final long SESSION_TIMEOUT = 7200L; // 2小时
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 保存 session
    public void saveSession(UserSession userSession) {
        String key = SESSION_KEY_PREFIX + userSession.getSessionId();
        redisTemplate.opsForValue().set(key, userSession, Duration.ofSeconds(SESSION_TIMEOUT));
    }
    
    // 获取 session
    public 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;
    }
    
    // 删除 session
    public 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 认证过滤器
java 复制代码
// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private RedisSessionService redisSessionService;
    
    @Override
    protected 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));
                    
                    // 设置到 SecurityContext
                    SecurityContextHolder.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 配置
java 复制代码
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public 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();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
4.2.8 Redis 配置
java 复制代码
// RedisConfig.java
@Configuration
public class RedisConfig {
    
    @Bean
    public 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 认证控制器
java 复制代码
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private RedisSessionService redisSessionService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    // 模拟用户数据
    private Map<String, User> userDatabase = new HashMap<>();
    
    @PostConstruct
    public 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("用户名或密码错误"));
        }
        
        // 创建 session
        UserSession userSession = new UserSession(
                user.getId(), 
                user.getUsername(), 
                user.getRoles()
        );
        
        // 生成 token
        String token = jwtTokenUtil.generateToken(userSession);
        userSession.setToken(token);
        
        // 保存 session 到 Redis
        redisSessionService.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));
    }
}
相关推荐
抛物线.5 小时前
Nerve:分布式基础设施智能管理平台的设计与实现
分布式
YC运维6 小时前
Kafka 全方位技术文档
分布式·kafka
harmful_sheep6 小时前
Kafka的概念
分布式·kafka
晨陌y7 小时前
从 0 到 1 开发 Rust 分布式日志服务:高吞吐设计 + 存储优化,支撑千万级日志采集
开发语言·分布式·rust
小码过河.8 小时前
Rabbitmq扇形队列取消绑定交换机之后任然接收消息问题
分布式·rabbitmq·ruby
tang7778913 小时前
如何利用代理 IP 构建分布式爬虫系统架构?
分布式·爬虫·tcp/ip
xiaoopin18 小时前
简单的分布式锁 SpringBoot Redisson‌
spring boot·分布式·后端
想ai抽1 天前
pulsar与kafka的架构原理异同点
分布式·架构·kafka
异构算力老群群1 天前
纠删码(erasure coding,EC)技术现状
分布式·纠删码·lrc