在Web应用开发中,会话管理一直是个绕不开的话题。传统单机部署时,我们通常直接使用Servlet容器提供的HttpSession就能满足需求。但随着业务规模扩大,系统不得不走向分布式架构,这时会话管理就面临几个棘手问题:
我在实际项目中遇到过这样一个案例:某电商平台大促期间,由于会话服务器内存不足导致频繁Full GC,最终引发连锁反应使整个集群崩溃。这迫使我们寻找更可靠的会话管理方案。
Redis作为内存数据库具有几个关键优势:
与Memcached相比,Redis的持久化能力和丰富的数据类型使其更适合会话存储场景。以下是关键指标对比:
| 特性 | Redis | Memcached |
|---|---|---|
| 持久化 | 支持 | 不支持 |
| 数据结构 | 丰富 | 仅Key-Value |
| 集群模式 | 原生支持 | 需要客户端实现 |
| 读写性能 | 微秒级 | 微秒级 |
Spring Session通过过滤器机制实现了对HttpSession的透明替换。核心组件包括:
典型的工作流程如下:
java复制// 伪代码展示核心流程
public void doFilter(ServletRequest request, ServletResponse response) {
// 1. 包装原生Request/Response
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request);
// 2. 通过SessionRepository操作会话
Session session = sessionRepository.findById(sessionId);
// 3. 将会话绑定到当前线程
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(wrappedRequest));
try {
chain.doFilter(wrappedRequest, response);
} finally {
// 4. 提交会话变更
sessionRepository.save(session);
}
}
首先在pom.xml中添加必要依赖:
xml复制<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后配置application.yml:
yaml复制spring:
session:
store-type: redis
timeout: 1800 # 会话超时时间(秒)
redis:
host: 127.0.0.1
port: 6379
password: yourpassword
默认使用JDK序列化存在两个问题:
推荐改用JSON序列化:
java复制@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
在多应用共享Redis时,建议为每个应用设置独立命名空间:
yaml复制spring:
session:
redis:
namespace: 'myapp:sessions'
这样存储在Redis中的key会变成myapp:sessions:sessionId的格式,避免键冲突。
对于生产环境,建议采用Redis Cluster方案。配置示例:
yaml复制spring:
redis:
cluster:
nodes:
- 192.168.1.101:6379
- 192.168.1.102:6379
- 192.168.1.103:6379
max-redirects: 3 # 最大重定向次数
重要提示:Redis Cluster要求所有节点使用相同密码,且不支持跨slot的多key操作
通过实现SessionAttributeMapper接口可以控制哪些属性需要持久化:
java复制public class CustomSessionAttributeMapper implements SessionAttributeMapper {
@Override
public Map<String, Object> mapAttributes(Set<String> attributeNames,
Session session) {
Map<String, Object> filtered = new HashMap<>();
// 只保留必要的属性
if (attributeNames.contains("userInfo")) {
filtered.put("userInfo", session.getAttribute("userInfo"));
}
return filtered;
}
}
对于读多写少的场景,可以配置读写分离:
yaml复制spring:
redis:
read-timeout: 1000
lettuce:
pool:
max-active: 8
max-idle: 8
read-from: REPLICA_PREFERRED # 优先从副本读取
对于高频访问的会话属性,可以引入本地缓存:
java复制@Controller
public class UserController {
@GetMapping("/profile")
public String profile(HttpSession session) {
UserInfo user = (UserInfo) session.getAttribute("userInfo");
if (user == null) {
user = loadFromRedis(session.getId());
session.setAttribute("userInfo", user); // 存入本地会话
}
return "profile";
}
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 会话频繁失效 | Redis内存不足触发逐出 | 增加maxmemory或优化数据 |
| 登录后跳转回登录页 | 跨域导致Cookie丢失 | 配置sameSite和domain属性 |
| 会话属性丢失 | 序列化异常 | 统一序列化方案 |
| 性能突然下降 | Redis连接数耗尽 | 调整连接池参数 |
建议监控以下关键指标:
Spring Boot Actuator提供了现成的端点:
yaml复制management:
endpoints:
web:
exposure:
include: 'sessions,redis'
为避免会话方案变更导致全站故障,建议采用分阶段上线:
具体实现示例:
java复制@Bean
public SessionRepository<?> sessionRepository(
@Value("${session.strategy}") String strategy) {
if ("hybrid".equals(strategy)) {
return new HybridSessionRepository(redisTemplate, localSessionStore);
}
return new RedisOperationsSessionRepository(redisTemplate);
}
Spring Session默认会更换会话ID,但还需额外配置:
java复制@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
HeaderHttpSessionIdResolver resolver = new HeaderHttpSessionIdResolver();
resolver.setChangeSessionIdOnAuthentication(true);
return resolver;
}
对特别敏感的属性建议单独加密:
java复制public class EncryptedSessionRepository implements SessionRepository {
private final CryptoService crypto;
public void save(Session session) {
Map<String, Object> attrs = new HashMap<>();
attrs.put("token", crypto.encrypt(session.getAttribute("token")));
// 其他处理...
}
}
推荐的安全配置组合:
yaml复制server:
servlet:
session:
cookie:
http-only: true
secure: true
same-site: lax
实现方案示例:
java复制public class UnifiedSessionRepository implements SessionRepository {
public Session createSession() {
// 生成跨平台会话ID
String sessionId = "web:" + UUID.randomUUID();
return new MapSession(sessionId);
}
}
监听会话事件实现业务逻辑:
java复制@EventListener
public void handleSessionCreated(SessionCreatedEvent event) {
String sessionId = event.getSessionId();
auditService.logLogin(sessionId);
}
编写迁移工具确保平滑过渡:
bash复制# 使用redis-cli批量迁移
redis-cli -h old_host --scan --pattern "session:*" | while read key; do
redis-cli -h old_host --raw dump $key | head -c-1 | redis-cli -h new_host -x restore $key 0
done
经过多个项目的实战验证,这套方案在万级QPS场景下平均延迟控制在5ms以内,会话丢失率低于0.001%。关键在于根据业务特点合理配置超时时间和选择合适的序列化方案。对于特别敏感的场景,建议增加本地缓存层作为降级方案。