📖 开场:游乐园的手环
想象你去迪士尼游乐园 🎢:
方案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: 四种方案:
- Session粘性:ip_hash(不推荐❌)
- Session复制:Tomcat同步(不推荐❌)
- Redis Session:集中存储(推荐⭐⭐⭐)
- 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管理方案!🎊
核心要点:
- Redis Session:Spring Session集成,透明化
- JWT Token:无状态,跨域友好
- 方案选择: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⭐!