WEB应用开发完成后部署到Tomcat或其他容器中供用户访问. 小型应用在一台服务器上安装Tomcat并部署WEB应用. 随着访问量增大, Tomcat的压力会越来越大, 直至崩溃. 为了保证WEB应用的承载能力, 需要对WEB应用进行集群处理.
技术发展到今天, 集群/负载均衡已经变的相对简单了. 下面用通俗的语言给刚入门的同学介绍下这两个概念:
某KFC开业时只有一个点餐窗口(一台Tocmat服务器, 可以节约成本)对外提供点餐服务. 应对日常点餐没有问题, 当饭口或者周末时一个窗口就会排起长队(高并发). 不仅顾客有怨言(请求响应时间长, 用户体验差), 服务员也会很累, 终于有一天他累倒了(Tomcat挂掉了).
这时在侧面增加了一个窗口(增加一台Tomcat服务器)提供点餐服务, 但是很多顾客不知道新窗口, 依旧在原有窗口排起了长队(用户依旧访问原来的Tomcat), 这时需要有一个人专门站在门口根据每个窗口的排队情况指引顾客去哪个窗口点餐(负载均衡器). 这个人作用是为了让各个窗口的点餐人数大致相等, 避免有的窗口很忙, 有的很闲. 随着顾客增加, 点餐窗口也会相应增加(Tomcat越来越多).
两个概念是同时出现的, 没有集群的服务(单一Tomcat)也不存在负载均衡之说, 集群的服务没有负载均衡会浪费资源.
WEB负载均衡方案很多, Nginx
+ Tomcat
是常用的方案之一. Nginx作为负载均衡器根据每个Tomcat的负载情况进行分流.
下面我们搭建负载均衡的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 http:// localhost:5688/port/get
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的负载均衡.
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; } 复制代码
location / { # root D:/; # 转发至名称为springsession的upstream处理 proxy_pass http://springsession; } 复制代码
访问 http://localhost:88/port/get
, Nginx将请求转发至两台tomcat中的一个进行处理. 可以发现请求返回的结果是不一样的
5688
上, 1次由 5677
处理. 5688
处理. 负载均衡配置好了, 有这样一个问题:
你在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
SESSOIN ID SESSION ID SESSION ID
出现上述情况的原因如下:
5677
, 由于没有SESSION, Tomcat5677
生成SESSION, ID为 1
, 并将 1
返回客户端 5677
, 浏览器携带 SESSION_ID=1
, Tomcat5677
找到对应的SESSION. 因此SESSION_ID为 1
5688
, 浏览器携带 SESSION_ID=1
, Tomcat5688
找不到对应的SESSION, 重新生成SESSION, ID为 2
, 并将 2
返回客户端 5677
, 浏览器携带 SESSION_ID=2
, Tomcat5677
找不到对应的SESSION, 重新生成SESSION, ID为 3
, 并将 3
返回客户端 5688
, 浏览器携带 SESSION_ID=3
, Tomcat5688
找不到对应的SESSION, 重新生成SESSION, ID为 4
, 并将 4
返回客户端 下面我们来用 Spring Session
来管理WEB应用的SESSION
参见文章 http://www.cnblogs.com/jaign/articles/7920588.html
// Spring Session依赖 "org.springframework.session:spring-session-data-redis:2.0.5.RELEASE", // Redis依赖 "io.lettuce:lettuce-core:5.0.4.RELEASE" 复制代码
在 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> 复制代码
在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) } 复制代码
启动项目, 多次访问 http://localhost:88/sessionid/get
, 无论如何访问 SESSION ID
都是一样的.
同时 Redis
中也出现了当前SESSION的记录.
使用 Spring Session
后访问集群下的WEB应用时SESSION处理过程:
5677
, 由于 Redis
中没有 SESSION
, 因此会生成一个 SESSION
并存入 Redis
, ID为 1
, 并将 1
返回客户端 5677
, 浏览器携带 SESSION_ID=1
, Tomcat5677
在 Redis
中找到了 SESSION
. 因此 SESSION_ID
为 1
5688
, 浏览器携带 SESSION_ID=1
, Tomcat5688
在 Redis
中找到了 SESSION
. 因此 SESSION_ID
为 1
Redis
, 再次访问 5677
, 由于 Redis
中没有ID为 1
的 SESSION
, 因此会重新生成, ID也相应变化了 此时我们已经实现了统一管理SESSION, 无论访问任一TOMCAT都可以找到相同的SESSION.
当我们的应用进行集群后, 统一管理SESSION势在必行, 实现统一管理SESSION的方式很多, 本文只是其中一种方式. 重在让同学们理解统一管理SESSION的重要性和他的基本原理.
https://github.com/atd681/alldemo atd681-springsession