分布式之session共享问题 4种解决方案及spring session的使用
session在分布式环境下存在的问题由于HTTP协议是无状态的,在开发中我们可以将用户的信息存储在服务器的session中,并生成与之相对应的JSESSIONID通过cookie返回给浏览器。浏览器下次访问,cookie会自动携带上次请求存储的数据(JSESSIONID)到服务器中,服务器根据JSESSIONID找到对应的session,从而获取用户的信息。该机制在单体应用中是没有问题的,但是如
session在分布式环境下存在的问题
由于HTTP协议是无状态的,在开发中我们可以将用户的信息存储在服务器的session中,并生成与之相对应的JSESSIONID通过cookie返回给浏览器。浏览器下次访问,cookie会自动携带上次请求存储的数据(JSESSIONID)到服务器中,服务器根据JSESSIONID找到对应的session,从而获取用户的信息。
该机制在单体应用中是没有问题的,但是如果在分布式环境下,会产生session共享问题,即session的数据在服务1中存在,但是在服务2中不存在。
就会出现下面的问题:
假设用户第一次访问的是会员服务1,会员服务1将用户的信息记录在自己的session中,但是当用户第二次访问的是会员服务2时,就会找不到用户信息
session共享解决方案
session复制
服务器将自己的session数据传送给其他服务器,使得每个服务器都拥有全量的数据。
优点:tomcat原生支持,只需要修改配置文件即可
缺点:
- session同步需要数据传输,会占用大量带宽,降低服务器集群的业务处理能力
- 任意一台web-server保存的都是所有web-server的session总和,浪费了大量的空间,且受内存限制无法水平扩展更多的web-server
- 大型分布式集群情况下,由于所有web-server都要全量保存数据,所以此方案不可取。
客户端存储
用户的信息不再保存在服务器中,而是保存在客户端(浏览器)中。
优点:服务器不需要保存用户信息,节省服务器资源
缺点:
- 每次http请求,携带用户在cookie中的完整信息,浪费网络带宽
- 用户信息存放在cookie中,cookie有长度4k限制,不能存放大量信息
- 用户信息存储在cookie中,存在泄漏、篡改、窃取等安全隐患
一般情况下不会使用这种方案。
hash一致性
nginx负载均衡的时候采用ip-hash策略,这样同一个客户端每次的请求都会被同一个服务器处理。
优点:
- 只需要修改nginx配置,不需要修改应用程序代码
- 可以支持web-server水平扩展
缺点:
- session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
- 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确session。
但以上缺点其实问题不大,因为session本来也是有有效期的,所以这个方案也经常被采用。
统一存储
jsessionid这个cookie默认是系统域名。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案。
将用户的信息存储在第三方中间件上,做到统一存储,如redis中,所有的服务都到redis中获取用户信息,从而实现session共享。
优点
- 没有安全隐患
- 可以水平扩展
- 服务器重启或扩容都不会造成session的丢失
不足:
- 增加了一次网络调用,速度有所下降
- 需要修改应用程序代码,如将所有的getSession方法替换为Redis查数据的方式。但这个问题可以通过spring session完美解决
整合spring session
现在我们知道,我们可以将session的信息存储在第三方数据库中,比如redis。但如果是我们自己去写这个逻辑的话太过麻烦。而spring session可以很简单的帮助我们实现这个功能。
官网地址:spring session官网地址
1、添加依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2、配置文件添加配置
spring.session.store-type=redis
3、配置redis连接
spring.redis.host=localhost # Redis server host.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.
4、使用json序列化机制
@Configuration
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericFastJsonRedisSerializer();
}
}
spring session默认是jdk序列化机制,要求类需要实现Serializable接口,序列化后是二进制,人看不懂。使用json序列化机制就没有这些问题。
5、在springboot启动类中添加@EnableRedisHttpSession
注解
这样就OK了
扩展
session不能跨不同域名共享
当我们的认证微服务以及其他微服务使用的是俩个不同的域名时,即使使用了spring session也会存在不同域名的共享问题。
比如,认证服务的域名为auth.fcpmall.com,订单服务的域名为
order.fcpmall.com,这种情况下,即使在认证服务登录成功,将用户的信息保存在redis中,订单服务也无法查询到。
session不能跨不同域名共享的原因
先回顾一下正常的session流程:
- session依赖于cookie的,服务器会将JSESSIONID放到cookie中,并返回给服务器。
- 浏览器下次访问时,携带的cookie信息中含有JSESSIONID,所以服务器可以根据JSESSIONID找到对应的session
在不同域名下会发生什么?
- 首先你需要知道浏览器在发送http请求时,只会携带domain为当前域名以及父域名cookie信息。也就是从order.fcpmall.com发出的http请求只会携带domain为order.fcpmall.com和fcpmall.com的域名信息。
- 浏览器在设置域名的时候默认使用的是当前的域名。即认证服务的JSESSIONID会被保存在domain为auth.fcpmall.com的cookie中
- 综上,订单服务在发送请求的时候,没有携带含有JSESSIONID的cookie信息。所以找不到对应的session信息
知道了原因后,解决请来就很简单了,只需要在设置cookie的时候,指定domain为父域名fcpmall.com即可。
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainName("fcpmall.com");
return serializer;
}
}
这篇博客的知识点总结:
脑图链接地址
下面的部分由于我水平有限,写得不太好,所以选看即可
spring session核心原理
为什么spring session可以在不修改应用程序代码的前提下,将getSession方法替换为Redis查询数据的方式?
原理很简单,在我们添加@EnableRedisHttpSession
注解的时候,它会为我们创建一个名为springSessionRepositoryFilter
的bean,这个bean实现了Filter
接口,在过滤器中将原先的HttpSession
替换掉了,采用了装饰者模式。
下面进行初浅的源码分析(源码这一块虽然我现在还很弱,源码也很难读,但我认为这一块还是必要去锻炼的,所以慢慢来吧)
//在EnableRedisHttpSession中导入RedisHttpSessionConfiguration配置类
@Import({RedisHttpSessionConfiguration.class})
@Configuration(
proxyBeanMethods = false
)
public @interface EnableRedisHttpSession{
...
}
@Configuration(
proxyBeanMethods = false
)
//RedisHttpSessionConfiguration继承SpringHttpSessionConfiguration
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
//注入sessionRepository,用来对redis进行增删操作的类
@Bean
public RedisIndexedSessionRepository sessionRepository() {
。。。
}
...
}
//看看SpringHttpSessionConfiguration做了些什么
@Configuration(
proxyBeanMethods = false
)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
//在容器中注入SessionRepositoryFilter,该类继承了Filter(关键)
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
...
}
//这个过滤器中实现了狸猫换太子
@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
//注意了,传入下一个过滤器的request和response已经被换成了wrappedRequest,wrappedResponse。这里使用了装饰者模式
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
wrappedRequest.commitSession();
}
}
}
到这里知道了,当我们使用spring session的时候,在经过spring session过滤器的时候HttpServletRequest
已经被换成了SessionRepositoryResponseWrapper
,接下来我们就看一下这个类对getSession动了什么手脚。
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
if (currentSession != null) {
return currentSession;
} else {
//获取session
S requestedSession = this.getRequestedSession();
.....
}
private S getRequestedSession() {
if (!this.requestedSessionCached) {
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
Iterator var2 = sessionIds.iterator();
while(var2.hasNext()) {
String sessionId = (String)var2.next();
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
//从sessionRepository获取session
S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
}
还记得前面RedisHttpSessionConfiguration
配置的RedisIndexedSessionRepository
吗?被spring session狸猫换太子后,我们后面对HttpSession的操作其实都是由这个类完成的。也就是说对session的增删操作实际上已经换成了对redis的增删操作了。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)