SpringSession+Redis实现集群会话共享

2,127 阅读10分钟

0) 前言

WEB应用开发完成后部署到Tomcat或其他容器中供用户访问. 小型应用在一台服务器上安装Tomcat并部署WEB应用. 随着访问量增大, Tomcat的压力会越来越大, 直至崩溃. 为了保证WEB应用的承载能力, 需要对WEB应用进行集群处理.

技术发展到今天, 集群/负载均衡已经变的相对简单了. 下面用通俗的语言给刚入门的同学介绍下这两个概念:

某KFC开业时只有一个点餐窗口(一台Tocmat服务器, 可以节约成本)对外提供点餐服务. 应对日常点餐没有问题, 当饭口或者周末时一个窗口就会排起长队(高并发). 不仅顾客有怨言(请求响应时间长, 用户体验差), 服务员也会很累, 终于有一天他累倒了(Tomcat挂掉了).

这时在侧面增加了一个窗口(增加一台Tomcat服务器)提供点餐服务, 但是很多顾客不知道新窗口, 依旧在原有窗口排起了长队(用户依旧访问原来的Tomcat), 这时需要有一个人专门站在门口根据每个窗口的排队情况指引顾客去哪个窗口点餐(负载均衡器). 这个人作用是为了让各个窗口的点餐人数大致相等, 避免有的窗口很忙, 有的很闲. 随着顾客增加, 点餐窗口也会相应增加(Tomcat越来越多).

  • 集群: 一群服务器集合在一起提供服务, 上例中多个点餐窗口(多台Tomcat)共同提供点餐服务就是集群.
  • 负载均衡: 让集群中每个点餐窗口(每个Tomcat)的负载情况保持均衡, 不要出现某一个或几个太空闲.

两个概念是同时出现的, 没有集群的服务(单一Tomcat)也不存在负载均衡之说, 集群的服务没有负载均衡会浪费资源.

WEB负载均衡方案很多, Nginx+Tomcat是常用的方案之一. Nginx作为负载均衡器根据每个Tomcat的负载情况进行分流.

  • 每个Tomcat都相当于点餐窗口, 都可以提供点餐服务
  • 每次要点餐都得先经过Nginx
  • Nginx会根据每个窗口的空闲情况进行分配用户去哪个窗口点餐
  • 第一次在1号窗口点餐, 点完后马上再次点餐, 有可能被分配到2号窗口

下面我们搭建负载均衡的WEB应用

1) 搭建WEB应用

准备WEB应用, 用两个Tomcat部署, 测试时为了能够区分请求是由哪个Tomcat进行处理, 将Tomcat端口号作为结果返回.

/**
 * 获取部署项目的Tomcat端口号
 */
@RequestMapping("/port/get")
@ResponseBody
public String getPort(HttpServletRequest request) {
    return String.valueOf(request.getLocalPort());
}

本例中分别使用5677, 5688两个端口部署该项目. 访问/port/get请求返回结果为Tomcat的端口号

  • http:// localhost:5677/port/get : 返回5677
  • http:// localhost:5688/port/get : 返回5688

2) Nginx配置负载均衡

Window下Nginx安装比较简单, 不会安装的同学自行百度, 简单介绍下Nginx配置文件: nginx.conf

# Nginx进程数
worker_processes  1;
 
events {
    # 最大并发链接数
    worker_connections  1024;
}
 
# Nginx处理HTTP请求相关的配置
# http不能重复, 全局唯一
http {
 
    # 虚拟主机, 可配置多个虚拟主机
    # Nginx监听88,89,90三个端口, 可配置三个server
    server {
        # 端口号, 访问88端口会都按照该server下的配置进行处理
        listen       88;
        # 主机名称
        server_name  localhost;
        # 根据正则表达式匹配URL, 匹配到的URL按照该location下的配置进行处理
        # /代表访问88端口的所有请求
        location / {
            # 静态资源所在根目录, 会从该目录下查找静态资源
            # 例: 访问/a.html, 找到D:/a.html并返回
            root  D:/;
        }
    }
 
}

上述配置文件最基础的Nginx配置, 当我们访问http://localhost:88时会由Nginx处理, 下面我们配置Nginx的负载均衡.

  • 配置1)中定义的两个tomcat, 在http节点下添加如下代码:
# 定义需要进行负载均衡的服务器信息
# upstream为关键字, springsession为自定义的名称
# server为关键字, 代表一个服务或服务(一个tomcat)
# server的内容为服务器的信息, 形式为ip:端口
# weight定义了服务器负载的权重, 每4次请求有3次转发到5688, 1次到5677
upstream springsession { 
    server localhost:5677 weight=1; 
    server localhost:5688 weight=3; 
}
  • 配置当访问Nginx的所有请求转发至两个服务器处理
location / {
    # root  D:/;
    # 转发至名称为springsession的upstream处理
    proxy_pass http://springsession; 
}

3) 测试负载均衡

访问http://localhost:88/port/get, Nginx将请求转发至两台tomcat中的一个进行处理. 可以发现请求返回的结果是不一样的

  • 根据配置的权重, 每4次访问有3次由5688上, 1次由5677处理.
  • 权重配置只是最终平均值为3/4和1/4, 不一定是前三次访问都会由5688处理.
  • 不配置weight时, 一次请求两个tomcat被分配到的概率各占50%

负载均衡配置好了, 有这样一个问题:

你在1号窗口点餐时把钥匙暂存到该窗口, 下次在点餐可能被分配到2号窗口或其他窗口(也有可能分配到1号窗口), 那么在其他窗口取钥匙显然是行不通的. 因为其他窗口没有你的钥匙. 这时你只能祈祷能快速把你分配到1号窗口.

如果保存钥匙的操作变为在SESSION中保存信息, 那么当你的请求被Tomcat1处理时, Tomcat1会为你生成一个SESSION, 你在SESSION里面设置了信息, 下次你的请求被分配到Tomcat2处理, Tomcat2又会为你生成一个SESSION. 这是两个独立的并且不共享的SESSION. 因此你是不可能的在Tomcat2中获取你在Tomcat1中保存的信息.

登录的原理其实就是在SESSION中保存登录状态, 按照上面的分析, 登录在集群部署的服务中就失效了. 在Tomcat1中登录, 下次访问Tomcat2, 此时通过SESSION判断登录状态得到的一定是未登录, 还需要再次登录. 用户疯, 你疯, 老板也会疯...


如果有一个公用位置用来存放东西, 所有的点餐窗口都在公用位置存取顾客物品, 上面的问题就迎刃而解了.

这就是本文重点: 统一管理集群下各WEB应用的SESSION.

  • 容器的选择: 我们需要一个能够统一存放SESSION的容器. 从以下3点分析, Redis无疑是最合适的. SESSION是经常被读取的, 因此数据库, 文件系统均不适合, 最好是从内存操作. SESSION是有ID的, 一个ID对应一个SESSION, 最好是一个K/V的容器 SESSION是有时效性的(时间长不用, 需要删除). 最好能够设置过期时间

  • SESSION存取机制: 由于SESSION是Tomcat生成的, 因此首先想到的是修改Tomcat的SESSION机制, 从Redis中存取SESSION, 这样会带来一个问题, 假设Tocmat升级了, 我们还需要重新对Tomcat进行修改. 因此这个方案可行性较差. 我们可以这样考虑, 即使Tomcat生成了SESSION, 我们也是在WEB应用中使用, 为什么不在WEB应用中重新生成一个SESSION呢, 编写这样一个过滤器, 在进入WEB应用之前, 抛弃Tomcat的SESSION, 从Redis中获取SESSION.


恰巧有这样一个框架帮助我们完成上面的想法, 只需要配置一下即可实现统一管理SESSION. 他就是Spring Session.

为了对Spring Session的功能印象深刻, 我们先来测试一下没有Spring Session时我们的集群应用是怎样处理SESSION的

把我们负载均衡的WEB应用中增加一个控制器方法, 把每次的SESSION ID输出一下.

/**
  * 获取部署项目的SESSION ID
  */
@RequestMapping("/sessionid/get")
@ResponseBody
public String getPort(HttpServletRequest request, HttpSession session) {
    int port = request.getLocalPort(); // 端口
    String sessionId = request.getSession().getId(); // SESSION ID
 
    return "port: " + port + ", session id: " + sessionId;
}

启动项目, 多次访问http://localhost:88/sessionid/get

  • 两次访问都在同一Tomcat下的SESSOIN ID是不变的
  • 两次访问在不同的Tomcat下的SESSION ID是变化的
  • 访问不同的Tomcat后, 再次访问同一Tomcat时SESSION ID也是变化的

出现上述情况的原因如下:

  1. 访问5677, 由于没有SESSION, Tomcat5677生成SESSION, ID为1, 并将1返回客户端
  2. 访问5677, 浏览器携带SESSION_ID=1, Tomcat5677找到对应的SESSION. 因此SESSION_ID为1
  3. 访问5688, 浏览器携带SESSION_ID=1, Tomcat5688找不到对应的SESSION, 重新生成SESSION, ID为2, 并将2返回客户端
  4. 访问5677, 浏览器携带SESSION_ID=2, Tomcat5677找不到对应的SESSION, 重新生成SESSION, ID为3, 并将3返回客户端
  5. 访问5688, 浏览器携带SESSION_ID=3, Tomcat5688找不到对应的SESSION, 重新生成SESSION, ID为4, 并将4返回客户端

4) 统一SESSION管理

下面我们来用Spring Session来管理WEB应用的SESSION

1) 安装Redis并开启

参见文章http://www.cnblogs.com/jaign/articles/7920588.html

2) 添加Spring Session依赖

// Spring Session依赖
"org.springframework.session:spring-session-data-redis:2.0.5.RELEASE",
// Redis依赖
"io.lettuce:lettuce-core:5.0.4.RELEASE"

3) 配置Spring Session过滤器

Web.xml中配置Spring Session提供的过滤器, 该过滤器主要负责将Tomcat生成的SESSION替换成Redis中保存的SESSION.

<!-- Spring Session过滤器 -->
<!-- 负责在进入WEB应用之前将Tomcat生成的SESSION替换为Redis中的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>
</filter-mapping>

4) SpringSession/Redis配置

在Spring配置文件中增加Spring Session配置和Redis配置

beans {
 
    xmlns context: "http://www.springframework.org/schema/context"
 
    // 启动注解方式
    context.'annotation-config'()
 
    // 配置Spring Session
    // 实际上是配置Web.xml中使用的Spring Session过滤器
    // 将Tomcat的Session替换为Redis中管理的Session
    sessionConfig(RedisHttpSessionConfiguration)
 
    // 配置Redis客户端连接
    // 默认连接本地6379端口
    lettuce(LettuceConnectionFactory)
 
}

5) 测试

启动项目, 多次访问http://localhost:88/sessionid/get, 无论如何访问SESSION ID都是一样的.

同时Redis中也出现了当前SESSION的记录.

使用Spring Session后访问集群下的WEB应用时SESSION处理过程:

  1. 访问5677, 由于Redis中没有SESSION, 因此会生成一个SESSION并存入Redis, ID为1, 并将1返回客户端
  2. 访问5677, 浏览器携带SESSION_ID=1, Tomcat5677Redis中找到了SESSION. 因此SESSION_ID1
  3. 访问5688, 浏览器携带SESSION_ID=1, Tomcat5688Redis中找到了SESSION. 因此SESSION_ID1
  4. 清除Redis, 再次访问5677, 由于Redis中没有ID为1SESSION, 因此会重新生成, ID也相应变化了

5) 示例代码

此时我们已经实现了统一管理SESSION, 无论访问任一TOMCAT都可以找到相同的SESSION.

当我们的应用进行集群后, 统一管理SESSION势在必行, 实现统一管理SESSION的方式很多, 本文只是其中一种方式. 重在让同学们理解统一管理SESSION的重要性和他的基本原理.

  • 示例代码地址: https://github.com/atd681/alldemo
  • 示例项目名称: atd681-springsession