🎫 设计一个分布式Session管理方案:通行证的秘密!

31 阅读7分钟

📖 开场:游乐园的手环

想象你去迪士尼游乐园 🎢:

方案1:单机Session(不方便)

入口:给你一个手环 👋
手环上写着:001号游客
    ↓
你玩了过山车 🎢
工作人员:检查手环,记录"001号玩过"
    ↓
你换个门去玩摩天轮 🎡
工作人员:手环001号?没记录 ❌
    ↓
你又要重新登记 💀

问题:
- 每个门各自记录 ❌
- 数据不共享 ❌

方案2:分布式Session(方便)

入口:给你一个手环 👋
手环上写着:001号游客
    ↓
所有数据存储在中央数据库 💾
    ↓
你玩了过山车 🎢
工作人员:查中央数据库,记录"001号玩过"
    ↓
你换个门去玩摩天轮 🎡
工作人员:查中央数据库,能看到"001号玩过过山车"✅
    ↓
无缝衔接 ✅

优点:
- 数据集中存储 ✅
- 所有门都能查到 ✅

这就是分布式Session:统一管理用户会话!


🤔 为什么需要分布式Session?

问题:单机Session的局限 💀

单机Session:
用户登录 → Tomcat1
    ↓
Session存储在Tomcat1的内存
    ↓
下次请求 → Nginx负载均衡 → Tomcat2
    ↓
Tomcat2:没有Session,需要重新登录 ❌

问题:
- Session不共享 ❌
- 用户体验差 ❌

🎯 解决方案

方案1:Session粘性(不推荐)❌

Nginx配置:
ip_hash;

原理:
根据用户IP,固定路由到同一台服务器

缺点:
1. 服务器宕机,Session丢失 ❌
2. 负载不均衡 ❌
3. 扩容困难 ❌

方案2:Session复制(不推荐)❌

Tomcat Session复制:
所有服务器Session互相同步

缺点:
1. 网络开销大 ❌
2. 同步延迟 ❌
3. 内存占用大 ❌

方案3:Redis存储Session(推荐)⭐⭐⭐

原理:
用户登录 → 生成Session
    ↓
Session存储到Redis
    ↓
下次请求 → 任意服务器
    ↓
从Redis读取Session ✅

优点:
- 集中存储 ✅
- 高可用 ✅
- 高性能 ✅

Spring Session + Redis实现

1. 引入依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置application.yml

spring:
  redis:
    host: localhost
    port: 6379
  
  session:
    store-type: redis  # ⭐ Session存储到Redis
    timeout: 1800s  # Session过期时间30分钟
    redis:
      namespace: spring:session  # Redis key前缀

3. 启用Session

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)  // 30分钟
public class SessionConfig {
    // 无需其他配置,Spring Session自动管理
}

4. 使用Session

@RestController
@RequestMapping("/user")
public class UserController {
    
    /**
     * ⭐ 登录(Session存储到Redis)
     */
    @PostMapping("/login")
    public Result<Void> login(@RequestBody LoginRequest request, 
                             HttpSession session) {
        // 1. 校验用户名密码
        User user = userService.login(request.getUsername(), request.getPassword());
        
        if (user == null) {
            return Result.fail("用户名或密码错误");
        }
        
        // ⭐ 2. 用户信息存入Session(自动存储到Redis)
        session.setAttribute("user", user);
        session.setAttribute("userId", user.getId());
        
        return Result.success();
    }
    
    /**
     * ⭐ 获取当前用户(从Session读取)
     */
    @GetMapping("/current")
    public Result<User> getCurrentUser(HttpSession session) {
        // 从Session读取用户信息(自动从Redis读取)
        User user = (User) session.getAttribute("user");
        
        if (user == null) {
            return Result.fail("未登录");
        }
        
        return Result.success(user);
    }
    
    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public Result<Void> logout(HttpSession session) {
        // 销毁Session(自动从Redis删除)
        session.invalidate();
        return Result.success();
    }
}

Redis中的数据

key: spring:session:sessions:1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6
value: (序列化的Session对象)

key: spring:session:expirations:1234567890000
value: (过期时间索引)

方案4:JWT Token(推荐)⭐⭐⭐

原理:
用户登录 → 生成JWT Token
    ↓
Token返回给客户端
    ↓
客户端每次请求携带Token
    ↓
服务器验证Token ✅

优点:
- 无状态(不需要存储Session)✅
- 跨域友好 ✅
- 移动端友好 ✅

JWT实现

1. 引入依赖

<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>

2. JWT工具类

@Component
public class JwtService {
    
    @Value("${jwt.secret}")
    private String secret = "mySecretKey1234567890123456789012";  // 至少32位
    
    @Value("${jwt.expiration}")
    private long expiration = 86400000;  // 24小时(毫秒)
    
    /**
     * ⭐ 生成Token
     */
    public String generateToken(Long userId) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
            .setSubject(String.valueOf(userId))  // 用户ID
            .setIssuedAt(now)  // 签发时间
            .setExpiration(expiryDate)  // 过期时间
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)  // 签名
            .compact();
    }
    
    /**
     * ⭐ 解析Token
     */
    public Long parseToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
            
            return Long.valueOf(claims.getSubject());
            
        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException("Token已过期");
        } catch (Exception e) {
            throw new TokenInvalidException("Token无效");
        }
    }
    
    /**
     * 验证Token是否有效
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

3. 登录接口

@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private JwtService jwtService;
    
    @Autowired
    private UserService userService;
    
    /**
     * ⭐ 登录(返回JWT Token)
     */
    @PostMapping("/login")
    public Result<LoginResponse> login(@RequestBody LoginRequest request) {
        // 1. 校验用户名密码
        User user = userService.login(request.getUsername(), request.getPassword());
        
        if (user == null) {
            return Result.fail("用户名或密码错误");
        }
        
        // ⭐ 2. 生成Token
        String token = jwtService.generateToken(user.getId());
        
        // 3. 返回Token
        LoginResponse response = new LoginResponse();
        response.setToken(token);
        response.setUser(user);
        
        return Result.success(response);
    }
    
    /**
     * 获取当前用户(从Token解析)
     */
    @GetMapping("/current")
    public Result<User> getCurrentUser(@RequestHeader("Authorization") String token) {
        // 去掉"Bearer "前缀
        token = token.substring(7);
        
        // 解析Token
        Long userId = jwtService.parseToken(token);
        
        // 查询用户信息
        User user = userService.getById(userId);
        
        return Result.success(user);
    }
}

4. 拦截器验证Token

@Component
public class JwtInterceptor implements HandlerInterceptor {
    
    @Autowired
    private JwtService jwtService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        // 1. 获取Token
        String token = request.getHeader("Authorization");
        
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(401);
            return false;
        }
        
        token = token.substring(7);
        
        // 2. 验证Token
        try {
            Long userId = jwtService.parseToken(token);
            
            // ⭐ 3. 将userId放入Request,后续可用
            request.setAttribute("userId", userId);
            
            return true;
            
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }
    }
}

/**
 * 注册拦截器
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private JwtInterceptor jwtInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
            .addPathPatterns("/**")
            .excludePathPatterns("/user/login", "/user/register");  // 白名单
    }
}

方案对比

方案优点缺点推荐
Session粘性简单单点故障、负载不均
Session复制自动同步网络开销大、延迟
Redis Session集中存储、高性能依赖Redis⭐⭐⭐
JWT Token无状态、跨域友好无法主动失效⭐⭐⭐

推荐

  • Web应用:Redis Session
  • 移动端/微服务:JWT Token

🎓 面试题速答

Q1: 分布式Session有哪些方案?

A: 四种方案

  1. Session粘性:ip_hash(不推荐❌)
  2. Session复制:Tomcat同步(不推荐❌)
  3. Redis Session:集中存储(推荐⭐⭐⭐)
  4. JWT Token:无状态(推荐⭐⭐⭐)

Q2: Redis Session如何实现?

A: Spring Session + Redis

<!-- 引入依赖 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
# 配置
spring:
  session:
    store-type: redis
// 使用(无需改代码)
session.setAttribute("user", user);  // 自动存储到Redis

Q3: JWT Token如何使用?

A: 生成 + 验证

// 生成Token
String token = Jwts.builder()
    .setSubject(String.valueOf(userId))
    .setExpiration(expiryDate)
    .signWith(key, SignatureAlgorithm.HS256)
    .compact();

// 验证Token
Claims claims = Jwts.parserBuilder()
    .setSigningKey(key)
    .build()
    .parseClaimsJws(token)
    .getBody();

Long userId = Long.valueOf(claims.getSubject());

Q4: Redis Session vs JWT Token?

A: 场景选择

Redis Session

  • 适合Web应用
  • 可以主动失效(删除Redis key)
  • 需要依赖Redis

JWT Token

  • 适合移动端、微服务
  • 无状态,不需要存储
  • 无法主动失效(只能等过期)

Q5: JWT Token如何刷新?

A: 双Token机制

Access Token:短期(1小时)
Refresh Token:长期(30天)

流程:
1. 登录:返回Access + Refresh
2. 请求:携带Access Token
3. Access过期:用Refresh换取新Access
4. Refresh过期:重新登录

Q6: Session如何设置过期时间?

A: 配置过期时间

# Redis Session
spring:
  session:
    timeout: 1800s  # 30分钟
// JWT Token
private long expiration = 86400000;  // 24小时

🎬 总结

       分布式Session管理方案

┌────────────────────────────────────┐
│ 1. Redis Session ⭐⭐⭐              │
│    - Spring Session集成            │
│    - 集中存储                      │
│    - 高可用                        │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 2. JWT Token ⭐⭐⭐                  │
│    - 无状态                        │
│    - 跨域友好                      │
│    - 移动端友好                    │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 3. 方案选择                        │
│    - Web应用:Redis Session        │
│    - 移动端:JWT Token             │
│    - 微服务:JWT Token             │
└────────────────────────────────────┘

🎉 恭喜你!

你已经完全掌握了分布式Session管理方案!🎊

核心要点

  1. Redis Session:Spring Session集成,透明化
  2. JWT Token:无状态,跨域友好
  3. 方案选择:Web用Redis,移动端用JWT

下次面试,这样回答

"分布式Session管理有两种推荐方案:Redis Session和JWT Token。

Redis Session使用Spring Session实现。引入spring-session-data-redis依赖,配置store-type为redis,无需修改代码,原有的session.setAttribute会自动存储到Redis。Session在Redis中的key格式为'spring:session:sessions:sessionId',value是序列化的Session对象。配置过期时间如30分钟,Redis会自动清理过期Session。

JWT Token是无状态方案。用户登录后,服务器生成JWT Token返回给客户端。Token包含用户ID、签发时间、过期时间,使用HMAC SHA256算法签名。客户端每次请求在Authorization头携带Token,服务器验证签名和过期时间,解析出用户ID。JWT的优点是无状态,不需要服务器存储Session,适合微服务和移动端。缺点是无法主动失效,只能等待过期。

两种方案的选择:Web应用推荐Redis Session,因为可以主动失效Session,且Spring Session集成透明,无需修改代码。移动端和微服务推荐JWT Token,因为无状态,服务器不需要存储,适合分布式架构。实际项目中,我们PC端用Redis Session,App端用JWT Token,既保证了易用性,又满足了移动端需求。"

面试官:👍 "很好!你对分布式Session的设计理解很深刻!"


本文完 🎬

上一篇: 222-设计一个地理位置附近的人功能.md
下一篇: 224-设计一个全链路压测系统.md

作者注:写完这篇,我觉得Session管理太重要了!🎫
如果这篇文章对你有帮助,请给我一个Star⭐!