集群方式部署服务器时,当高并发量的请求到达服务端时,服务端通过负载均衡算法将请求分配到集群中某个服务器,那么同一用户的多个请求可能被分发到不同的服务器,如果将session保存到某个服务器内存中,可能会出现session丢失的情况。
因此在集群时存在session共享一致性的问题。session复制或者使用hash算法反向代理存在不足,本篇利用spring-session框架把session储存到第三方容器(database,redis等)
本地容器选择redis,客户端使用redisson。
jar包
org.redisson
redisson
3.7.5
org.springframework.session
spring-session-data-redis
2.0.5.RELEASE
1.我们需要做的事很简单,只需要配置redis客户端并使用@EnableRedissonHttpSession注解。RedissonHttpSessionConfiguration类中实现了RedissonSessionRepository的注入,它用来操作session存储值。
RedissonSessionConfig配置类。
@EnableRedissonHttpSession
public class RedissonSessionConfig {
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://192.168.56.129:7000", "redis://192.168.56.129:7001","redis://192.168.56.129:7002");
return Redisson.create(config);
}
}
------------------------------------------------------------------------------------------------------------------------------------------------------
观察EnableRedissonHttpSession注解,通过@Import向容器导入了有@Configuration注解的RedissonHttpSessionConfiguration配置类,其中包含与session存储有关的配置。(4.2版本后@Import支持向spring容器导入没有@Configuration的类)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Import({RedissonHttpSessionConfiguration.class})
@Configuration
public @interface EnableRedissonHttpSession {
int maxInactiveIntervalInSeconds() default 1800;
String keyPrefix() default "";
}
接着观察RedissonHttpSessionConfiguration,实现了ImportWare接口,通过setImportMetadata() 方EnableRedissonHttpSession注解下的两个属性值(maxInactiveIntervalInSeconds,keyPrefix)注入RedissonHttpSessionConfiguration属性,向容器注入了RedissonSessionRepository 一个与session存储有关的容器类,它需要指定RedissonClient (redis客户端)。
@Configuration
public class RedissonHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware {
private Integer maxInactiveIntervalInSeconds;
private String keyPrefix;
public RedissonHttpSessionConfiguration() {
}
@Bean
public RedissonSessionRepository sessionRepository(RedissonClient redissonClient, ApplicationEventPublisher eventPublisher) {
RedissonSessionRepository repository = new RedissonSessionRepository(redissonClient, eventPublisher);
if (StringUtils.hasText(this.keyPrefix)) {
repository.setKeyPrefix(this.keyPrefix);
}
repository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds.intValue());
return repository;
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map map = importMetadata.getAnnotationAttributes(EnableRedissonHttpSession.class.getName());
AnnotationAttributes attrs = AnnotationAttributes.fromMap(map);
this.keyPrefix = attrs.getString("keyPrefix");
this.maxInactiveIntervalInSeconds = (Integer)attrs.getNumber("maxInactiveIntervalInSeconds");
}
}
观察父类SpringHttpSessionConfiguration,其中向spring容器中注入了SessionRepositoryFilter的Bean。spring正是通过这个filter用来过滤包装session
@Bean
public SessionRepositoryFilter extends Session> springSessionRepositoryFilter(SessionRepository sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
2.我们需要过滤request请求,对session进行包装
web.xml 中添加过滤器 (spring boot中省略)
springSessionRepositoryFilter
org.springframework.web.filter.DelegatingFilterProxy
springSessionRepositoryFilter
/*
REQUEST
ERROR
------------------------------------------------------------------------------------------------------------------------------------------------------
DelegatingFilterProxy 类是filter的代理类,交给spring去管理filter。如果未指定init-param参数的话,DelegatingFilterProxy就会把filter-name作为要查找的Bean对象的name。这里去spring容器中寻找名字为springSessionRepositoryFilter的bean,得知为SessionRespositoryFilter过滤器。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
boolean hasAlreadyFilteredAttribute = request.getAttribute(this.alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {
filterChain.doFilter(request, response);
} else {
request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);
try {
this.doFilterInternal(httpRequest, httpResponse, filterChain);
} finally {
request.removeAttribute(this.alreadyFilteredAttributeName);
}
}
} else {
throw new ServletException("OncePerRequestFilter just supports HTTP requests");
}
}
观察SessionRespositoryFilter的doFilter()方法,里面调用了doFilterInternal()方法,在doFilterInternal中实现了对session的包装
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
wrappedRequest.commitSession();
}
}
写一个controller类,获得session并设值
@Controller
public class HelloController {
@RequestMapping("/helloWorld")
public String helloWorld(HttpServletRequest request,Model model) throws Exception {
request.getSession().setAttribute("value","this is a test");
return "hello";
}
}
观察redis,以hash结构存储。hash的key为redisson_spring_session:sessionId 构成。
参考 spring-session 2.0.5官方