在微服务架构中,往往由多个微服务共同⽀撑前端请求,如果涉及到⽤户状态就需要考虑分布式 Session 管


理问题,⽐如⽤户登录请求分发在服务器 A ,⽤户购买请求分发到了服务器 B , 那么服务器就必须可以获取


到⽤户的登录信息,否则就会影响正常交易。因此,在分布式架构或微服务架构下,必须保证⼀个应⽤服务


器上保存 Session 后,其他应⽤服务器可以同步或共享这个 Session 。


⽬前主流的分布式 Session 管理有两种⽅案。


Session 复制


部分 Web 服务器能够⽀持 Session 复制功能,如 Tomcat 。⽤户可以通过修改 Web 服务器的配置⽂件,让


Web 服务器进⾏ Session 复制,保持每⼀个服务器节点的 Session 数据都能达到⼀致。


这种⽅案的实现依赖于 Web 服务器,需要 Web 服务器有 Session 复制功能。当 Web 应⽤中 Session 数量


较多的时候,每个服务器节点都需要有⼀部分内存⽤来存放 Session ,将会占⽤⼤量内存资源。同时⼤量的


Session 对象通过⽹络传输进⾏复制,不但占⽤了⽹络资源,还会因为复制同步出现延迟,导致程序运⾏错


误。


在微服务架构中,往往需要 N 个服务端来共同⽀持服务,不建议采⽤这种⽅案。


Session 集中存储


在单独的服务器或服务器集群上使⽤缓存技术,如 Redis 存储 Session 数据,集中管理所有的 Session ,所


有的 Web 服务器都从这个存储介质中存取对应的 Session ,实现 Session 共享。将 Session 信息从应⽤中


剥离出来后,其实就达到了服务的⽆状态化,这样就⽅便在业务极速发展时⽔平扩充。


在微服务架构下,推荐采⽤此⽅案,接下来详细介绍。


Session 共享

Session

什么是 Session


由于 HTTP 协议是⽆状态的协议,因⽽服务端需要记录⽤户的状态时,就需要⽤某种机制来识具体的⽤户。


Session 是另⼀种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,⽽ Session 保存在服务器


上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session 。


客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。


为什么需要 Session 共享


在互联⽹⾏业中⽤户量访问巨⼤,往往需要多个节点共同对外提供某⼀种服务,如下图:




redis怎么session共享 redis怎么实现session共享_Spring Session 


 


⽤户的请求⾸先会到达前置⽹关,前置⽹关根据路由策略将请求分发到后端的服务器,这就会出现第⼀次的


请求会交给服务器 A 处理,下次的请求可能会是服务 B 处理,如果不做 Session 共享的话,就有可能出现⽤


户在服务 A 登录了,下次请求的时候到达服务 B ⼜要求⽤户重新登录。


前置⽹关我们⼀般使⽤ lvs 、 Nginx 或者 F5 等软硬件,有些软件可以指定策略让⽤户每次请求都分发到同⼀


台服务器中,这也有个弊端,如果当其中⼀台服务 Down 掉之后,就会出现⼀批⽤户交易失效。在实际⼯作


中我们建议使⽤外部的缓存设备来共享 Session ,避免单个节点挂掉⽽影响服务,使⽤外部缓存 Session


后,我们的共享数据都会放到外部缓存容器中,服务本身就会变成⽆状态的服务,可以随意的根据流量的⼤


⼩增加或者减少负载的设备。


Spring 官⽅针对 Session 管理这个问题,提供了专⻔的组件 Spring Session ,使⽤ Spring Session 在项⽬中


集成分布式 Session ⾮常⽅便。


Spring Session


Spring Session 提供了⼀套创建和管理 Servlet HttpSession 的⽅案。 Spring Session 提供了集群


Session ( Clustered Sessions )功能,默认采⽤外置的 Redis 来存储 Session 数据,以此来解决 Session 共


享的问题。


Spring Session 为企业级 Java 应⽤的 Session 管理带来了⾰新,使得以下的功能更加容易实现:


  • API 和⽤于管理⽤户会话的实现;
  • HttpSession,允许以应⽤程序容器(即 Tomcat)中性的⽅式替换 HttpSession;
  • 将 Session 所保存的状态卸载到特定的外部 Session 存储中,如 Redis 或 Apache Geode 中,它们能 够以独⽴于应⽤服务器的⽅式提供⾼质量的集群;
  • ⽀持每个浏览器上使⽤多个 Session,从⽽能够很容易地构建更加丰富的终端⽤户体验;
  • 控制 Session ID 如何在客户端和服务器之间进⾏交换,这样的话就能很容易地编写 Restful API,因为 它可以从 HTTP 头信息中获取 Session ID,⽽不必再依赖于 cookie;GitChat
  • 当⽤户使⽤ WebSocket 发送请求的时候,能够保持 HttpSession 处于活跃状态。

需要说明的很重要的⼀点就是, Spring Session 的核⼼项⽬并不依赖于 Spring 框架,因此,我们甚⾄能够将


其应⽤于不使⽤ Spring 框架的项⽬中。


Spring 为 Spring Session 和 Redis 的集成提供了组件: spring-session-data-redis ,接下来演示如何使⽤。


快速集成

引⼊依赖包


<dependency>
 <groupId>org.springframework.session</groupId>
 <artifactId>spring-session-data-redis</artifactId>
</dependency>

添加配置⽂件


 


# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnico
de=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA 配置
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql= true
# Redis 配置
# Redis 数据库索引(默认为0)
spring.redis.database=0 
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端⼝
spring.redis.port=6379 
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最⼤连接数(使⽤负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.shutdown-timeout=100
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0


整体配置分为三块:数据库配置、 JPA 配置、 Redis 配置,具体配置项在前⾯课程都有所介绍。


在项⽬中创建 SessionConfifig 类,使⽤注解配置其过期时间。 GitChat


Session 配置:


@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
}


maxInactiveIntervalInSeconds: 设置 Session 失效时间,使⽤ Redis Session 之后,原 Spring Boot 中


的 server.session.timeout 属性不再⽣效。


仅仅需要这两步 Spring Boot 分布式 Session 就配置完成了。


测试验证


我们在 Web 层写两个⽅法进⾏验证。


@RequestMapping(value = "/setSession")
public Map<String, Object> setSession (HttpServletRequest request){
 Map<String, Object> map = new HashMap<>();
 request.getSession().setAttribute("message", request.getRequestURL());
 map.put("request Url", request.getRequestURL());
 return map;
}


上述⽅法中获取本次请求的请求地址,并把请求地址放⼊ Key 为 message 的 Session 中,同时结果返回⻚


⾯。


@RequestMapping(value = "/getSession")
public Object getSession (HttpServletRequest request){
 Map<String, Object> map = new HashMap<>();
 map.put("sessionId", request.getSession().getId());
 map.put("message", request.getSession().getAttribute("message"));
 return map;
}


getSession() ⽅法获取 Session 中的 Session Id 和 Key 为 message 的信息,将获取到的信息封装到 Map 中


并在⻚⾯展示。


 


在测试前我们需要将项⽬ spring-boot-redis-session 复制⼀份,改名为 spring-boot-redis-session-1 并将端⼝


改为: 9090(server.port=9090) 。修改完成后依次启动两个项⽬。


 


⾸先访问 8080 端⼝的服务,浏览器输⼊⽹址 http://localhost:8080/setSession ,返


回: {"request Url":"http://localhost:8080/setSession"} ;浏览器栏输⼊⽹址


http://localhost:8080/getSession ,返回信息如下:


 


{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:80
80/setSession"}


说明 Url 地址信息已经存⼊到 Session 中。


访问 9090 端⼝的服务,浏览器栏输⼊⽹址 http://localhost:9090/getSession ,返回信息如下:


{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:80
80/setSession"}


通过对⽐发现, 8080 和 9090 服务返回的 Session 信息完全⼀致,说明已经实现了 Session 共享。


模拟登录


在实际中作中常常使⽤共享 Session 的⽅式去保存⽤户的登录状态,避免⽤户在不同的⻚⾯多次登录。我们


来简单模拟⼀下这个场景,假设有⼀个 index ⻚⾯,必须是登录的⽤户才可以访问,如果⽤户没有登录给出


请登录的提示。在⼀台实例上登录后,再次访问另外⼀台的 index 看它是否需要再次登录,来验证统⼀登录


是否成功。


添加登录⽅法,登录成功后将⽤户信息存放到 Session 中。


@RequestMapping(value = "/login")
public String login (HttpServletRequest request,String userName,String password){
 String msg="logon failure!";
 User user= userRepository.findByUserName(userName);
 if (user!=null && user.getPassword().equals(password)){
 request.getSession().setAttribute("user",user);
 msg="login successful!";
 }
 return msg;
}


通过 JPA 的⽅式查询数据库中的⽤户名和密码,通过对⽐判断是否登录成功,成功后将⽤户信息存储到


Session 中。


在添加⼀个登出的⽅法,清除掉⽤户的 Session 信息。


@RequestMapping(value = "/loginout")
public String loginout (HttpServletRequest request){
 request.getSession().removeAttribute("user");
 return "loginout successful!";
}


定义 index ⽅法,只有⽤户登录之后才会看到: index content ,否则提示请先登录。


 


@RequestMapping(value = "/index")
public String index (HttpServletRequest request){
 String msg="index content";
 Object user= request.getSession().getAttribute("user");
 if (user==null){
 msg="please login first!";
 }
 return msg;
}


和上⾯⼀样我们需要将项⽬复制为两个,第⼆个项⽬的端⼝改为 9090 ,依次启动两个项⽬。在 test 数据库


中的 user 表添加⼀个⽤户名为 neo ,密码为 123456 的⽤户,脚本如下:


INSERT INTO `user` VALUES ('1', 'ityouknow@126.com', 'smile', '123456', '2018', 'n
eo');


也可以利⽤ Spring Data JPA 特性在应⽤启动时完成数据初始化:当配置 spring.jpa.hibernate.ddl-auto


: create-drop ,在应⽤启动时,⾃动根据 Entity ⽣成表,并且执⾏ classpath 下的 import.sql 。


 


⾸先测试 8080 端⼝的服务,直接访问⽹址 http://localhost:8080/index ,返回: please login fifirst !提示请先


登录。我们将验证⽤户名为 neo ,密码为 123456 的⽤户登录。访问地址 http://localhost:8080/login?


userName=neo&password=123456 模拟⽤户登录,返回: login successful! ,提示登录成功。我们再次访问


地址 http://localhost:8080/index ,返回 index content 说明已经可以查看受限的资源。


 


再来测试 9090 端⼝的服务,直接访问⽹址 http://localhost:9090/index ,⻚⾯返回 index content ,并没有提


示请先进⾏登录,这说明 9090 服务已经同步了⽤户的登录状态,达到了统⼀登录的⽬的。


 


我们在 8080 服务上测试⽤户退出系统,再来验证 9090 的⽤户登录状态是否同步失效。⾸先访问地址


http://localhost:8080/loginout 模拟⽤户在 8080 服务上退出,访问⽹址 http://localhost:8080/index ,返回


please login fifirst !说明⽤户在 8080 服务上已经退出。再次访问地址 http://localhost:9090/index ,⻚⾯返


回: please login fifirst !,说明 9090 服务上的退出状态也进⾏了同步。


 


注意 ,本次实验只是简单模拟统⼀登录,实际⽣产中我们会以 Filter 的⽅式对登录状态进⾏校验,在本


课程的最后⼀节课中也会讲到这⽅⾯的内容。


 


我们最后来看⼀下,使⽤ Redis 作为 Session 共享之后的示意图:



redis怎么session共享 redis怎么实现session共享_redis怎么session共享_02


从上图可以看出,所有的服务都将 Session 的信息存储到 Redis 集群中,⽆论是对 Session 的注销、更新都


会同步到集群中,达到了 Session 共享的⽬的。


总结


在微服务架构下,系统被分割成⼤量的⼩⽽相互关联的微服务,因此需要考虑分布式 Session 管理,⽅便平


台架构升级时⽔平扩充。通过向架构中引⼊⾼性能的缓存服务器,将整个微服务架构下的 Session 进⾏统⼀


管理。


Spring Session 是 Spring 官⽅提供的 Session 管理组件,集成到 Spring Boot 项⽬中轻松解决分布式


Session 管理的问题。