redis中session会话共享的三种方案

2025-08-05 21:50

本文主要是介绍redis中session会话共享的三种方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《redis中session会话共享的三种方案》本文探讨了分布式系统中Session共享的三种解决方案,包括粘性会话、Session复制以及基于Redis的集中存储,具有一定的参考价值,感兴趣的可以了...

在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器A并创建了Session,而第二次请求被路由到服务器B,服务器B无法识别该用户的Session状态,导致用户需要重新登录,这显然是灾难性的用户体验。

三种解决方案

粘性会话(Sticky Sessions)

例如在Nginx的负载均衡策略中,通过IP哈希等策略将同一个ip的用户请求固定到同一服务器中,这样session自然也没有失效。

缺点:单点故障风险高(服务器宕机导致Session丢失);扩容时Rehash引发路由混乱。

Session复制

例如在Tomcat集群中实现Session复制,需通过修改配置文件使不同节点间自动同步会话数据。集群内所有服务器实时同步Session数据。

缺点:同步开销随服务器数量指数级增长,引发网络风暴和内存浪费。

redis统一存储

SpringBoot整合Spring Session,通过redis存储方式实现session共享。

通过集中存储Session(如Redis),实现:

  • 无状态扩展:新增服务器无需同步Session,直接访问中央存储。
  • 高可用性:即使单服务器宕机,会话数据仍可从Redis恢复,用户无感知。
  • 数据一致性:所有服务器读写同一份Session数据,避免状态冲突

Spring Session + Redis集成

添加依赖

在pom.XML中引入关键依赖:

<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配置类

需要注入一个名为springSessionDefaultRedisSerializer的序列化对象,用于在redis中写入对象时进行序列化,不然session中存入对象会抛出异常。

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() {
        // 需要注入一个名为springSessionDefaultRedisSerializer的序列化对象
        // 不然session中存入对象会抛出异常
        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) {
        // 验证用户凭证...
        session.setAttribute("currentUser", user);
        return "登录成功,SessionID:" + session.getId();
    }

    @GetMapping("/profile")
    @ResponseBody
    public User profile(HttpSession session) {
        // 任意服务节点都能获取到相同Session
        return (User) session.getAttribute("currentUser");
    }
}

session共享验证

调用登录接口:

$ 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

可以看到返回的响应头中带有cookie,后续请求需要带上这个cookie去请求接口才能识别出用户。

查询用户信息:

$ curl --location --request GET 'http://172.23.208.1:8080/profile' --cookie 'SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy'
{"name":"morris"}

可以修改端口再启动一个服务,换个服务查询用户信息:

$ curl --location 'http://172.23.208.1:8082/profile' --cookie 'SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy'
{"name":"morris"}

高级配置

自定义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类中会引入RedisSessionConfiguration。

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SessionRepository.class)
@Import({ RedisSessionConfiguration.class, JdbcSessionConfiguration.class, HazelcastSessionConfiguration.class,
    MongoSessionConfiguration.class })
static class ServletSessionRepositoryConfiguration {

}

RedisSessionConfiguration类中会引入RedisHttpSessionConfiguration:

@Configuration(proxyBeanMethods http://www.chinasem.cn= 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拦截所有请求,透明地替换了Servlet容器原生的HttpSession实现。

将请求包装为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()方法实现session会话替换。

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_LOGGERjs.debug(
					"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
		}
		setAttribute(INVALID_SESSIhttp://www.chinasem.cnON_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

javascript

RedisSessionRepository负责创建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保存时使用的是sessionRedisOperations,其实就是RedisTemplate,这个RedisTemplate是spring session自己创建的,而不是使用的项目中的。

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().toEpochMilandroidli())
        .plusSeconds(getMaxInactiveInterval().getSeconds()));
  this.delta.clear();
}

到此这篇关于redis中session会话共享的三种方案的文章就介绍到这了,更多相关redis session会话共享内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)! 

这篇关于redis中session会话共享的三种方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155609

相关文章

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Python实现批量CSV转Excel的高性能处理方案

《Python实现批量CSV转Excel的高性能处理方案》在日常办公中,我们经常需要将CSV格式的数据转换为Excel文件,本文将介绍一个基于Python的高性能解决方案,感兴趣的小伙伴可以跟随小编一... 目录一、场景需求二、技术方案三、核心代码四、批量处理方案五、性能优化六、使用示例完整代码七、小结一、

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

使用Python实现Word文档的自动化对比方案

《使用Python实现Word文档的自动化对比方案》我们经常需要比较两个Word文档的版本差异,无论是合同修订、论文修改还是代码文档更新,人工比对不仅效率低下,还容易遗漏关键改动,下面通过一个实际案例... 目录引言一、使用python-docx库解析文档结构二、使用difflib进行差异比对三、高级对比方

JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法

《JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法》:本文主要介绍JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法,每种方法结合实例代码给大家介绍的非常... 目录引言:为什么"相等"判断如此重要?方法1:使用some()+includes()(适合小数组)方法2

sky-take-out项目中Redis的使用示例详解

《sky-take-out项目中Redis的使用示例详解》SpringCache是Spring的缓存抽象层,通过注解简化缓存管理,支持Redis等提供者,适用于方法结果缓存、更新和删除操作,但无法实现... 目录Spring Cache主要特性核心注解1.@Cacheable2.@CachePut3.@Ca

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署