文章内容輸出來源:拉勾教育Java高薪訓練營
文章目錄
-
-
- 為什麼要實作Session共享
- 實作方案
-
- 1. Nginx的IP_Hash政策
- 2. Tomcat的Session複制
- 3. Session集中存儲
- SpringSession使用示例
-
- 資料庫腳本
- 項目實作步驟
- Tomcat配置步驟
- Nginx配置步驟
- 示範效果
- 項目代碼
-
為什麼要實作Session共享
Http是無狀态的,為了保持使用者的資訊,就需要通過Cookie或者Session來存儲會話資訊。如使用者登入成功後,在Session中存儲使用者資訊,此使用者後續的操作就不用再重複登入。
但當在叢集環境中,同一個站點部署在多台伺服器上,就需要實作Session共享。不然使用者登入時,請求指向A伺服器執行個體,在Session中存儲了會話資訊。但下一次請求可能指向了B伺服器執行個體,B伺服器拿不到使用者之前登入的資訊,又要再登入一次。
實作方案
1. Nginx的IP_Hash政策
-
方案說明
同一IP的請求都路由到同一台伺服器上,即會話粘滞
- 優點
- 配置簡單
- 無代碼侵入
- 缺點
- 伺服器重新開機會導緻Session丢失
- 存在單點故障風險
推薦使用
2. Tomcat的Session複制
-
方案說明
多個Tomcat之間修改配置檔案達到Session複制的目的。這是早期處理的一種方案
- 優點
- 伺服器重新開機不會導緻Session丢失
- 無代碼侵入
- 友善伺服器的水準擴充
- 缺點
- 性能低。存儲的資料越多,性能越低下
- 消耗記憶體
- 複制存在延遲性
不推薦使用的方案
3. Session集中存儲
-
方案說明
Session的本質是緩存,将它交給專業的緩存中間件(如Redis來處理)
- 優點
- 伺服器重新開機不會導緻Session丢失
- 擴充能力強
- 适合大叢集環境的使用
- 缺點
- 代碼侵入性,需要在代碼中實作與Redis的互動
推薦使用。Spring Session使得基于Redis的Session共享應⽤起來⾮常之簡單
SpringSession使用示例
開發一個簡單的SpringBoot項目,實作了登入以及履歷增、删、改、查的功能。頁面直接使用JSP進行實作。
在本機上配置Nginx以及Tomcat模仿叢集環境。配置兩個Tomcat執行個體,将上述的項目打包為war包,部署在兩個Tomcat執行個體中。
資料庫腳本
CREATE DATABASE db_test DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE `tb_resume`(
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '使用者ID',
`name` varchar(200) NOT NULL COMMENT '名稱',
`address` varchar(200) COMMENT '位址',
`phone` varchar(200) COMMENT '手機',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
項目實作步驟
- 建立SpringBoot項目
cluster_demo
- 實作了履歷表
的資料通路,并實作了履歷的增、删、改、查功能(具體詳見項目代碼)t_resume
- 實作簡單的登入功能
- 登入後端請求,實作登入頁面跳轉以及登入請求,登入請求中直接判斷使用者名和密碼為admin即登入成功
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage(HttpServletRequest request) {
Object loginUser = request.getSession().getAttribute("loginUser");
if(null != loginUser) {
return "redirect:resumes/list";
}
return "login";
}
@PostMapping("/login")
@ResponseBody
public Res login(HttpServletRequest request, @RequestBody LoginVO loginVO) {
if(null == loginVO.getName() || loginVO.getName().trim().length() == 0) {
return Res.fail("使用者名不能為空");
}
if(null == loginVO.getPassword() || loginVO.getPassword().trim().length() == 0) {
return Res.fail("密碼不能為空");
}
if(!loginVO.getName().equals("admin") || !loginVO.getPassword().equals("admin")) {
return Res.fail("使用者名或密碼錯誤");
}
//登入成功後,在session中存儲使用者資訊
request.getSession().setAttribute("loginUser", "admin");
return Res.ok("登入成功");
}
}
- 登入攔截器的配置,判斷非登入頁面的請求,判斷使用者是否登入,沒有登入則跳轉到登入頁面
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object loginUser = request.getSession().getAttribute("loginUser");
if(null != loginUser) {
return true;
}
response.sendRedirect("/login");
return false;
}
}
@Configuration
public class LoginAppConfigurer extends WebMvcConfigurerAdapter{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/login/**","/error");
super.addInterceptors(registry);
}
}
- 登入表單的頁面以及登入請求的互動實作
<html>
<body>
<h2>使用者登入</h2>
<div>
<div>
<b>使用者名:</b><input id="name" type="text" placeholder="請輸入使用者名"/>
</div>
<div>
<b>密碼:</b><input id="password" type="password" placeholder="請輸入使用者密碼"/>
</div>
<div>
<input id="btnLogin" type="button" value="登入"/>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript">
$('#btnLogin').on('click', function() {
var name = $('#name').val().trim();
var password = $('#password').val().trim();
if(name.length == 0) {
alert('請輸入使用者名');
return;
}
if(password.length == 0) {
alert('請輸入密碼');
return;
}
var params = {
'name': name,
'password': password
};
$.ajax({
type: 'post',
url: 'login',
data: JSON.stringify(params),
contentType: 'application/json;charset=utf-8',
dataType: 'json',
success: function(result) {
if(result.success) {
window.location.href = 'resumes/list';
}else {
alert(result.data);
}
}
});
});
</script>
</html>
- 開始實作Session共享功能。引入Redis以及SpringSession的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 項目配置檔案
中配置下redis的配置資訊application.properties
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 在
中添加上ClusterApplication
注解,實作Session共享的配置EnableRedisHttpSession
@SpringBootApplication
@EnableCaching
@EnableRedisHttpSession
public class ClusterApplication extends SpringBootServletInitializer {}
- 在
中增加輸出端口号以及SessionIDlogin.jsp
<div>
<div>
<p>伺服器端口:${pageContext.request.localPort}</p>
<p>目前SessionID:${pageContext.session.id}</p>
</div>
</div>
- 使用
打包項目為mvn clean package
demo.war
Tomcat配置步驟
- 建立兩個Tomcat執行個體,分别配置為8080和8081端口
- 将上述打包的
分别放到兩個tomcat的webapps目錄下demo.war
- 修改每個Tomcat執行個體的
配置server.xml
- 在
和Server
修改通路端口為8080或8081(預設為8080端口的話則不用再修改)Connector
<Server port="8006" shutdown="SHUTDOWN">
<Connector port="8081"/>
</Server>
- 在
标簽上增加Host
子标簽,指定相應項目的路徑以及通路路徑。這裡配置如下:Context
<Host name="localhost" appBase="webapps"
deployOnStartup="false" unpackWARs="true" autoDeploy="true">
<Context docBase="demo.war" path="/" />
</Host>
- 啟動tomcat,确認項目通路成功
sh bin/startup.sh
Nginx配置步驟
在Nginx上配置負載均衡政策
- 修改
配置檔案,配置反向代理。此處是監聽80端口,域名是www.yyh.com。配置内容如下:nginx.conf
http{
upstream cluster-server {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name www.yyh.com;
access_log /logs/nginx/cluster.access.log;
location / {
proxy_pass http://cluster-server;
}
}
}
- 修改電腦的hosts檔案,配置上
與本機的映射關系www.yyh.com
127.0.0.1 www.yyh.com
- Nginx執行熱加載
nginx -s reload
示範效果
在浏覽器上通路
http://www.yyh.com
檢視實作效果
- Nginx轉發請求給了
tomcat-8080
- Nginx轉發請求給了
tomcat-8081
從上面可以看到伺服器端口在8080和8081順序變化着,但是目前SessionID的值沒有變化,則實作了Session的共享
- 輸入
進行登入,登入成功後跳轉到履歷清單頁面admin/admin
項目代碼
- cluster_demo項目代碼