基於redis解決Clustered Sessions問題(Spring Session + Redisson)
前言
叢集方式部署伺服器時,當高併發量的請求到達服務端時,服務端通過負載均衡演算法將請求分配到叢集中某個伺服器,那麼同一使用者的多個請求可能被分發到不同的伺服器,如果將session儲存到某個伺服器記憶體中,可能會出現session丟失的情況。
因此在叢集時存在session共享一致性的問題。session複製或者使用hash演算法反向代理存在不足,本篇利用spring-session框架把session儲存到第三方容器(database,redis等)
本地容器選擇redis,客戶端使用redisson。
整合Redisson
jar包
<!--redis客戶端 redisson--> <!-- JDK 1.8+ compatible --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.7.5</version> </dependency> <!--spring session--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>2.0.5.RELEASE</version> </dependency>
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<String, Object> 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 <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
2.我們需要過濾request請求,對session進行包裝
web.xml 中新增過濾器 (spring boot中省略)
<!--spring session-->
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
------------------------------------------------------------------------------------------------------------------------------------------------------
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<S>.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 構成。