redis中session會(huì)話共享的三種方案
在分布式系統(tǒng)架構(gòu)中,用戶請(qǐng)求可能被負(fù)載均衡器分發(fā)到不同的服務(wù)器節(jié)點(diǎn)。如果用戶的第一次請(qǐng)求落在服務(wù)器A并創(chuàng)建了Session,而第二次請(qǐng)求被路由到服務(wù)器B,服務(wù)器B無法識(shí)別該用戶的Session狀態(tài),導(dǎo)致用戶需要重新登錄,這顯然是災(zāi)難性的用戶體驗(yàn)。
三種解決方案
粘性會(huì)話(Sticky Sessions)
例如在Nginx的負(fù)載均衡策略中,通過IP哈希等策略將同一個(gè)ip的用戶請(qǐng)求固定到同一服務(wù)器中,這樣session自然也沒有失效。
缺點(diǎn):?jiǎn)吸c(diǎn)故障風(fēng)險(xiǎn)高(服務(wù)器宕機(jī)導(dǎo)致Session丟失);擴(kuò)容時(shí)Rehash引發(fā)路由混亂。
Session復(fù)制
例如在Tomcat集群中實(shí)現(xiàn)Session復(fù)制,需通過修改配置文件使不同節(jié)點(diǎn)間自動(dòng)同步會(huì)話數(shù)據(jù)。集群內(nèi)所有服務(wù)器實(shí)時(shí)同步Session數(shù)據(jù)。
缺點(diǎn):同步開銷隨服務(wù)器數(shù)量指數(shù)級(jí)增長(zhǎng),引發(fā)網(wǎng)絡(luò)風(fēng)暴和內(nèi)存浪費(fèi)。
redis統(tǒng)一存儲(chǔ)
SpringBoot整合Spring Session,通過redis存儲(chǔ)方式實(shí)現(xiàn)session共享。
通過集中存儲(chǔ)Session(如Redis),實(shí)現(xiàn):
- 無狀態(tài)擴(kuò)展:新增服務(wù)器無需同步Session,直接訪問中央存儲(chǔ)。
- 高可用性:即使單服務(wù)器宕機(jī),會(huì)話數(shù)據(jù)仍可從Redis恢復(fù),用戶無感知。
- 數(shù)據(jù)一致性:所有服務(wù)器讀寫同一份Session數(shù)據(jù),避免狀態(tài)沖突
Spring Session + Redis集成
添加依賴
在pom.xml中引入關(guān)鍵依賴:
<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中加上Redis的配置:
spring:
data:
redis:
host: localhost
port: 6379
redis配置類
需要注入一個(gè)名為springSessionDefaultRedisSerializer的序列化對(duì)象,用于在redis中寫入對(duì)象時(shí)進(jìn)行序列化,不然session中存入對(duì)象會(huì)拋出異常。
package com.morris.redis.demo.session;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public GenericJackson2JsonRedisSerializer springSessionDefaultRedisSerializer() {
// 需要注入一個(gè)名為springSessionDefaultRedisSerializer的序列化對(duì)象
// 不然session中存入對(duì)象會(huì)拋出異常
return new GenericJackson2JsonRedisSerializer();
}
}
不需要顯示的通過注解@EnableRedisHttpSession來開啟session共享。
使用Session
package com.morris.redis.demo.session;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.*;
@RestController
public class AuthController {
@PostMapping("/login")
public String login(HttpSession session, @RequestBody User user) {
// 驗(yàn)證用戶憑證...
session.setAttribute("currentUser", user);
return "登錄成功,SessionID:" + session.getId();
}
@GetMapping("/profile")
@ResponseBody
public User profile(HttpSession session) {
// 任意服務(wù)節(jié)點(diǎn)都能獲取到相同Session
return (User) session.getAttribute("currentUser");
}
}
session共享驗(yàn)證
調(diào)用登錄接口:
$ curl --location --request POST 'http://172.23.208.1:8080/login' --header 'Content-Type: application/json' --data-raw '{"name": "morris"}' -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 172.23.208.1:8080...
* TCP_NODELAY set
* Connected to 172.23.208.1 (172.23.208.1) port 8080 (#0)
> POST /login HTTP/1.1
> Host: 172.23.208.1:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 18
>
* upload completely sent off: 18 out of 18 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Set-Cookie: SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy; Path=/; HttpOnly; SameSite=Lax
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 63
< Date: Tue, 24 Jun 2025 03:23:52 GMT
<
* Connection #0 to host 172.23.208.1 left intact
登錄成功,SessionID:e14b7962-81be-4df0-a449-a0c6d8f51bf2
可以看到返回的響應(yīng)頭中帶有cookie,后續(xù)請(qǐng)求需要帶上這個(gè)cookie去請(qǐng)求接口才能識(shí)別出用戶。
查詢用戶信息:
$ curl --location --request GET 'http://172.23.208.1:8080/profile' --cookie 'SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy'
{"name":"morris"}
可以修改端口再啟動(dòng)一個(gè)服務(wù),換個(gè)服務(wù)查詢用戶信息:
$ curl --location 'http://172.23.208.1:8082/profile' --cookie 'SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy'
{"name":"morris"}
高級(jí)配置
自定義Cookie配置(支持跨域)
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setDomainNamePattern("example.com");
serializer.setCookiePath("/");
return serializer;
}
Spring Session核心原理
SessionAutoConfiguration
這就是為什么不需要使用注解@EnableRedisHttpSession來開啟session共享。
SessionAutoConfiguration類中會(huì)引入RedisSessionConfiguration。
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SessionRepository.class)
@Import({ RedisSessionConfiguration.class, JdbcSessionConfiguration.class, HazelcastSessionConfiguration.class,
MongoSessionConfiguration.class })
static class ServletSessionRepositoryConfiguration {
}
RedisSessionConfiguration類中會(huì)引入RedisHttpSessionConfiguration:
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.session.redis", name = "repository-type", havingValue = "default", matchIfMissing = true)
@Import(RedisHttpSessionConfiguration.class)
static class DefaultRedisSessionConfiguration {
而注解@EnableRedisHttpSession引入的配置類也是RedisSessionConfiguration:
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(SpringHttpSessionConfiguration.class)
public @interface EnableSpringHttpSession {
}
SessionRepositoryFilter
自定義過濾器SessionRepositoryFilter攔截所有請(qǐng)求,透明地替換了Servlet容器原生的HttpSession實(shí)現(xiàn)。
將請(qǐng)求包裝為SessionRepositoryRequestWrapper:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
HttpServletRequestWrapper
HttpServletRequestWrapper中重寫getSession()方法實(shí)現(xiàn)session會(huì)話替換。
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
S requestedSession = getRequestedSession();
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.markNotNew();
setCurrentSession(currentSession);
return currentSession;
}
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
}
if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
&& this.response.isCommitted()) {
throw new IllegalStateException("Cannot create a session after the response has been committed");
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException("For debugging purposes only (not an error)"));
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
RedisSessionRepository
RedisSessionRepository負(fù)責(zé)創(chuàng)建RedisSession。
public RedisSession createSession() {
MapSession cached = new MapSession(this.sessionIdGenerator);
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
session.flushIfRequired();
return session;
}
RedisSession
session保存時(shí)使用的是sessionRedisOperations,其實(shí)就是RedisTemplate,這個(gè)RedisTemplate是spring session自己創(chuàng)建的,而不是使用的項(xiàng)目中的。
private void save() {
saveChangeSessionId();
saveDelta();
if (this.isNew) {
this.isNew = false;
}
}
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String key = getSessionKey(getId());
RedisSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key, new HashMap<>(this.delta));
RedisSessionRepository.this.sessionRedisOperations.expireAt(key,
Instant.ofEpochMilli(getLastAccessedTime().toEpochMilli())
.plusSeconds(getMaxInactiveInterval().getSeconds()));
this.delta.clear();
}
到此這篇關(guān)于redis中session會(huì)話共享的三種方案的文章就介紹到這了,更多相關(guān)redis session會(huì)話共享內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSession+Redis實(shí)現(xiàn)集群會(huì)話共享的方法
- Java簡(jiǎn)單實(shí)現(xiàn)session保存到redis的方法示例
- spring boot整合redis實(shí)現(xiàn)shiro的分布式session共享的方法
- 詳解springboot中redis的使用和分布式session共享問題
- 如何使用Redis保存用戶會(huì)話Session詳解
- Spring Boot高級(jí)教程之使用Redis實(shí)現(xiàn)session共享
- spring-redis-session 自定義 key 和過期時(shí)間
- SpringBoot使用redis實(shí)現(xiàn)session共享功能
相關(guān)文章
Redis查看KEY的數(shù)據(jù)類型的方法和步驟
在Redis中,可以使用 TYPE 命令來查看指定key的數(shù)據(jù)類型,該命令會(huì)返回存儲(chǔ)在指定key中的值的數(shù)據(jù)類型,本文給大家介紹了具體的使用方法和步驟,感興趣的朋友可以參考下2024-04-04
關(guān)于redis可視化工具讀取數(shù)據(jù)亂碼問題
大家來聊一聊在日常操作redis時(shí)用的是什么工具,redis提供的一些命令你都了解了嗎,今天通過本文給大家介紹redis可視化工具讀取數(shù)據(jù)亂碼問題,感興趣的朋友跟隨小編一起看看吧2021-07-07

