對於分佈式應用來講,最開始遇到的問題就是 session 的存儲了,解決方案大體有以下幾種前端
本文內容主要說 spring-session 使用 redis 來存儲 session ,實現原理,修改過時時間,自定義 key 等java
spring-session 對於內部系統來講仍是能夠的,使用方便,但若是用戶量上來了的話,會使 redis 有很大的 session 存儲開銷,不太划算。mysql
使用起來比較簡單,簡單說一下,引包,配置,加註解 。以下面三步,就配置好了使用 redis-sessiongit
<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>
spring.redis.host=localhost # 其它 超時,端口,庫,鏈接池,集羣,就本身去找了
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800)
測試:由於是在 getSession 的時候纔會建立 Session ,因此咱們必須在接口中調用一次才能看到效果redis
@GetMapping("/sessionId") public String sessionId(){ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpSession session = request.getSession(); session.setAttribute("user","sanri"); return session.getId(); }
它的存儲結果以下算法
hash spring:session:sessions:e3d4d84f-cc9f-44d5-9199-463cd9de8272 string spring:session:sessions:expires:e3d4d84f-cc9f-44d5-9199-463cd9de8272 set spring:session:expirations:1577615340000
第一個 hash 結構存儲了 session 的一些基本信息和用戶設置的一些屬性信息spring
creationTime 建立時間sql
lastAccessedTime 最後訪問時間數據庫
maxInactiveInterval 過時時長,默認是 30 分鐘,這裏保存的秒值緩存
sessionAttr:user 這是我經過 session.setAttribute 設置進去的屬性
第二個 string 結構,它沒有值,只有一個 ttl 信息,標識這組 key 還能活多久,能夠用 ttl 查看
第三個 set 結構,保存了因此須要過時的 key
說明:這個實現沒多少難度,我就照着源碼念一遍了,就是一個過濾器的應用而已。
首先從網上了解到,它是使用過濾器來實現把 session 存儲到 redis 的,而後每次請求都是從 redis 拿到 session 的,因此目標就是看它的過濾器是哪一個,是怎麼存儲的,又是怎麼獲取的。
咱們能夠從它惟一的入口 @EnableRedisHttpSession
進入查看,它引入了一個 RedisHttpSessionConfiguration
開啓了一個定時器,繼承自 SpringHttpSessionConfiguration
,能夠留意到 RedisHttpSessionConfiguration
建立一個 Bean RedisOperationsSessionRepository
repository 是倉庫的意思,因此它就是核心類了,用於存儲 session ;那過濾器在哪呢,查看SpringHttpSessionConfiguration
它屬於 spring-session-core 包,這是一個 spring 用來管理 session 的包,是一個抽象的概念,具體的實現由 spring-session-data-redis 來完成 ,那過濾器確定在這裏建立的,果真能夠看到它建立一個 SessionRepositoryFilter
的過濾器,下面分別看過濾器和存儲。
SessionRepositoryFilter
過濾器必定是有 doFilter 方法,查看 doFilter 方法,spring 使用 OncePerRequestFilter
把 doFilter 包裝了一層,最終是調用 doFilterInternal 來實現的,查看 doFilterInternal 方法
實現方式爲使用了包裝者設計把 request 和 response 響應進行了包裝,咱們通常拿 session 通常是從 request.getSession() ,因此包裝的 request 確定要重寫 getSession ,因此能夠看 getSession 方法來看是如何從 redis 獲取 session ;
前面都是已經存在 session 的判斷相關,關鍵信息在這裏
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
這裏的 sessionRepository 就是咱們用來存取 session 的 RedisOperationsSessionRepository
查看 createSession 方法
RedisOperationsSessionRepository
// 這裏保存了在 redis 中 hash 結構能看到的數據 RedisSession redisSession = new RedisSession(); this(new MapSession()); this.delta.put(CREATION_TIME_ATTR, getCreationTime().toEpochMilli()); this.delta.put(MAX_INACTIVE_ATTR, (int) getMaxInactiveInterval().getSeconds()); this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime().toEpochMilli()); this.isNew = true; this.flushImmediateIfNecessary();
在 flushImmediateIfNecessary 方法中,若是 redisFlushMode 是 IMMEDIATE
模式,則會當即保存 session 進 redis ,但默認配置的是 ON_SAVE ,那是在哪裏保存進 redis 的呢,咱們回到最開始的過濾器 doFilterInternal 方法中,在 finally 中有一句
wrappedRequest.commitSession();
就是在這裏將 session 存儲進 redis 的 ,咱們跟進去看看,核心語句爲這句
SessionRepositoryFilter.this.sessionRepository.save(session);
session.saveDelta(); if (session.isNew()) { String sessionCreatedKey = getSessionCreatedChannel(session.getId()); this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); session.setNew(false); }
進入 saveDelta ,在這裏進行了 hash 結構的設置
getSessionBoundHashOperations(sessionId).putAll(this.delta);
最後一行進行了過時時間的設置和把當前 key 加入 set ,讀者自行查看
RedisOperationsSessionRepository.this.expirationPolicy .onExpirationUpdated(originalExpiration, this);
實際業務中,可能須要修改一些參數才能達到咱們業務的需求,最多見的需求就是修改 session 的過時時間了,在 EnableRedisHttpSession
註解中,已經提供了一些基本的配置如
maxInactiveIntervalInSeconds 最大過時時間,默認 30 分鐘 redisNamespace 插入到 redis 的 session 命名空間,默認是 spring:session cleanupCron 過時 session 清理任務,默認是 1 分鐘清理一次 redisFlushMode 刷新方式 ,其實在上面原理的 flushImmediateIfNecessary 方法中有用到,默認是 ON_SAVE
redisNamespace 是必定要修改的,這個不修改會影響別的項目,通常使用咱們項目的名稱加關鍵字 session 作 key ,代表這是這個項目的 session 信息。
不過這樣的配置明顯不夠,對於最大過時時間來講,有可能須要加到配置文件中去,而不是寫在代碼中,可是這裏沒有提供佔位符的功能,回到 RedisOperationsSessionRepository
的建立,最終配置的 maxInactiveIntervalInSeconds 仍是要設置到這個 bean 中去的,咱們能夠把這個 bean 的建立過程覆蓋,重寫 maxInactiveIntervalInSeconds 的獲取過程,就解決了,代碼以下
@Autowired RedisTemplate sessionRedisTemplate; @Autowired ApplicationEventPublisher applicationEventPublisher; @Value("${server.session.timeout}") private int sessionTimeout = 1800; @Primary // 使用 Primary 來覆蓋默認的 Bean @Bean public RedisOperationsSessionRepository sessionRepository() { RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate); // 這裏要把原來的屬性引用過來,避免出錯 ,能夠引用原來的類並複製屬性 ;像 redisNamespace,redisFlushMode 都要複製過來 return sessionRepository; }
還有一個就是 redis 的序列化問題,默認是使用的 jdk 的對象序列化,很容易出現加一個字段或減小一個字段出現不能反序列化,因此序列化方式是須要換的,若是項目中的緩存就已經使用了對象序列化的話,那就面要爲其單獨寫一個 redisTemplate 並設置進去,在構建 RedisOperationsSessionRepository
的時候設置 redisTemplate
還有一個很重要的問題就是登陸踢出問題,有時候只容許一個端登陸,其它的端都要退出,這個須要在登陸成功後才能把其它端踢出,我嘗試過不少方法去修改存入 redis 的 key ,但最終都是失敗的,由於操做不了 RedisSession
,就算建一個同包的類去修改它,SessionRepositoryRequestWrapper
也是沒法修改的,須要在 commitSession 的時候把 sessionId 寫入前端,這裏在以前就已經寫了原來的 UUID 值,因此咱們能作的就是在登陸成功後,把當前登陸的 sessionId 和當前用戶對應起來存入 redis ,下次登陸以前作一次檢測,若是已經登陸,則把以前的 key 刪除,但做者嘗試過使用 SessionRepository
的 deleteById 刪除 redis 的 session 記錄,可是無效,刪不乾淨,看源碼發現那裏是直接使用值來刪除 set 中內容的,多是序列化問題,但願有成功的大神能夠指導下。
創做不易,但願能夠支持下個人開源軟件,及個人小工具,歡迎來 gitee 點星,fork ,提 bug 。
Excel 通用導入導出,支持 Excel 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi
使用模板代碼 ,從數據庫生成代碼 ,及一些項目中常常能夠用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven