zoukankan      html  css  js  c++  java
  • Spring Session解决Session共享

    1. 分布式Session共享

      在分布式集群部署环境下,使用Session存储用户信息,往往出现Session不能共享问题。
      例如:服务集群部署后,分为服务A和服务B,当用户登录时负载到服务A登录成功返回用户Session存到本地Cookie中,下一次操作时从Cookie中获取session添加到请求头并负载到服务B,服务B通过Session Id无法获取用户信息,则返回新的Sssion,这样就出现了Session不共享问题。

    2. 常见解决方案

    • 使用Cookie存储用户信息(不推荐,极其不安全)
    • 通过Tomcat内置的Session同步机制(不推荐,可能出现延迟)
    • 使用数据库同步Session(不推荐,效率低)
    • 使用Nginx的ip_hash策略(可用,同一个ip只能负载到同一个server,消失负载均衡功能)
    • 使用token代替Session(推荐,前后端分离/微服务首选)
    • 使用Spring Session实现Session共享(推荐,单体应用集群部署可用)
        以Spring Session将Session存储在Redis中实现Session共享为例。

    3. 演示问题

    • 创建Maven工程
    • 修改pom.xml
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<groupId>com.c3stones</groupId>
    	<artifactId>spring-session-demo</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>spring-session-demo</name>
    	<description>Spring Boot Session Demo</description>
    
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.2.6.RELEASE</version>
    		<relativePath />
    	</parent>
    
    	<properties>
    		<java.version>1.8</java.version>
    		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    
    • 创建Controller
    import javax.servlet.http.HttpSession;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 用户Controller
     * 
     * @author CL
     *
     */
    @RestController
    public class UserController {
    
    	private static Logger logger = LoggerFactory.getLogger(UserController.class);
    
    	@Value("${server.port}")
    	private Integer port;
    
    	/**
    	 * 模拟登录
    	 * 
    	 * @param session  HttpSession
    	 * @param username 用户名
    	 * @return
    	 */
    	@RequestMapping(value = "/login")
    	public String login(HttpSession session, String username) {
    		session.setAttribute("username", username);
    		String msg = String.format("用户登录的当前服务端口:%s,用户名称为:%s,Session ID为:%s", port, username, session.getId());
    		logger.info(msg);
    		return msg;
    	}
    
    	/**
    	 * 获取用户信息
    	 * 
    	 * @param session  HttpSession
    	 * @param username 用户名
    	 * @return
    	 */
    	@RequestMapping(value = "/getUserInfo")
    	public String getUserInfo(HttpSession session) {
    		String username = (String) session.getAttribute("username");
    		String msg = String.format("获取到的当前服务端口:%s,用户名称为:%s,Session ID为:%s", port, username, session.getId());
    		logger.info(msg);
    		return msg;
    	}
    
    }
    
    • 创建启动类
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * 启动类
     * 
     * @author CL
     *
     */
    @SpringBootApplication
    public class Application {
    
    	public static void main(String[] args) {
    		SpringApplication.run(Application.class, args);
    	}
    
    }
    
    • 创建配置文件application.yml
    server:
       port: ${PORT:8080}
    
    • 打jar包
    #CMD进入项目根路径执行命令
    mvn clean install
    
    upstream backend {
        server 192.168.0.100:8081 weight=1; 
        server 192.168.0.100:8082 weight=1; 
    }
    
    server {
        listen  8080;
        server_name  192.168.0.100;
        
        location / {
            proxy_pass  http://backend;
        }
    }
    
    • 上传至服务器并启动
    #启动Nginx
    /usr/local/nginx/sbin/nginx
    
    #启动服务,端口8081
    nohup java -jar -DPORT=8081 spring-session-demo-0.0.1-SNAPSHOT &
    
    #启动服务,端口8082
    nohup java -jar -DPORT=8082 spring-session-demo-0.0.1-SNAPSHOT &
    
    • 测试
        (可选)本地Hosts文件配置域名映射:192.168.0.100 www.c3stones.com
      模拟登录,生成Session:

      获取用户信息:


        从返回可以看出三次调用返回的Session Id均不一致。
        使用Nginx默认轮询策略,第一次登录负载到8081端口服务上,生成Session,浏览器缓存Session,第二次获取用户信息,浏览器将8081生成的Session绑定到请求头,Nginx负载到8082服务上,8082服务找不到该Session信息,重新生成Session,浏览器将新生成的Session替换之前保存的Session,重复循环,造成用户登录后,两个服务均无用户信息。

    4. Spring Session概述

      Spring Session是Spring家族中的一个子项目,Spring Session提供了用于管理用户会话信息的API和实现。它把Servlet容器实现的HttpSession替换为Spring Session,专注于解决Session管理问题,Session信息存储在Redis中,可简单快速的集成到应用中;
      Spring Session官网地址:https://spring.io/projects/spring-session
      Spring Session的特性:
        a. 提供用户Session管理的API和实现;
        b. 提供HttpSession,取代web容器的Session;
        c. 支持集群的Session处理,解决集群下的Session共享问题;

    5. 解决问题,实现Session共享

    • 修改pom.xml文件,添加配置
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<groupId>com.c3stones</groupId>
    	<artifactId>spring-session-demo</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>spring-session-demo</name>
    	<description>Spring Boot Session Demo</description>
    
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.2.6.RELEASE</version>
    		<relativePath />
    	</parent>
    
    	<properties>
    		<java.version>1.8</java.version>
    		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
    	</properties>
    
    	<dependencies>
    		<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>
    		<dependency>
    			<groupId>org.apache.commons</groupId>
    			<artifactId>commons-pool2</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>redis.clients</groupId>
    			<artifactId>jedis</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    
    server:
       port: ${PORT:8080}
       
    spring:
       redis:
          database: 0
          host: 192.168.0.100
          port: 6379
          password: 123456
          jedis:
             pool:
                max-active: 8
                max-wait: -1
                max-idle: 8
                min-idle: 0
          timeout: 10000
    
    • 添加Session配置类
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
    import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
    import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
    
    /**
     * Session配置类
     * 
     * @EnableRedisHttpSession:启用支持Redis存储Session</br>
     * 												maxInactiveIntervalInSeconds:Session最大过期时间
     * 
     * @author CL
     *
     */
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
    public class SessionConfig {
    
    	@Value("${spring.redis.database}")
    	private Integer database;
    
    	@Value("${spring.redis.host}")
    	private String host;
    
    	@Value("${spring.redis.port}")
    	private Integer port;
    
    	@Value("${spring.redis.password}")
    	private String password;
    
    	/**
    	 * 注入Redis连接工厂
    	 * 
    	 * @return
    	 */
    	@Bean
    	public JedisConnectionFactory connectionFactory() {
    		RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
    		configuration.setHostName(host);
    		configuration.setPort(port);
    		configuration.setPassword(password);
    		configuration.setDatabase(database);
    		return new JedisConnectionFactory(configuration);
    	}
    
    }
    
    • 添加Session初始化类
    import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
    
    /**
     * Session初始化,向Servlet容器中添加SpringSessionRepositoryFilter
     * 
     * @author CL
     *
     */
    public class SessionInitializer extends AbstractHttpSessionApplicationInitializer {
    
    	public SessionInitializer() {
    		super(SessionConfig.class);
    	}
    
    }
    
    • 添加线程池配置类
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    
    /**
     * 线程池配置
     * 
     * @author CL
     *
     */
    @Configuration
    public class ThreadPoolConfig {
    
    	/**
    	 * Spring Session任务执行器配置
    	 * 
    	 * @return
    	 */
    	@Bean
    	public ThreadPoolTaskExecutor springSessionRedisTaskExecutor() {
    		ThreadPoolTaskExecutor springSessionRedisTaskExecutor = new ThreadPoolTaskExecutor();
    		springSessionRedisTaskExecutor.setCorePoolSize(8);
    		springSessionRedisTaskExecutor.setMaxPoolSize(16);
    		springSessionRedisTaskExecutor.setKeepAliveSeconds(10);
    		springSessionRedisTaskExecutor.setQueueCapacity(1000);
    		springSessionRedisTaskExecutor.setThreadNamePrefix("spring-session-thread:");
    		return springSessionRedisTaskExecutor;
    	}
    }
    
    • 重新打包,部署
    • 测试
      模拟登录,生成Session:

      获取用户信息:


        从返回可以看出三次调用返回的Session Id一致,已经实现了Session共享问题。
    • 查看Redis

        a.Spring Session定时任务触发Session过期,数据类型:Set
          Key:spring:session:expirations:XXXXX
        b.存储Session,数据类型:Hash
          Key:spring:session:sessions:XXXXXXX
        c. Redis TTL触发Session过期,数据类型:String
          Key:spring:session:sessions:expires:XXXXX

    6. 实现原理

      当Web服务器接收到http请求后,请求进入SpringSessionRepositoryFilter,将原本需要由Web服务器创建会话的过程转交给Spring Session进行创建,本来创建的会话保存在Web服务器内存中,通过Spring Session创建的会话信息保存第三方的服务中,如:Redis、Mysql等。Web服务器之间通过连接第三方服务来共享数据,实现Session共享!

    7. 项目地址

      spring-session-demo

  • 相关阅读:
    P4715 【深基16.例1】淘汰赛
    P4913 【深基16.例3】二叉树深度
    P1478 陶陶摘苹果(升级版)
    P1223 排队接水
    【深基12.例1】部分背包问题
    全排列和组合
    P1036 选数
    100——第25例
    100——第24例
    100——第23例
  • 原文地址:https://www.cnblogs.com/cao-lei/p/13038893.html
Copyright © 2011-2022 走看看