应用服务器集群是当前互联网模式下解决高并发的常用手段,当一台应用服务器的处理能力不足时,不要企图更换配置更高的服务器,对于大型网站而言,不管多么强大的服务器,都满足不了持续增长的业务需求,在这种情况下,更好的做法是增加多台配置较低的应用服务器去分担原来服务器的压力,因为这样可以在较低成本的情况下,使系统的可扩展和可伸缩性更好。
多台应用服务器共同对外提供服务,本质上是为了避免当某一台应用服务器宕机或者故障不能对外提供服务时候,对客户端保持透明。在B/S应用中,服务器通过Session跟踪浏览器回话行为,为了达到这个目的就有了Session共享的问题。
由于需要多台应用服务器对外提供服务并且需要对外界保持透明,那就需要能够使得用户请求可以自动的分发至不同的应用服务器,针对这种场景,诞生了以F5为代表的硬件负载均衡设备和以LVS,Ngnix为代表的软负载软件。它们都能够根据请求流量,将用户请求分发至不同的应用服务器当中。本文主要针对一些使用软负载进行Session共享的场景。
Spring官方对Spring-Session的解决方案目前主要以下几种方式,其原理大体相同,主要都是通过将Session存储进行集中化,只是存储的途径不同,目前主要有以下几种:
Spring Redis
Spring JDBC
Spring MongoDB
Spring Hazelcast
本文主要介绍基于Redis和JDBC为例进行说明和演示用法,其它方式可参考官方文档。
application.properties配置文件
spring.session.store-type=redis #保存方式
server.servlet.session.timeout=60s #session超时时间
spring.session.redis.flush-mode=ON_SAVE #session更新策略,有ON_SAVE、IMMEDIATE 前者是SessionRepository save方法的时候刷新,后者是任何更新机会就刷新
spring.session.redis.namespace=spring:session:babyfinger #redis中key的namespace prefix
spring.redis.database=0
spring.redis.port=6379
spring.redis.host=192.168.105.11
``
@RestController
public class BabyFingerController {
@RequestMapping
@ResponseStatus(HttpStatus.OK)
public String sayHello(HttpSession httpSession , String greeting){
String storedName = (String) httpSession.getAttribute("greeting");
if (storedName == null) {
httpSession.setAttribute("greeting", greeting);
}
return "say: " + greeting ;
}
}
@SpringBootApplication
//@EnableRedisHttpSession 改配置意义和 spring.session.store-type=redis 相同,保留其一即可
public class DistributionSessionApplication {
public static void main(String[] args) {
SpringApplication.run(DistributionSessionApplication.class, args);
}
}
通过浏览器访问:
可以看到redis中已经存储了访问的 回话信息。对应的session 为一个hash值存储,可以看到里边对应的键值对信息。如果通过Spring-Security配置了用户登录行为,那么我们在用户登录成功后的SecurityContextHolder.getContext().setAuthentication(Authentication authentication) 也会存储在该Session中了。
在经过一分钟之后,我们再去访问,发现Session已经超时,此时将不能再获取到Session中的数据。
application.properties配置文件更改如下,其中platform可以根据数据的类型进行选择,官方内置的建表语句可在jar包中查到。
server.servlet.session.timeout= 60s
spring.session.jdbc.initialize-schema=ALWAYS
spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema.
spring.session.jdbc.table-name=SPRING_SESSION # Name of the database table used to store sessions.
以Spring内置的H2数据库为例[如有配置问题,可参考 [参考1] [[参考2] [参考3]] ,主要有两张表,一张用于存储每一个客户端来的Session请求,另一个存放Session的属性信息。
CREATE TABLE SPRING_SESSION (
PRIMARY_ID CHAR(36) NOT NULL,
SESSION_ID CHAR(36) NOT NULL,
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
EXPIRY_TIME BIGINT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);
CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_PRIMARY_ID CHAR(36) NOT NULL,
ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);
同样的我们也可以在我们的SpringApplication中添加 @EnableJdbcHttpSession 注解来代替配置文件实现session在数据库中的存储。配置后的访问效果如下图所示:
1 浏览器在首次访问服务器时,服务器端会生成一个SessionId,放入Cookie中返回给浏览器,这样同一个回话请求在下次请求时候就会在请求头部中附带该SessionId在Cookie头部给服务器端那为何 浏览器中 SessionId为 ZmU3MjgwNTItMWM2Mi00NTQ4LWFiOTgtMzA1ZWE5MWE4YTA2,而数据库/reids中记录的id为一个 uuid fe728052-1c62-4548-ab98-305ea91a8a06呢? 其实不难发现base64.decode(ZmU3MjgwNTItMWM2Mi00NTQ4LWFiOTgtMzA1ZWE5MWE4YTA2) = fe728052-1c62-4548-ab98-305ea91a8a06
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
该包中其实包含的类并不多,其中最关键的就是 JdbcOperationsSessionRepository (redis共享的话是RedisOperationsSessionRepository)类,该类提供了操作Session数据库表的主要方法,我们在 调用 httpSession.setAttribution(key,value)的时候,其实调用的是 HttpSessionAdapter.setAttribution(key,value) , 而该方法内部又会调用到JdbcOperationsSessionRepository 的setAttribution(key,value) 方法。JdbcOperationsSessionRepository 内部有一个 JDBCSession的内部类,该类的构造器中 将 primaryKey设置为 uuid,在初次生成SessionId返回给客户端的时候,会将SessionId进行base64.encode,在收到请求的时候又会将其进行 decode. (具体可以通过SessionRepositoryFilter 进行入手跟踪代码)
Spring Session的原理不是本文的主要介绍对象,感兴趣可以参考[参考1] [参考2]。
本文实例代码可以通过 github进行下载:
https://github.com/zc0604