這裏主要研究tomcat中session的管理方式以及sessionId的原理,下文將研究sessionid存到redis中以及基於redis實現session共享。php
平時也就是了解session是基於cookie實現的,cookie是保存在客戶端,而session是保存在服務端,對其原來也沒有深刻理解。下面將深刻理解。html
對Tomcat而言,Session是一塊在服務器開闢的內存空間,其內部的有一個ConcurrentHashMap,咱們作setAttribute和removeAttribute的時候都操做的是此map。(補充一句,request對象的setAttribute也操做的是內部的一個Map)java
public class StandardSession implements HttpSession, Session, Serializable { private static final long serialVersionUID = 1L; protected static final boolean STRICT_SERVLET_COMPLIANCE; protected static final boolean ACTIVITY_CHECK; protected static final boolean LAST_ACCESS_AT_START; protected static final String[] EMPTY_ARRAY; protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap(); ... }
Http協議是一種無狀態協議,即每次服務端接收到客戶端的請求時,都是一個全新的請求,服務器並不知道客戶端的歷史請求記錄;linux
Session的主要目的就是爲了彌補Http的無狀態特性。簡單的說,就是服務器能夠利用session存儲客戶端在同一個會話期間的一些操做記錄;nginx
Session中有一個ConcurrentMap,咱們向Session中setAttribute和removeAttribute的時候操做的是此map。web
先看兩個問題,以下:
一、服務器如何判斷客戶端發送過來的請求是屬於同一個會話?redis
答:用Session id區分,Session id相同的即認爲是同一個會話,在Tomcat中Session id用JSESSIONID表示;sql
二、服務器、客戶端如何獲取Session id?Session id在其之間是如何傳輸的呢?數據庫
答:服務器第一次接收到請求時,開闢了一塊Session空間(建立了Session對象),同時生成一個Session id,並經過響應頭的Set-Cookie:「JSESSIONID=XXXXXXX」命令,向客戶端發送要求設置cookie的響應;apache
客戶端收到響應後,在本機客戶端設置了一個JSESSIONID=XXXXXXX的cookie信息,該cookie的過時時間爲瀏覽器會話結束;
接下來客戶端每次向同一個網站發送請求時,請求頭都會帶上該cookie信息(包含Session id);
而後,服務器經過讀取請求頭中的Cookie信息,獲取名稱爲JSESSIONID的值,獲得這次請求的Session id;
ps:服務器只會在客戶端第一次請求響應的時候,在響應頭上添加Set-Cookie:「JSESSIONID=XXXXXXX」信息,接下來在同一個會話的第二第三次響應頭裏,是不會添加Set-Cookie:「JSESSIONID=XXXXXXX」信息的;
而客戶端是會在每次請求頭的cookie中帶上JSESSIONID信息;
咱們知道request.getSession(boolean create) 獲取session,並根據參數動態的獲取session,若是傳的參數是true的話不存在session就建立一個並返回一個session;若是傳false,不存在session也不會建立,以下代碼:
package com.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TestServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger LOGGER = LoggerFactory.getLogger(TestServlet.class); public TestServlet() { LOGGER.info("call servlet constructor!"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(true);// 傳true會建立一個並返回,false不會建立 PrintWriter writer = response.getWriter(); if (session == null) { writer.write("null"); } else { writer.write(session.toString()); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
查看request.etSession(boolean create)的源碼並分析:
不帶參數的getSession()裏面調用的是getSession(true)方法。
public HttpSession getSession() { return this.getSession(true); } public HttpSession getSession(boolean create) { if (this.crossContext) { if (this.context == null) { return null; } else if (this.session != null && this.session.isValid()) { return this.session.getSession(); } else { HttpSession other = super.getSession(false); if (create && other == null) { other = super.getSession(true); } if (other != null) { Session localSession = null; try { localSession = this.context.getManager().findSession(other.getId()); if (localSession != null && !localSession.isValid()) { localSession = null; } } catch (IOException arg4) { ; } if (localSession == null && create) { localSession = this.context.getManager().createSession(other.getId()); } if (localSession != null) { localSession.access(); this.session = localSession; return this.session.getSession(); } } return null; } } else { return super.getSession(create); } }
上述結構圖:
分析上面源碼:
(1)當前的session存在而且有效(根據session的過時時間以及內部的一些屬性進行判斷)的話返回session
代碼:
else if (this.session != null && this.session.isValid()) { return this.session.getSession(); }
查看this.session.getSession()的源碼:(返回真正的session的代碼)
下面是StandardSession中的代碼:
public HttpSession getSession() { if(this.facade == null) { if(SecurityUtil.isPackageProtectionEnabled()) { this.facade = (StandardSessionFacade)AccessController.doPrivileged(new 1(this, this)); } else { this.facade = new StandardSessionFacade(this); } } return this.facade; }
看到代碼是初始化了一個facade(門面)而且返回去。facade又是什麼?
protected transient StandardSessionFacade facade = null;
是一個實現HttpSession接口的類
package org.apache.catalina.session; import java.util.Enumeration; import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionContext; import org.apache.catalina.session.StandardSession; public class StandardSessionFacade implements HttpSession { private HttpSession session = null; public StandardSessionFacade(StandardSession session) { this.session = session; ... }
(2)接下來研究session不存在的時候session建立而且返回的過程:
建立session對象,session的建立是調用了ManagerBase的createSession方法來實現的
@Override public Session createSession(String sessionId) { if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) { rejectedSessions++; throw new TooManyActiveSessionsException( sm.getString("managerBase.createSession.ise"), maxActiveSessions); } // Recycle or create a Session instance Session session = createEmptySession(); // Initialize the properties of the new session and return it session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60); String id = sessionId; if (id == null) { id = generateSessionId(); } session.setId(id); sessionCounter++; SessionTiming timing = new SessionTiming(session.getCreationTime(), 0); synchronized (sessionCreationTiming) { sessionCreationTiming.add(timing); sessionCreationTiming.poll(); } return (session); }
Session管理器定義:Session管理器組件負責管理Session對象,例如,建立和銷燬Session對象。
首先看一張Session管理器的類繼承結構圖:
簡述:下面依次總結下每一個類(參考官網信息):
(1) Manager:定義了關聯到某一個容器的用來管理session池的基本接口。
(2) ManagerBase:實現了Manager接口,該類提供了Session管理器的常見功能的實現。
(3) StandardManager:繼承自ManagerBase,tomcat的默認Session管理器(不指定配置,默認使用這 個),是tomcat處理session的非集羣實現(也就說是單機版的),tomcat關閉(必須是正常關閉,調用shutdown.bat)時,內存session信息會持久化到磁盤保存爲 SESSION.ser,再次啓動時恢復。
(4) PersistentManagerBase:繼承自ManagerBase,實現了和定義了session管理器持久化的基礎功能。
(5) PersistentManager:繼承自PersistentManagerBase,主要實現的功能是會把空閒的會話對象(經過設定超時時間)交換到磁盤上。
(6) ClusterManager:實現了Manager接口,經過類名應該能猜到,這個就是管理集羣session的管理器和上面那個 StandardManager單機版的session管理器是相對的概念。這個類定義類集羣間session的複製共享接口。
(7) ClusterManagerBase:實現了ClusterManager接口,繼承自ManagerBase。該類實現了session複製的基本操做。
(8) BackupManager:繼承自ClusterManagerBase, 集羣間session複製策略的一種實現,會話數據只有一個備份節點,這個備份節點的位置集羣中全部節點均可見。這種設計使它有個優點就是支持異構部署。
(9) DeltaManager:繼承自ClusterManagerBase,集羣建session複製策略的一種實現,和BackupManager不一樣的是,會話數據會複製到集羣中全部的成員節點,這也就要求集羣中全部節點必須同構,必須部署相同的應用。
補充:下面再具體總結一點就是在PersistentManagerBase類中有個成員變量Store:
持久化session管理器的存儲策略就是有這個Store對象定義的,這個Store的類繼承結構以下:
簡述:接口Store及其實例是爲session管理器提供了一套存儲策略,store定義了基本的接口,而StoreBase提供了基本的實現。 其中FileStore類實現的策略是將session存儲在以setDirectory()指定目錄並以.session結尾的文件中的。 JDBCStore類是將Session經過JDBC存入數據庫中,所以須要使用JDBCStore,須要分別調用setDriverName()方法和 setConnectionURL()方法來設置驅動程序名稱和鏈接URL。
補充:Tomcat session相關的配置
從兩個層面總結一下session相關的配置和設置。首先是從配置文件層面,session是有過時時間的,這個默認的過時時間是 在$catalina_home/conf/web.xml有定義的。具體的默認配置以下(默認的過時時間是30min,即30min沒有訪 問,session就過時了):
Tomcat7.x默認這個manager的配置是註釋掉的。conf/context.xml文件:
若是要指定的PersistentManager爲默認管理器的話能夠修改:
正常關閉與開啓以後會生成一個持久化的文件:(這裏的正常關閉指的是經過startup.bat和shutdown.bat或者linux對應的sh文件進行的操做)
其實看到這也就發現了,其實session管理器或者Store存儲策略,只要實現了相關的接口,都是能夠自定義的。本身寫一個配置在這裏就ok了。
session會話在單臺服務器下不會出現共享問題,如今應用部署方式都是分佈式,或者集羣部署,這樣必然會面臨一個問題,session共享。其主要方式也有如下幾種:
1.web服務器的粘性請求,好比採用 nginx 請求分發,使用ip_hash這種負載均衡方式,客戶端請求只會被分發到相同的後臺server,這樣能夠避免session共享的問題。可是缺點也很明顯,若是一臺服務器宕機了session會丟失。
2.基於數據庫存儲(網站用戶量大的狀況下,頻繁dml數據,對db壓力大)
3.基於cookie存儲(安全問題、雖然能夠加密存儲、可是我以爲永遠不能將敏感數據放在客戶端,不信任啊O(∩_∩)O哈哈~)
4.服務器內置的session複製域(好比was下提供session複製功能、但這個損耗服務器內存)
5.基於nosql(memcache、redis均可以)
最簡單的就是1和5,下面研究基於1和5的session共享。
關於nginx的安裝以及同一臺機器運行多個tomcat參考我以前的文章。
在這裏簡單部署一個簡單的JSP頁面查看session的值與tomcat的目錄:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> jsessionid=${pageContext.session.id} <br /> <%=request.getRealPath("/")%> </body> </html>
單獨啓動兩個tomcat部署以後查看效果:
(1)研究簡單的nginx集羣==與session共享無關,只是實現負載均衡
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 84;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / { proxy_connect_timeout 3; proxy_send_timeout 30; proxy_read_timeout 30; proxy_pass http://clustername; }
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
#集羣配置:服務器列表 upstream clustername { server 127.0.0.1:85 weight=1;#服務器配置 weight是權重的意思,權重越大,分配的機率越大。 server 127.0.0.1:86 weight=1;#服務器配置 weight是權重的意思,權重越大,分配的機率越大。 }
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
(2)nginx的ip_hash實現根據ip分配到指定的服務器:(一種簡單的session共享)
只須要將上面的 權重分配改成 ip_hash便可,以下:
#集羣配置:服務器列表
upstream clustername {
ip_hash;
server 127.0.0.1:85;#服務器配置
server 127.0.0.1:86;#服務器配置
}
參考個人另外一篇博客。http://www.javashuo.com/article/p-dkyogdhl-bb.html