共計 8935 個字符,預計需要花費 23 分鐘才能閱讀完成。
這篇文章主要講解了“怎么解決 redis 中分布式 session 不一致性”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著丸趣 TV 小編的思路慢慢深入,一起來研究和學習“怎么解決 redis 中分布式 session 不一致性”吧!
分布式 session 不一致性解決方案
一、Session 有什么作用?
Session 是客戶端與服務器通訊會話跟蹤技術,服務器與客戶端保持整個通訊的會話基本信息。【相關推薦:Redis 視頻教程】
客戶端在第一次訪問服務端的時候,服務端會響應一個 sessionId 并且將它存入到本地 cookie 中,在之后的訪問會將 cookie 中的 sessionId 放入到請求頭中去訪問服務器,
如果通過這個 sessionId 沒有找到對應的數據, 那么服務器會創建一個新的 sessionId 并且響應給客戶端。
二、分布式 Session 有什么問題?
單服務器 web 應用中,session 信息只需存在該服務器中,這是我們前幾年最常接觸的方式
但是近幾年隨著分布式系統的流行,單系統已經不能滿足日益增長的百萬級用戶的需求,集群方式部署服務器已在很多公司運用起來
當高并發量的請求到達服務端的時候通過負載均衡的方式分發到集群中的某個服務器,這樣就有可能導致同一個用戶的多次請求被分發到集群的不同服務器上,就會出現取不到 session 數據的情況,于是 session 的共享就成了一個問題。
三、服務做集群一般是怎么樣做的?
SpringBoot 項目,那么只要改下端口號啟動幾個,然后用 nginx 統一做反向代理。
SpringCloud 微服務項目,那么這個時候,可以使用 ribbon 本地負載均衡。
四、nginx 負載均衡和 ribbon 負載均衡的區別
nginx 做負載均衡是服務器端的負載均衡,統一訪問一個地址,根據負載均衡算法訪問決定訪問那一個服務器。
ribbon 負載均衡,這是本地負載均衡(客戶端負載均衡),把提供服務的客戶端地址都緩存記錄下來,根據本地的算法實現負載均衡。
五、Session 一致性解決方案
1. session 復制(同步)
思路:多個服務端之間相互同步 session,這樣每個服務端之間都包含全部的 session
優點:服務端支持的功能,應用程序不需要修改代碼
缺點:
session 的同步需要數據傳輸,占內網帶寬,有時延
所有服務端都包含所有 session 數據,數據量受內存限制,無法水平擴展
2. 客戶端存儲法
思路:服務端存儲所有用戶的 session,內存占用較大,可以將 session 存儲到瀏覽器 cookie 中,每個端只要存儲一個用戶的數據了
優點:服務端不需要存儲
缺點:
每次 http 請求都攜帶 session,占外網帶寬
數據存儲在端上,并在網絡傳輸,存在泄漏、篡改、竊取等安全隱患
session 存儲的數據大小和域名 cookie 個數都受限制的
注:該方案雖然不常用,但確實是一種思路。
3. 反向代理 hash 一致性
思路:服務端為了保證高可用,有多臺冗余,反向代理層能不能做一些事情,讓同一個用戶的請求保證落在一臺服務端上呢?
方案一:四層代理 hash
反向代理層使用用戶的 ip 來做 hash,以保證同一個 ip 的請求落在同一個服務端上
方案二:七層代理 hash
反向代理使用 http 協議中的某些業務屬性來做 hash,例如 sid,city_id,user_id 等,能夠更加靈活的實施 hash 策略,以保證同一個瀏覽器用戶的請求落在同一個服務器上
優點:
只需要改 nginx 配置,不需要修改應用代碼
負載均衡,只要 hash 屬性是均勻的,多臺服務端的負載是均衡的
可以支持服務端水平擴展(session 同步法是不行的,受內存限制)
缺點:
如果服務端重啟,一部分 session 會丟失,產生業務影響,例如部分用戶重新登錄
如果服務端水平擴展,rehash 后 session 重新分布,也會有一部分用戶路由不到正確的 session
session 一般是有有效期的,所有不足中的兩點,可以認為等同于部分 session 失效,一般問題不大。
對于四層 hash 還是七層 hash,個人推薦前者:讓專業的軟件做專業的事情,反向代理就負責轉發,盡量不要引入應用層業務屬性,除非不得不這么做(例如,有時候多機房多活需要按照業務屬性路由到不同機房的服務器)。
四層、七層負載均衡的區別
4. 后端統一集中存儲
優點:
沒有安全隱患
可以水平擴展,數據庫 / 緩存水平切分即可
服務端重啟或者擴容都不會有 session 丟失
不足:增加了一次網絡調用,并且需要修改應用代碼
對于 db 存儲還是 cache,個人推薦后者:session 讀取的頻率會很高,數據庫壓力會比較大。如果有 session 高可用需求,cache 可以做高可用,但大部分情況下 session 可以丟失,一般也不需要考慮高可用。
總結
保證 session 一致性的架構設計常見方法:
session 同步法:多臺服務端相互同步數據
客戶端存儲法 一個用戶只存儲自己的數據
反向代理 hash 一致性 四層 hash 和七層 hash 都可以做,保證一個用戶的請求落在一臺服務端上
后端統一存儲 服務端重啟和擴容,session 也不會丟失(推薦后端 cache 統一存儲)
六、案例實戰:SpringSession+redis 解決分布式 session 不一致性問題
步驟 1:加入 SpringSession、redis 的依賴包
dependency
groupId org.springframework.boot /groupId
artifactId spring-boot-starter-redis /artifactId
version 1.4.7.RELEASE /version
/dependency
dependency
groupId org.springframework.session /groupId
artifactId spring-session-data-redis /artifactId
/dependency
步驟 2:配置文件
# 為某個包目錄下 設置日志
logging.level.com.ljw=debug
# 設置 session 的存儲方式,采用 redis 存儲
spring.session.store-type=redis
# session 有效時長為 10 分鐘
server.servlet.session.timeout=PT10M
## Redis 配置
## Redis 數據庫索引(默認為 0)spring.redis.database=0
## Redis 服務器地址
spring.redis.host=127.0.0.1
## Redis 服務器連接端口
spring.redis.port=6379
## Redis 服務器連接密碼(默認為空)spring.redis.password=
步驟 3:配置攔截器
@Configuration
public class SessionConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityInterceptor())
// 排除攔截的 2 個路徑
.excludePathPatterns(/user/login)
.excludePathPatterns(/user/logout)
// 攔截所有 URL 路徑
.addPathPatterns( /**
}
}
@Configuration
public class SecurityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { HttpSession session = request.getSession();
// 驗證當前 session 是否存在,存在返回 true true 代表能正常處理業務邏輯
if (session.getAttribute(session.getId()) != null){ log.info( session 攔截器,session={},驗證通過 ,session.getId());
return true;
}
//session 不存在,返回 false,并提示請重新登錄。 response.setCharacterEncoding( UTF-8
response.setContentType( application/json; charset=utf-8
response.getWriter().write( 請登錄!!!!! log.info(session 攔截器,session={},驗證失敗 ,session.getId());
return false;
}
}
HandlerInterceptor
preHandle:在業務處理器處理請求之前被調用。預處理,可以進行編碼、安全控制、權限校驗等處理;
postHandle:在業務處理器處理請求執行完成后,生成視圖之前執行。后處理(調用了 Service 并返回 ModelAndView,但未進行頁面渲染),有機會修改 ModelAndView
afterCompletion:在 DispatcherServlet 完全處理完請求后被調用,可用于清理資源等。返回處理(已經渲染了頁面)
步驟 4:控制器
@RestController
@RequestMapping(value = /user)
public class UserController { Map String, User userMap = new HashMap ();
public UserController() {
// 初始化 2 個用戶,用于模擬登錄
User u1=new User(1, user1 , user1
userMap.put(user1 ,u1);
User u2=new User(2, user2 , user2
userMap.put(user2 ,u2);
}
@GetMapping(value = /login)
public String login(String username, String password, HttpSession session) {
// 模擬數據庫的查找
User user = this.userMap.get(username);
if (user != null) { if (!password.equals(user.getPassword())) {
return 用戶名或密碼錯誤!!! } else { session.setAttribute(session.getId(), user);
log.info(登錄成功 {} ,user);
}
} else {
return 用戶名或密碼錯誤!!! }
return 登錄成功!!! }
/**
* 通過用戶名查找用戶
*/
@GetMapping(value = /find/{username} )
public User find(@PathVariable String username) { User user=this.userMap.get(username);
log.info(通過用戶名 ={}, 查找出用戶 {} ,username,user);
return user;
}
/**
* 拿當前用戶的 session
*/
@GetMapping(value = /session)
public String session(HttpSession session) { log.info( 當前用戶的 session={} ,session.getId());
return session.getId();
}
/**
* 退出登錄
*/
@GetMapping(value = /logout)
public String logout(HttpSession session) { log.info( 退出登錄 session={} ,session.getId());
session.removeAttribute(session.getId());
return 成功退出!! }
}
步驟 5:實體類
@Data
public class User implements Serializable{
private int id;
private String username;
private String password;
public User(int id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
}
步驟 6:訪問測試
先登錄:http://127.0.0.1:8080/user/login?username=user1 password=user1
再查詢 http://127.0.0.1:8080/user/find/user1
七、剖析 SpringSession 的 redis 原理
步驟 1:分析 SpringSession 的 redis 數據結構
127.0.0.1:6379 keys *
1) spring:session:sessions:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a
2) spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
3) spring:session:expirations:1635413520000
4) spring:session:sessions:expires:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a
5) spring:session:expirations:1635412980000
6) spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
共同點:3 個 key 都是以 spring:session: 開頭的,代表了 SpringSession 的 redis 數據。
查詢類型
127.0.0.1:6379 type spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
hash
127.0.0.1:6379 hgetall spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
// session 的創建時間
1) creationTime
2) \xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xdb\xecu
// sesson 的屬性,存儲了 user 對象
3) sessionAttr:d3434f61-4d0a-4687-9070-610bd7790f3b
4) \xac\xed\x00\x05sr\x00\x1ecom.ljw.redis.controller.User\x16\ _m\x1b\xa0W\x7f\x02\x00\x03I\x00\x02idL\x00\bpasswordt\x00\x12Ljava/lang/String;L\x00\busernameq\x00~\x00\x01xp\x00\x00\x00\x01t\x00\x05user1q\x00~\x00\x03
// 最后的訪問時間
5) lastAccessedTime
6) \xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xe1\xc7\xed
// 失效時間 100 分鐘
7) maxInactiveInterval
8) \xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x17p
步驟 2:分析 SpringSession 的 redis 過期策略
對于過期數據,一般有三種刪除策略:
定時刪除,即在設置鍵的過期時間的同時,創建一個定時器,當鍵的過期時間到來時,立即刪除。
惰性刪除,即在訪問鍵的時候,判斷鍵是否過期,過期則刪除,否則返回該鍵值。
定期刪除,即每隔一段時間,程序就對數據庫進行一次檢查,刪除里面的過期鍵。至于要刪除多少過期鍵,以及要檢查多少個數據庫,則由算法決定。
redis 刪除過期數據采用的是懶性刪除 + 定期刪除組合策略,也就是數據過期了并不會及時被刪除。
但由于 redis 是單線程,并且 redis 對刪除過期的 key 優先級很低;如果有大量的過期 key,就會出現 key 已經過期但是未刪除。
為了實現 session 過期的及時性,spring session 采用了定時刪除 + 惰性刪除的策略。
定時刪除
127.0.0.1:6379 type spring:session:expirations:1635413520000
127.0.0.1:6379 smembers spring:session:expirations:1635413520000
1) \xac\xed\x00\x05t\x00,expires:d3434f61-4d0a-4687-9070-610bd7790f3b
2) spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
3) spring:session:expirations:1635413520000
6) spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
1635412980000 是時間戳,等于 2021-10-28 17:23:00,即是該可以在這個時刻過期
springsession 定時(1 分鐘)輪詢,刪除 spring:session:expirations:[?] 的過期成員元素,例如:spring:session:expirations:1635413520000
springsesion 定時檢測超時的 key 的值,根據值刪除 seesion,例如 key:spring:session:expirations:1635413520000,值為(sessionId):d3434f61-4d0a-4687-9070-610bd7790f3b 的 seesion
惰性刪除
127.0.0.1:6379 type spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
string
127.0.0.1:6379 get spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
127.0.0.1:6379 ttl spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
(integer) 3143
127.0.0.1:6379
訪問 spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b 的時候,判斷 key 是否過期,過期則刪除,否則返回改進的值。
例如 訪問 spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b 的時候,判斷 ttl 是否過期,過期就直接刪除
2) spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
3) spring:session:expirations:1635413520000
6) spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
感謝各位的閱讀,以上就是“怎么解決 redis 中分布式 session 不一致性”的內容了,經過本文的學習后,相信大家對怎么解決 redis 中分布式 session 不一致性這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是丸趣 TV,丸趣 TV 小編將為大家推送更多相關知識點的文章,歡迎關注!