SpringSession介绍

生产环境我们的应用示例不可能是单节点部署, 通常都是多结点部署, 结点上层会进行域映射, 实例之间负载响应请求. 比如常见的Nginx + Tomcat负载均衡场景中。常用的均衡算法有IP_Hash、轮训、根据权重、随机等。不管对于哪一种负载均衡算法,由于Nginx对不同的请求分发到某一个Tomcat,Tomcat在运行的时候分别是不同的容器里,因此会出现session不同步或者丢失的问题。

解决方案

IP_HASH

nginx可以根据客户端IP进行负载均衡,在upstream里设置ip_hash,就可以针对同一个C类地址段中的客户端选择同一个后端服务器,除非那个后端服务器宕了才会换一个. 这样如果该类QPS高会导致该台服务器的负载升高,负载不均.

nginx基于ip_hash的session管理方案

通过容器插件

在容器层面扩展可共享存储的插件; 比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jetty-session-redis等等。好处是对项目来说是透明的,无需改动代码。该方案由于过于依赖容器,一旦容器升级或者更换意味着又得从新来过。并且代码不在项目中,对开发者来说维护也是个问题。

会话管理工具

自己写一套会话管理的工具类,包括Session管理和Cookie管理,在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到Redis中。很显然这个方案灵活性最大,但开发需要一些额外的时间。并且系统中存在两套Session方案,很容易弄错而导致取不到数据。

开源解决方案

这里以开源框架Spring-Session为例,Spring-Session扩展了Servlet的会话管理(所有的request都会经过SessionRepositoryFilter,而 SessionRepositoryFilter是一个优先级最高的javax.servlet.Filter,它使用了一个SessionRepositoryRequestWrapper类接管了Http Session的创建和管理工作),既不依赖容器,又不需要改动代码. 可插拔, 轻量级. 支持多维度存储;诸如 Redis 、Pivotal GemFire、Jdbc、Mongo 、Hazelcast等

SpringSession应用

SpringMvc项目使用SpringSession

maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>

配置web.xml

1
2
3
4
5
6
7
8
<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>
</filter-mapping>

配置redis、以及redisHttpSession存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

<!-- 将session放入redis -->
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="10" />
</bean>

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="hostName" value="127.0.0.1"/>
<property name="port" value="6379"/>
<property name="password" value="" />
<property name="timeout" value="3000"/>
<property name="usePool" value="true"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
</bean>

<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>

</beans>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RestController
@RequestMapping("/api/cloud")
public class ApiSessionController extends BaseMultiController {

private static final Logger LOG = LoggerFactory.getLogger(ApiSessionController.class);

@Autowired
protected HttpSession httpSession;

@GetMapping("/session/put")
public APIResult sessionPut(){
httpSession.setAttribute("cloud", JSON.toJSONString(new User("Elonsu", "123456")));
String userString = (String)httpSession.getAttribute("cloud");
LOG.info("[session][set]:" + userString);
return APIResult.success(true);
}

@GetMapping("/session/get")
public APIResult sessionGet(){
String userString = (String)httpSession.getAttribute("cloud");
LOG.info("[session][get]:" + userString);
User user = JSONObject.parseObject(userString, User.class);
return APIResult.success(user);
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class User implements Serializable {
private String username;
private String password;
}

}

测试输出

启两个容器实例,端口分别使用8080和8081进行访问

实例1访问结果

实例2访问结果

查看redis中存储的session

应用SpringSession

maven依赖

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
</dependencies>

启动主类增加注解

启动类上增加注解@EnableRedisHttpSession

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@SpringBootApplication
@EnableAutoConfiguration
@EnableRedisHttpSession
public class Application extends WebMvcStrap {

protected final static Logger LOG = LoggerFactory.getLogger(Application.class);

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

应用配置文件配置

应用配置文件application.properties 增加如下配置

1
2
3
4
5
6
7
# spring redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
# spring session
spring.session.store-type=redis
server.session.timeout=5

官方文档