杰瑞科技汇

Java实现Redis共享Session,如何保证高并发安全?

为什么需要 Session 共享?

我们理解一下为什么要做这件事。

  • 问题场景:当你的 Java Web 应用(如 Spring Boot + Tomcat)部署在多台服务器上时,通常会使用负载均衡器(如 Nginx, F5)将用户请求分发到不同的服务器。
  • 产生的问题:用户的第一次请求可能被分发到 Server A,Session 信息被创建并存储在 Server A 的内存中,用户的下一次请求可能被分发到 Server B,但 Server B 并没有用户的 Session 信息,导致用户需要重新登录,或者丢失之前的操作状态。
  • 解决方案:Session 共享,我们将 Session 数据从各个服务器的本地内存中,统一存储到一个所有服务器都能访问的外部存储中,当任何一台服务器需要获取 Session 数据时,都从这个外部存储中读取和写入,Redis 因为其高性能、丰富的数据结构,是实现 Session 共享最主流的选择。

实现原理

实现 Session 共享的核心思想是 将 Session 的创建、读取、更新和删除操作从 Web 容器(如 Tomcat)重定向到 Redis

  1. 拦截请求:当用户第一次访问应用时,一个过滤器会拦截这个请求。
  2. 创建/获取 Session ID:如果请求中不包含有效的 Session ID,过滤器会创建一个新的 Session ID(通常是 JSESSIONID)。
  3. 操作 Redis
    • 创建 Session:过滤器将新的 Session ID 作为 key,将 Session 对象序列化后的数据作为 value,存入 Redis 中,并设置一个过期时间(与 Session 超时时间一致)。
    • 读取 Session:如果请求中包含 Session ID,过滤器会根据这个 ID 去 Redis 中查找对应的 Session 数据,如果找到,就将其反序列化并设置为当前请求的 Session 对象。
  4. 后续处理:之后,应用代码中所有的 request.getSession() 操作,实际上都是操作这个从 Redis 中加载的 Session 对象。
  5. Session 失效:当 Session 超时或调用 invalidate() 方法时,对应的 key-value 对会从 Redis 中被删除。

如何实现?(主流方案)

主要有两种实现方式:手动编码使用 Spring Session


手动编码(不推荐,但有助于理解原理)

这种方式需要你自己处理所有与 Redis 交互的逻辑,代码量多且容易出错,通常只在学习或特殊需求下使用。

Java实现Redis共享Session,如何保证高并发安全?-图1

核心步骤:

  1. 添加依赖

    <!-- Redis 客户端 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 序列化工具,如 Jackson -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
  2. 配置 Redis

    # application.properties
    spring.redis.host=your-redis-host
    spring.redis.port=6379
  3. 创建过滤器: 创建一个 HttpSessionListenerFilter,在 doFilter 方法中实现核心逻辑。

    Java实现Redis共享Session,如何保证高并发安全?-图2

    // 伪代码示例
    public class RedisSessionFilter implements Filter {
        private RedisTemplate<String, Object> redisTemplate;
        @Override
        public void init(FilterConfig filterConfig) {
            // 初始化 RedisTemplate
        }
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            // 1. 从请求中获取 sessionId
            String sessionId = getSessionIdFromCookie(httpRequest);
            // 2. 如果没有 sessionId,生成一个新的
            if (sessionId == null) {
                sessionId = generateSessionId();
                // 将 sessionId 写入 Cookie
                addSessionIdToCookie(httpResponse, sessionId);
            }
            // 3. 从 Redis 中获取 Session 数据
            Map<String, Object> sessionData = (Map<String, Object>) redisTemplate.opsForValue().get("session:" + sessionId);
            HttpSessionWrapper sessionWrapper = null;
            if (sessionData == null) {
                // Redis 中没有,创建一个新的 HttpSessionWrapper
                sessionWrapper = new HttpSessionWrapper(httpRequest.getSession(false));
            } else {
                // 如果有,创建一个包装的 HttpSession,数据从 Redis 来
                sessionWrapper = new HttpSessionWrapper(httpRequest.getSession(true));
                // 将数据填充到 sessionWrapper 中...
            }
            // 4. 将包装后的 Session 绑定到请求上
            httpRequest = new SessionRequestWrapper(httpRequest, sessionWrapper);
            chain.doFilter(httpRequest, response);
        }
        // ... 其他辅助方法 ...
    }

    注意:你需要自己处理 Session 的序列化/反序列化,以及 Session 对象的包装,非常繁琐。


使用 Spring Session(强烈推荐)

Spring Session 是 Spring 家族的一个项目,它提供了一种简单的方式来管理 HTTP 会话,你几乎不需要写任何代码,只需要做一些配置,就能自动完成 Session 共享。

核心优势:

  • 零侵入:你原有的业务代码(request.getSession())完全不需要修改。
  • 简单易用:只需要添加依赖和配置即可。
  • 功能强大:支持多种数据存储(Redis, JDBC, MongoDB 等),支持 WebSocket、Session 事件等。

实现步骤(以 Spring Boot + Redis 为例):

Java实现Redis共享Session,如何保证高并发安全?-图3

添加依赖

在你的 pom.xml 中添加 spring-boot-starter-data-redisspring-session-data-redis

<dependencies>
    <!-- Spring Boot Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- Spring Session Data Redis (核心) -->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
</dependencies>

配置 Redis 和 Spring Session

application.propertiesapplication.yml 中进行配置。

application.properties 示例:

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379
# Spring Session 配置
# 这个注解会由 @EnableRedisHttpSession 替代,这里只是展示概念
# spring.session.store-type=redis
# 设置 Session 过期时间,单位秒,默认 30 分钟
server.servlet.session.timeout=1800 

application.yml 示例 (推荐):

spring:
  redis:
    host: localhost
    port: 6379
  session:
    store-type: redis # 关键配置:指定使用 Redis 作为 Session 存储
    timeout: 1800s    # Session 超时时间
    # 如果需要自定义 Redis 中 Session 的 key 前缀
    # redis:
    #   namespace: myapp:sessions

启用 Spring Session

在你的 Spring Boot 启动类上添加 @EnableRedisHttpSession 注解,这个注解是整个配置的核心,它会自动为你配置好所有必要的 Bean(包括上面提到的过滤器)。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@SpringBootApplication
// 这个注解会启用 Redis-backed HTTP Session
// maxInactiveInSeconds: 设置 Session 的最大不活动时间(秒),会覆盖 application.yml 中的配置
@EnableRedisHttpSession(maxInactiveInSeconds = 1800) 
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

验证

你的应用已经配置完成!你可以编写一个简单的 Controller 来测试。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@RestController
public class SessionController {
    @GetMapping("/set-session")
    public String setSession(HttpSession session) {
        // 从 Session 中获取一个属性,如果没有则创建
        String userId = (String) session.getAttribute("userId");
        if (userId == null) {
            userId = UUID.randomUUID().toString();
            session.setAttribute("userId", userId);
        }
        return "Session attribute 'userId' has been set to: " + userId;
    }
    @GetMapping("/get-session")
    public String getSession(HttpSession session) {
        String userId = (String) session.getAttribute("userId");
        if (userId == null) {
            return "No 'userId' found in session.";
        }
        return "Session attribute 'userId'

分享:
扫描分享到社交APP
上一篇
下一篇