1、使用場景java
1)一臺服務器上的軟負載均衡應用mysql
2)分佈式應用web
2、實現方式redis
1)session數據存cookiespring
將session存儲至cookie中,每次請求從cookie中讀取session,缺點:不安全,大小有限制sql
2)粘性sessionexpress
粘性session是指Ngnix每次都將同一用戶的全部請求轉發至同一臺服務器上,即將用戶與服務器綁定,缺點:某臺服務器不可用時,獲取不到session數據瀏覽器
3)session複製安全
每次session發生變化時,建立或者修改,就廣播給全部集羣中的服務器,使全部的服務器上的session相同,缺點:副本數據都同樣,數據冗餘,佔用空間服務器
4)session共享
使用redis、mysql等存儲session
3、使用配置
1)pom.xml引入jar包
2)web.xml配置filter
3)application.xml啓用spring-session
4、流程圖
步驟:
1)請求被filter過濾器攔截,其實是被SessionRepositoryFilter攔截器處理
2)生成request、response包裝類,後續操做中跟request、response相關的操做都是調用包裝類的方法
3)業務代碼中調用request.getSession()時,實際調用的是SessionRepositoryRequestWrapper類的方法
4)SessionRepositoryRequestWrapper的getSession()方法會獲取當前域中的cookie,獲取sessionID
5)根據sessionID到redis中查找與之對應的RedisSession對象
6)當無RedisSession返回時,建立RedisSession對象,以後調用setAttribute和getAttribute方法時,分別是往對象中到map存放和獲取值
7)將RedisSession對象放入request中,供後續使用,如SessionRepositoryFilter$SessionRepositoryRequestWrapper. commitSession()
8)將數據保存至redis,實際上保存到是RedisSession對象中的delta屬性,該屬性的數據類型爲Map,對應的redis數據結構爲hash
9)將cookie寫入瀏覽器,cookie包括sessionID
5、源碼解析
1)加載SessionRepositoryFilter過濾器
web.xml的過濾器爲DelegatingFilterProxy,過濾器實現了InitializingBean接口,故會調用afterPropertiesSet()方法,最終對應的是SessionRepositoryFilter
爲何最後對應的filter是SessionRepositoryFilter?因爲DelegatingFilterProxy類中的targetBeanName值爲springSessionRepositoryFilter,而initDelegate()方法是在spring容器中找到id爲springSessionRepositoryFilter的對象即爲filter的具體實現類。回到application.xml文件,該文件中有一行配置:
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
查看RedisHttpSessionConfiguration源碼發現其父類有個方法,方法中使用了@Bean註解,該註解相似<bean />,id默認爲方法名稱,方法參數默認依賴spring容器中id爲參數名稱的對象,故該代碼最後會往spring容器中注入SessionRepositoryFilter對象,id=springSessionRepositoryFilter,注入的SessionRepositoryFilter對象,且參數sessionRepository依賴spring容器中的id=sessionRepository對象,具體代碼以下:
@Bean public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) { sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy) this.httpSessionStrategy); } else { sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy); } return sessionRepositoryFilter; }
2)加載RedisOperationsSessionRepository
在application.xml中有以下配置,查看RedisHttpSessionConfiguration源碼,發現Spring容器中id=springSessionRepositoryFilter的類,即爲SessionRepositoryFilter,且默認的session持久化容器爲redis
圖中的springSessionRepositoryFilter()方法表示初始化SessionRepositoryFilter對象,注入到spring容器中,且id=springSessionRepositoryFilter,其中方法參數sessionRepository表示依賴spring容器中id=sessionRepository的對象。從圖中sessionRepository()方法可知,注入spring容器的id=sessionRepository的對象爲RedisOperationsSessionRepository,即默認的session持久化到redis中
3)生成request&response包裝對象
spring-session 的核心思想是對HttpServletRequest和HttpServletResonse進行包裝,後續全部操做request、response的方法均調用包裝對象的方法,生成包裝對象是在SessionRepositoryFilter中進行,通過濾器處理後,controller方法中的HttpServletRequest和HttpServletResponse對象均爲SessionRepositoryRequestWrapper和MultiSessionHttpServletResponse,具體代碼以下:
4)request.getSession()解析
1。獲取sessionID
當調用request.getSession()方法時,會從cookie中獲取sessionID,代碼以下:
2。根據sessionID查找Session對象
獲取到sessionID且值不爲空,則須要到redis中查找與之對應的session對象
loadSession方法定義在RedisOperationsSessionRepository類中,目的是爲了將redis中鍵爲spring:session:sessions:{sessionId}到hash結構的屬性值複製到MapSession中,而生成的MapSession對象作爲RedisSession類的構造函數的參數,也就是說在RedisSession對象中保存了MapSession對象的引用,能夠直接操做RedisSession對象獲取redis保存的屬性值,具體代碼以下:
private MapSession loadSession(String id, Map<Object, Object> entries) { MapSession loaded = new MapSession(id); for (Map.Entry<Object, Object> entry : entries.entrySet()) { String key = (String) entry.getKey(); if (CREATION_TIME_ATTR.equals(key)) { loaded.setCreationTime((Long) entry.getValue()); } else if (MAX_INACTIVE_ATTR.equals(key)) { loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue()); } else if (LAST_ACCESSED_ATTR.equals(key)) { loaded.setLastAccessedTime((Long) entry.getValue()); } else if (key.startsWith(SESSION_ATTR_PREFIX)) { loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue()); } } return loaded; }
3。建立Session對象
獲取不到sessionID或者值爲空時,則須要建立Session對象
當調用request.getSession.setAttribute(name,value)時,實際是往RedisSession對象中的delta屬性中設值,具體代碼在RedisOperationsSessionRepository中,以下:
public void setAttribute(String attributeName, Object attributeValue) { this.cached.setAttribute(attributeName, attributeValue); this.putAndFlush(getSessionAttrNameKey(attributeName), attributeValue); } private void putAndFlush(String a, Object v) { this.delta.put(a, v); this.flushImmediateIfNecessary(); }
4。Session對象保存至Redis
經過以上步驟獲取或建立Session對象後,以後就是將Session對象保存到redis中。而保存操做是在SessionRepositoryFilter類的doFilterInternal方法的finally中執行
五、定時任務清理過時key
雖然redis自帶了過時key的清理,但採用可是按期刪除+懶性刪除方式,若是併發量比較大的時候,redis會存在不少無效key,形成內容浪費。鑑於redis清理key方式的弊端,spring-session開啓了一個定時任務,定時清理redis中過時的key,其具體思路是取得當前時間的時間戳(精確到分)做爲 key,去 redis 中定位到 spring:session:expirations:{當前時間戳} ,這個 set 裏面存放的即是全部過時的 key 。具體實現以下:
org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanupExpiredSessions
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions
public void cleanExpiredSessions() { long now = System.currentTimeMillis(); // 獲取當前時間戳對應的分 long prevMin = roundDownMinute(now); if (logger.isDebugEnabled()) { logger.debug("Cleaning up sessions expiring at " + new Date(prevMin)); } // 獲取到spring:session:expirations:{當前時間戳-精確到分} String expirationKey = getExpirationKey(prevMin); // 取出當前這一分鐘應當過時的session Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 刪除spring:session:expirations:{當前時間戳-精確到分}鍵,不是刪除session自己 this.redis.delete(expirationKey); // 遍歷這一分鐘要過時的session for (Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); // 訪問session touch(sessionKey); } } private void touch(String key) { // 並非刪除key,而只是訪問key this.redis.hasKey(key); }
6、客戶端禁用cookie
當瀏覽器禁用cookie後,是沒法獲取到cookie數據的,也就是說沒法獲取到jsessionID,獲取不到jessionID則沒法得到對應的session對象。爲了解決這個問題,能夠對URL重寫,使用response.encodeURL(url)便可。重寫URL的目的是在url後面加上jsessonID,這樣便能在請求中獲取到jsessionID,進一步得到對應的session對象。測試了一下,當集成spring-session後,使用response.encodeURL(url)重寫URL時,是不會在url後面加上jsessionID參數,這或許是設計spring-session時,就必需要求不能禁用cookie。