1、概述
实现Session共享是构建分布式Web应用时的一个重要需求,尤其是在水平扩展和高可用性要求较高的场景下。
在分布式服务或集群服务中往往会出现这样一个问题:用户登录A服务后可以正常访问A服务中的接口。但是我们知道,分布式服务通常都是有多个微服务一起构建形成的。如果后续请求转发到了B服务,B服务后端没有这个用户的Session信息,就会强制让用户重新登录,导致业务无法顺利完成。因此,就需要将Session进行共享,保证每个系统都能获取用户的Session状态。
Redis可以用来存储Web应用的用户会话信息,支持快速的会话管理和跨服务器的会话共享。
2、常见实现session共享的方式
(1)、客户端存储(使用Cookie)
实现方式
- 原理:将Session数据直接存储在客户端的Cookie中,每次请求时,浏览器会自动将Cookie发送给服务器。
优点:
- 无服务器状态:服务器不需要存储Session数据,减少了服务器的内存占用和状态管理开销。
- 易于扩展:由于服务器无状态,可以轻松地进行水平扩展,无需担心Session同步或共享问题。
- 简单实现:实现相对简单,适合小型应用或临时解决方案。
缺点:
- 安全性低:Cookie中的Session数据容易被篡改或窃取,存在安全风险。即使使用加密,也无法完全防止攻击(如XSS攻击)。
- 可靠性差:如果用户的浏览器禁用了Cookie,或者用户手动清除了Cookie,Session信息将会丢失。
- 大小限制:Cookie的最大大小为4KB,无法存储较大的Session数据。
- 跨域问题:Cookie只能在同源域名下有效,跨域时需要额外处理。
适用场景:
- 轻量级应用:适合对安全性要求不高、Session数据较少且不需要持久化的应用场景。
- 单页应用(SPA):对于前后端分离的单页应用,可以结合JWT(JSON Web Token)来实现无状态的身份验证。
代码示例:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@RestController
public class CookieSessionController {
private static final String SESSION_COOKIE_NAME = "sessionId";
@GetMapping("/set-cookie")
public String setCookie(HttpServletResponse response) {
// 生成唯一的Session ID
String sessionId = UUID.randomUUID().toString();
// 创建Cookie并设置有效期为1小时
Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
cookie.setPath("/");
cookie.setMaxAge(60 60); // 1小时
// 将Cookie添加到响应中
response.addCookie(cookie);
return "Cookie set with session ID: " + sessionId;
}
@GetMapping("/get-cookie")
public String getCookie(HttpServletRequest request) {
// 从请求中获取Cookie
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(SESSION_COOKIE_NAME)) {
return "Session ID from Cookie: " + cookie.getValue();
}
}
}
return "No session ID found in Cookie.";
}
}
说明:
通过set-cookie的方法,将必要的用户信息保存到response中,返回给浏览器。浏览器会自动将response中这些需要设置cookie的信息保存到cookie中,之后对相同的域进行请求,就会自动携带这些cookie信息。
(2)、Session绑定(Nginx IP绑定策略)
实现方式
通过Nginx的ip_hash指令,将来自同一IP的请求始终路由到同一台后端服务器。这样可以确保用户的Session信息保存在同一台服务器上。
优点:
- 简单实现:配置简单,只需在Nginx中设置ip_hash即可。
- 性能较好:由于Session数据直接存储在本地内存中,访问速度较快。
缺点:
- 单点故障:如果某一台服务器宕机,该服务器上的Session信息将会丢失,导致用户需要重新登录或重新创建Session。
- 负载不均衡:由于同一个IP的请求总是路由到同一台服务器,可能会导致某些服务器的负载过高,而其他服务器闲置。
- 不适合动态IP:如果用户的IP地址频繁变化(如移动设备),可能会导致Session丢失或错误的Session分配。
- 扩展性差:随着用户数量的增加,单台服务器的压力会越来越大,难以进行水平扩展。
适用场景:
- 小型应用:适合用户量较小、服务器数量有限的应用场景。
- 内部系统:对于内部系统或企业内部网,用户的IP地址相对固定,可以考虑使用这种方式。
示例:(nginx配置)
http {
upstream backend {
ip_hash; 使用IP哈希算法,确保同一IP的请求总是路由到同一台服务器
server 192.168.1.1:8080;
server 192.168.1.2:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
说明:
nginx配置ip_hash后,每一次经过nginx代理的请求。nginx都会将请求的ip代理到固定的服务Ip上(实际是请求ip通过哈希算法映射的结果)。这种方法保证了来自同一IP的请求始终会路由到同一台后端服务器上。
(3)、Session同步(Tomcat内置Session同步)
实现方式
- Tomcat提供了内置的Session复制机制,可以通过集群中的多台服务器之间同步Session数据。
常见的同步方式包括:- 内存复制:每台服务器将Session数据复制到其他服务器的内存中。
- Delta复制:只复制Session中发生变化的部分,减少同步的数据量。
- 文件存储:将Session数据写入共享文件系统,所有服务器从该文件系统读取Session数据。
优点:
- 高可用性:即使某一台服务器宕机,其他服务器仍然可以访问到用户的Session信息,避免了Session丢失。
- 自动同步:无需额外开发,Tomcat内置了Session同步功能,配置相对简单。
缺点:
- 同步延迟:Session同步可能会产生一定的延迟,尤其是在网络状况不佳或Session数据较大时,影响用户体验。
- 性能开销:每次请求都会触发Session同步操作,增加了服务器之间的通信开销,降低了整体性能。
- 扩展性有限:随着集群规模的扩大,Session同步的复杂性和开销也会增加,可能导致性能瓶颈。
- 资源浪费:所有服务器都需要存储相同的Session数据,占用了大量的内存资源。
适用场景:
- 中小型应用:适合用户量适中、服务器数量不多的应用场景。
- 对Session同步要求不高的应用:如果Session数据更新频率较低,且对同步延迟不敏感,可以考虑使用这种方式。
(4)、Session共享(Redis等缓存中间件)
实现方式
- 将Session数据存储在独立的缓存中间件(如Redis、Memcached)中,所有服务器都可以通过访问该中间件来获取和更新Session数据。常见的实现方式包括:
- Redis作为Session存储:将Session数据以键值对的形式存储在Redis中,使用唯一的Session ID作为键。
- Spring Session:Spring框架提供了Spring Session模块,可以轻松集成Redis作为Session存储。
优点:
- 高可用性:Session数据集中存储在Redis中,所有服务器都可以访问,避免了单点故障。即使某一台服务器宕机,其他服务器仍然可以继续使用用户的Session。
- 高性能:Redis是基于内存的NoSQL数据库,具有极高的读写性能,能够快速响应Session请求。
- 易于扩展:Redis支持集群模式,可以根据需要水平扩展,满足大规模应用的需求。
- 灵活性强:可以结合Redis的过期时间、持久化等功能,灵活管理Session的生命周期。
- 减轻服务器压力:Session数据不再存储在服务器内存中,减少了服务器的内存占用,提升了服务器的性能。
缺点:
- 依赖外部服务:需要额外部署和维护Redis等缓存中间件,增加了系统的复杂性。
- 网络延迟:虽然Redis的性能很高,但仍然存在网络延迟,尤其是在跨机房或跨区域部署时,可能会影响Session的读取速度。
- 数据一致性问题:如果Redis集群出现故障或网络分区,可能会导致Session数据的一致性问题。
适用场景:
- 大型分布式应用:适合用户量大、服务器数量多的分布式应用,尤其是需要高可用性和水平扩展的场景。
- 高并发应用:适合对性能要求较高的应用,Redis的高效读写能力可以应对大量并发请求。
- 微服务架构:在微服务架构中,多个服务实例可以共享同一个Redis实例,方便统一管理Session。
Redis实现session共享示例代码
第一步:导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
第二步:配置文件
spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=
spring.redis.database=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=500
spring.session.store-type=redis #使用Redis作为Session存储
spring.session.timeout=1800s #Session超时时间
第三步:配置类
需要注解启用RedisSession。同时配置Redis的序列化方式。
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;
@Configuration
@EnableRedisHttpSession // 启用Redis session
public class SessionConfig {
// 可以自定义session id解析器,这里我们使用header解析器
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化器为 StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 设置值的序列化器为 Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
第四步:测试类
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/session")
public class SessionController {
@GetMapping("/set")
@ResponseBody
public String setSessionAttribute(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("username", "John Doe");
return "Session attribute 'username' set to 'John Doe'.";
}
@GetMapping("/get")
@ResponseBody
public String getSessionAttribute(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
String username = (String) session.getAttribute("username");
return "Session attribute 'username': " + username;
} else {
return "No session found.";
}
}
}
第五步:测试验证
当调用get方法后,查看Redis可以发现已经存在session信息了。
3、四种实现session共享方案对比
如果使用分布式系统,用户量大,服务器数量多,且对高可用性和性能有较高要求*,强烈推荐使用Session共享(Redis等缓存中间件)*。Redis的高性能和可扩展性使其成为最合适的方案,尤其是在微服务架构中。
进一步优化:
- 结合JWT:对于无状态的应用,可以考虑使用JWT(JSON Web Token)来替代传统的Session机制。
- Redis集群:为了提高Redis的可用性和性能,可以使用Redis集群或哨兵模式(Sentinel)。
- Session压缩:如果Session数据较大,可以考虑对Session数据进行压缩后再存储到Redis中。
- TTL设置:合理设置Session的过期时间(TTL),避免长时间未使用的Session占用过多资源。