原创

用Spring-Session实现Redis应用集群下的session共享

1. 应用集群

我们知道位于应用层的服务器为了应对高并发的访问请求,且提高网站的伸缩性,会通过负载均衡设备将一组服务器组成一个集群共同对外提供服务。并在监测到某台应用服务器不可用时,就将其从集群中剔除,从而实现应用的高可用:

集群下的应用服务器有共同的特点,即服务无状态,指的是应用服务器不保存业务的上下文信息,而仅根据每次请求提交数据进行相应的业务逻辑处理。

这种特性导致了集群中的应用色号中甚至无法记录某用户客户端的登录状态,因为如果某用户在其中一台服务器上登录之后,在下次访问其他接口时转发到了其他的应用服务器,将丢失登录状态,这种状态上下文对象称之为Session。我们都熟悉,在单机情况下,由Tomcat的Web容器来负责管理Session;在集群的情况下,有以下的几种Session共享方式:

  • 使用Nginx中的ip_hash来将同一IP源的客户端始终转发到同一服务器,保证session总能在该台服务器上获取;
  • 将Session记录在客户端,即用户浏览器的Cookie当中;
  • 利用独立部署的Session服务器来统一管理Session;
    其他的两种方式都略有缺陷,这里我们介绍通过Spring-Session与Redis实现Session共享的统一化管理。

    2. Session共享

    2.1 模拟集群环境
    首先我们来模拟应用集群的环境,以便后边测试Session共享。我们创建两个Spring-Boot项目,并将两个项目分别部署在8080和8081端口。为了区分是不同的服务器处理的逻辑,我们在两个项目的Controller中返回不同的内容:

      // server 1
      @GetMapping("")
      public String main() {
          return "This is Server 1";
      }
    
      // server 2
      @GetMapping("")
      public String main() {
          return "This is Server 2";
      }
    

    修改本地的Nginx配置文件,通过轮询的方式(除此之外,还可以根据权重进行分发)配置集群:

    http {
      upstream tomcat {
          server localhost:8080;  # server 1
          server localhost:8081;  # server 2
      }
    
      server {
          listen       80;
          server_name  localhost;
          location / {
              proxy_pass http://tomcat;
          }
    }
    

    我们将两个项目启动并重启Nginx后访问http://localhost ,会发现每次访问都会由不同的服务器提供响应:

    这样集群的环境就搭建好了,接下来要做的是用Spring-Session来实现Session的共享。

    2.2 配置SpringSession
    我们先为两个项目添加相关的库依赖:

    <!--spring-session-data-redis-->
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
      <version>2.1.3.RELEASE</version>
    </dependency>
    <!--lettuce-core-->
    <dependency>
      <groupId>io.lettuce</groupId>
      <artifactId>lettuce-core</artifactId>
      <version>5.1.4.RELEASE</version>
    </dependency>
    

    然后创建SessionConfig配置类,通过@EnableRedisHttpSession让Spring-Session创建springSessionRepositoryFilter,该过滤器实现了Filter接口,负责替换Tomcat内置的HttpSession。并在connectionFactory方法中配置连接的Redis服务器信息:

    @Configuration
    @EnableRedisHttpSession
    public class SessionConfig {
    
      @Bean
      public LettuceConnectionFactory connectionFactory() {
          RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
          configuration.setHostName("localhost");
          configuration.setPort(6379);
          return new LettuceConnectionFactory(configuration);
      }
    
      /**
       * 让Spring Session不再执行config命令
       */
      @Bean
      public static ConfigureRedisAction configureRedisAction() {
          return ConfigureRedisAction.NO_OP;
      }
    
    }
    

    为了保证springSessionRepositoryFilter过滤器在每个请求中都能正常工作,我们还需要加载SessionConfig配置类,让Servlet容器能在每个请求都使用该过滤器:

    public class Initializer extends AbstractHttpSessionApplicationInitializer {
    
      public Initializer() {
          super(SessionConfig.class);
      }
    
    }
    

    这样Spring-Session就配置好了。使用SpringSession的好处是,我们甚至不需要修改单机上的逻辑代码,因为Spring-Session替换了Tomcat自带的HttpSession对象,因此和在单机上操作Session是一样的:

    @GetMapping("")
    public String demo(HttpServletRequest request) {
      HttpSession session = request.getSession();
      ...
    }
    

    2.3 测试
    我们为两个应用项目同时添加一个登录接口和一个获取个人信息的接口的相同逻辑,但是在返回内容中区分是由不同应用处理的响应。

    @GetMapping("/login")
    public String login(HttpServletRequest request, @RequestParam String name) {
      request.getSession().setAttribute("curUser", name);
      return "【Server 1】success";
    }
    
    @GetMapping("/users/me")
    public String getMyInfo(HttpServletRequest request) {
      System.out.println("【server 1】session id => " + request.getSession().getId());
      Object attr = request.getSession().getAttribute("curUser");
      if (attr == null) {
          return "请登录";
      } else {
          return "【Server 1】" + String.valueOf(attr);
      }
    }
    

    重启服务器后再访问http://locahost ,如下图测试流程我们可以看到,在Server2服务器登录之后,不论是Server1还是Server2应用都能够获取到当前客户端保存的Session信息:

    而之所以可以达到Session共享的功能,是因为SpringSession将Session对象的信息都存储在了Redis服务器中:

正文到此结束
本文目录