五、Spring Session—基于Redis的分布式session共享
5.1 背景
我们在开发的过程中,有很多时候常常陷入追求一个功能或者系统的高性能,但却忽略了高可用。
我们都知道HttpSession是通过Servlet容器创建和管理的,像Tomcat/Jetty都是保存在内存中的,而如果我们把web服务器搭建成分布式的集群,然后利用LVS或Nginx做负载均衡,那么来自同一用户的Http请求将有可能被分发到两个不同的web站点中去。那么问题就来了,如果不保证系统的高可用行,如何保证不同的web站点能够共享同一份session数据呢?
当同一个用户通过浏览器去访问时:
这时如果TomcatA修改session中的数据,TomcatB中的session并不会改变,这就会出现session不一致,那么这个问题该怎么解决呢?
5.2 session不一致问题解决方案
-
Session 黏连
使用 nginx 实现会话黏连,将相同 sessionid 的浏览器所发起的请求,转发到同一台服务器。这样,就不会存在多个 Web 服务器创建多个 Session 的情况,也就不会发生 Session 不一致的问题。
也就是说通过nginx的负载均衡做 ip_hash ,路由到特定的服务器上(通过客户端请求ip进行hash,再通过hash值选择后端server)来实现。
不过,这种方式常会出现单点故障,目前基本不被采用。因为,如果一台服务器重启,那么会导致转发到这个服务器上的 Session 全部丢失。
注:ip_hash是通过IP地址做循环,循环时只是将IP的前三个端作为参数加入hash函数。这样做的目的是保证ip地址前三位相同的用户经过hash计算将分配到相同的后端server。
-
Session 复制
Web 服务器之间,进行 Session 复制同步。仅仅适用于实现 Session 复制的 Web 容器,例如说 Tomcat 、Weblogic 等等。
不过,这种方式目前基本也不被采用。试想一下,如果我们有 5 台 Web 服务器,所有的 Session 都要同步到每一个节点上,一个是效率低,一个是浪费内存。
具体怎么实现这种方式,可以自己去查阅一下有关资料哈。
-
Session 外部化存储
不同于上述的两种方案,Session 外部化存储,考虑不再采用 Web 容器的内存中存储 Session ,而是将 Session 存储外部化,持久化到 MySQL、Redis、MongoDB 等等数据库中。这样,Tomcat 就可以无状态化,专注提供 Web 服务或者 API 接口,未来拓展扩容也变得更加容易。
这种方式就是我们的“主角”!
5.3 Spring Session概述
查看官方文档:https://spring.io/projects/spring-session 。
Spring Session是Spring的项目之一,把servlet容器实现的httpSession替换为spring-session,专注于解决session管理问题。Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。
spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。
5.4 Spring Session+Redis快速入门
创建一个springboot项目模块,不会建的童鞋可以参考一下:https://blog.csdn.net/qq_50994235/article/details/119330209,我这里还是基于:通过Dubbo来实现服务消费方远程调用服务提供方的方法创建的两个模块springboot_dubbo_provider和springboot_dubbo_consumer来做说明:
废话不多说,直接上代码!
配置代码
-
服务提供方
-
引入相关依赖
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.ebuy</groupId> <artifactId>springboot_dubbo_provider</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot_dubbo_provider</name> <description>Demo project for Spring Boot</description> <properties> <java.version>9</java.version> </properties> <dependencies> <!--是一个web场景启动器,启动的是springboot的web场景 —— springweb核心组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--启动web场景需要的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--测试场景的启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--dubbo —— springboot依赖--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.6</version> </dependency> <!--注册中心zookeeper--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-zookeeper</artifactId> <version>2.7.6</version> <exclusions> <exclusion> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </exclusion> </exclusions> </dependency> <!--选举集群中的leader--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.2.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
应用配置文件
server: port: 8081 #模拟配置8081和8082两个服务提供方 dubbo: application: name: springboot_dubbo_provider registry: address: 127.0.0.1:2181 #注册中心地址 protocol: zookeeper protocol: port: 20881 #注册端口号 注册20881和20882两个端口,与8081和8082对应 name: dubbo #注册协议 #只要Spring Boot启动文件和要扫描的文件在同一级目录下,就不用配置扫描 scan: base-packages: cn.ebuy.service.impl
-
接口及实现类(只是为了测试是否遵循负载均衡策略)
//接口 package cn.ebuy.service; public interface HelloService { public String sayHello(String name); } //实现类 package cn.ebuy.service.impl; import cn.ebuy.service.HelloService; import org.apache.dubbo.config.annotation.Service; @Service(loadbalance = "roundrobin") public class HelloServiceImpl implements HelloService{ @Override public String sayHello(String name) { return "hello---8082---20882----"+name; } }
-
启动类
package cn.ebuy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringbootDubboProviderApplication { public static void main(String[] args) { SpringApplication.run(SpringbootDubboProviderApplication.class, args); } }
-
-
服务消费方
-
引入依赖
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!--这是Spring Boot的父级依赖,这样当前的项目就是Spring Boot项目了。 spring-boot-starter-parent是一个特殊的starter,它用来提供相关的Maven默认依赖。 使用它之后,常用的包依赖可以省去version标签--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.ebuy</groupId> <artifactId>springboot_dubbo_consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot_dubbo_consumer</name> <description>Demo project for Spring Boot</description> <properties> <java.version>9</java.version> </properties> <dependencies> <!--是一个web场景启动器,启动的是springboot的web场景 —— springweb核心组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--启动web场景需要的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--测试场景的启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--dubbo —— springboot依赖--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.6</version> </dependency> <!--注册中心zookeeper--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-zookeeper</artifactId> <version>2.7.6</version> <exclusions> <exclusion> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </exclusion> </exclusions> </dependency> <!--选举集群中的leader--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.2.0</version> </dependency> <!-- 实现对Spring Session使用Redis作为数据源的自动化配置 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!-- 实现对Spring Data Redis的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <!-- 去掉对 Lettuce 的依赖,因为Spring Boot优先使用Lettuce作为Redis客户端 --> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入Jedis的依赖,这样 Spring Boot实现对Jedis的自动化配置 --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
应用配置文件
server: port: 2011 #模拟配置2011 2012 2013三个服务消费方 dubbo: application: name: springboot_dubbo_consumer registry: address: 127.0.0.1:2181 #注册中心地址 一个注册中心 protocol: zookeeper check: false spring: #配置spring session保存在redis中 redis: password: 123456 #redis服务器密码,默认为空,自己设置。 jedis: pool: max-active: 8 #连接池最大连接数,默认为0,使用负数表示没有限制。 max-idle: 8 #默认连接数最大空闲的连接数,默认为8,使用负数表示没有限制。 min-idle: 0 #默认连接池最小空闲的连接数,默认为0,允许设置0和正数。 max-wait: -1 #连接池最大阻塞等待时间,单位:毫秒。默认为-1,表示不限制。 host: 127.0.0.1 port: 6379
-
SessionConfiguration
package cn.ebuy.util; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; /** * 自定义RedisSerializer并返回成json格式的数据,便于查看session中的数据 */ @Configuration //自动化配置Spring Session使用Redis作为数据源 @EnableRedisHttpSession public class SessionConfiguration { //创建 {@link RedisOperationsSessionRepository}使用的RedisSerializer Bean 。 @Bean(name = "springSessionDefaultRedisSerializer") /** * 注意:RedisSerializer的name要命名为"springSessionDefaultRedisSerializer", * 否则这个redis序列器是不生效的。 * * 在@EnableRedisHttpSession这个注解中有@Import({RedisHttpSessionConfiguration.class}) * 导入了RedisHttpSessionConfiguration配置,而这个类RedisHttpSessionConfiguration中标注了 * @Qualifier("springSessionDefaultRedisSerializer"),所以必须命名"springSessionDefaultRedisSerializer", * 否则这个redis序列器是不生效的。 */ public RedisSerializer springSessionDefaultRedisSerializer(){ //采用JSON序列化方式。因为默认情况下,采用 Java自带的序列化方式 ,可读性很差,所以进行替换。 return RedisSerializer.json(); } }
-
接口直接复制即可
-
controller
package cn.ebuy.controller; import cn.ebuy.service.HelloService; import org.apache.dubbo.config.annotation.Reference; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; @Controller @RequestMapping("/demo") public class HelloController { @Reference HelloService helloService; int port=2011; //模拟三个端口号 @RequestMapping("/toIndex") @ResponseBody public String doLogin(HttpServletRequest request){ System.out.println(port); request.getSession().setAttribute("USERS","zhangsan"); return "success"; } @RequestMapping("/doIndex") @ResponseBody public String doIndex(HttpServletRequest request){ System.out.println(port+"=="+request.getSession().getAttribute("USERS")); return (String)request.getSession().getAttribute("USERS"); } }
-
启动类
package cn.ebuy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringbootDubboConsumerApplication { public static void main(String[] args) { SpringApplication.run(SpringbootDubboConsumerApplication.class, args); } }
-
运行测试
-
启动redis
-
启动zookeeper并发布服务
-
在页面上输入地址:http://www.ebuy.cn/demo/toIndex进行访问测试
-
在页面上输入地址:http://www.ebuy.cn/demo/doIndex进行访问测试
这不就欧了嘛…