返回顶部
分享到

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

Redis 来源:互联网 作者:佚名 发布时间:2025-09-04 21:31:48 人浏览
摘要

在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器A并创建了Session,而第二次请求被路由到服务器B,服务器B无法识别该用户的Ses

在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器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中引入关键依赖:

1

2

3

4

5

6

7

8

9

<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的配置:

1

2

3

4

5

spring:

  data:

    redis:

      host: localhost

      port: 6379

redis配置类

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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共享验证

调用登录接口:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

$ 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去请求接口才能识别出用户。

查询用户信息:

1

2

$ curl --location --request GET 'http://172.23.208.1:8080/profile' --cookie 'SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy'

{"name":"morris"}

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

1

2

$ curl --location 'http://172.23.208.1:8082/profile' --cookie 'SESSION=ZTE0Yjc5NjItODFiZS00ZGYwLWE0NDktYTBjNmQ4ZjUxYmYy'

{"name":"morris"}

高级配置

自定义Cookie配置(支持跨域)

1

2

3

4

5

6

7

8

@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。

1

2

3

4

5

6

7

@Configuration(proxyBeanMethods = false)

@ConditionalOnMissingBean(SessionRepository.class)

@Import({ RedisSessionConfiguration.class, JdbcSessionConfiguration.class, HazelcastSessionConfiguration.class,

    MongoSessionConfiguration.class })

static class ServletSessionRepositoryConfiguration {

 

}

RedisSessionConfiguration类中会引入RedisHttpSessionConfiguration:

1

2

3

4

@Configuration(proxyBeanMethods = false)

@ConditionalOnProperty(prefix = "spring.session.redis", name = "repository-type", havingValue = "default", matchIfMissing = true)

@Import(RedisHttpSessionConfiguration.class)

static class DefaultRedisSessionConfiguration {

而注解@EnableRedisHttpSession引入的配置类也是RedisSessionConfiguration:

1

2

3

4

5

6

7

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)

@Target({ java.lang.annotation.ElementType.TYPE })

@Documented

@Import(SpringHttpSessionConfiguration.class)

public @interface EnableSpringHttpSession {

 

}

SessionRepositoryFilter

自定义过滤器SessionRepositoryFilter拦截所有请求,透明地替换了Servlet容器原生的HttpSession实现。

将请求包装为SessionRepositoryRequestWrapper:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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会话替换。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

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负责创建RedisSession。

1

2

3

4

5

6

7

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自己创建的,而不是使用的项目中的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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();

}


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • redis中session会话共享的三种方案
    在分布式系统架构中,用户请求可能被负载均衡器分发到不同的服务器节点。如果用户的第一次请求落在服务器A并创建了Session,而第二次请
  • shell脚本批量导出redis key-value方式
    1 背景 需求:工作中需要导出线上redis数据,但需避免使用keys命令全量扫描,导致瞬间响应卡顿,从而引发超时等问题 方法:最安全的方式
  • redis通用配置类的使用

    redis通用配置类的使用
    redis通用配置类 作用 处理Springboot使用 RedisTemplate过程中的编码问题 现象如下,看数据的时候不方便 所以添加一下的配置类之后,就可以了
  • linux部署redis集群遇到的问题及解决
    版本信息: redis:5.0.8 linux服务器:CentOS 7 不同版本问题处理方式可能有所不同 1、在java程序中,连接不上redisCluster 报错信息: no reachable
  • Redis中对大Key进行处理方式

    Redis中对大Key进行处理方式
    什么是大key 很多铁子可能会认为大key,是这个key的值很大其实不是,而是key的value值很大一般对于下面这些我们可以称为大key. String 类型值
  • 一文浅析如何在Redis中实现缓存功能
    Redis 是一种高性能的键值存储系统,广泛用于实现缓存功能。它通过将数据存储在内存中,能够快速读写数据,从而显著提高应用程序的性
  • Redis Cluster模式配置
    分片 一、分片的本质与核心价值 问题根源 单机 Redis 存在内存容量和吞吐量瓶颈,分片通过将数据分散到多个节点解决此问题。 核心价值
  • Redis中的Lettuce使用介绍
    Lettuce 是一个高级的、线程安全的 Redis 客户端,用于与 Redis 数据库交互。它提供了许多方法来配置连接池的参数,例如最大连接数、最小空
  • redis过期key的删除策略
    在使用redis的过程中,不免会产生过期的key,而这些key过期后并不会实时地马上被删除,当这些key数量累积越来越多,就会占用很多内存,因
  • React实现组件之间通信的几种常用方法
    React 中如何实现组件之间的通信? 1. Props 传递 最直接的通信方式是通过 props 将数据从父组件传递给子组件。父组件通过属性将数据传递给
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计