您的位置:首页 > 数据库 > Redis

分布式Session一致性解决方案

2018-08-14 12:41 447 查看
1、session的作用?

服务器为每个用户创建一个会话,存储用户的相关信息,以便多次请求能够定位到同一个上下文。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在 整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

2、分布式session是什么?

单服务器web应用中,session只会存储在该服务器中,而随着日益增长的用户量,单服务节点无法应对大量用户的 访问,我们开始考虑适用服务器集群,例如采用nginx做负载均衡反向代理,那么就会出现用户每次访问都会轮训到 不同的web应用服务器节点,从而导致获取不到Session的情况,这就是我们谈到的分布式session一致性问题。



如下案例

首先我们创建一个项目名称为distributed-session的springboot项目如下

<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>cn.itchao</groupId>
<artifactId>distributed-session</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>

<dependencies>
<!--SpringBoot WEB组件  -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--common-lang3工具包  -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>




书写如下接口

package cn.itchao.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

@Value("${server.port}")
private String port;

@RequestMapping("/createSession")
public String createSession(HttpServletRequest request,String name){
HttpSession session = request.getSession();
session.setAttribute("name", name);
log.info("port:"+port+",sessonId:"+session.getId());
return "创建Session成功-对应的服务器端口号为:"+port;
}

@RequestMapping("/getSession")
public String getSession(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session==null){
return "没有获取到Session"+"-服务器端口号为:"+port;
}
log.info("port:"+port+",sessonId:"+session.getId());
String name = (String) session.getAttribute("name");
return "获取Session成功:"+name+"-对应的服务器端口号为:"+port;
}
}

启动类:

package cn.itchao;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SessionApp {

public static void main(String[] args) throws Exception {
SpringApplication.run(SessionApp.class, args);
}

}

application.yml配置如下:

server:
port: 8080

server:
port: 8081

分别启动两台服务器,本地ip为192.168.100.1。即现在启动了两台服务分别是

192.168.100.1:8080和192.168.100.1:8081



接下来我们配置nginx反向代理负载均衡,我的nginx服务器是在一台ip为192.168.100.131的虚拟机上安装的,本机ip为192.168.100.1,所以我在本地配置了一个域名指向这台服务器的ip。如下:



接下里我在ip为192.168.100.131的虚拟机配置了反向代理如下:



接下来我们创建第一个session:
http://www.itchao.cn/user/createSession?name=user1


发现在8081服务器上创建了session

接着我们在创建第二个session
http://www.itchao.cn/user/createSession?name=user2


发现这时候轮训到8080服务器了

接下来我们来获取这个session
http://www.itchao.cn/user/getSession


发现轮训到8081服务器上没有获取到对应的session

我们再来获取一次Session



发现在8080服务器上获取到了刚才创建session,这是为什么呢?

原因很简单由于这段代码没有设置创建session的时候为false



requset.getSession(false)代表获取session,如果没有不创建,而默认requset.getSession()等同与requset.getSession(true)代表每次获取session如果获取不到创建一个新的session,故此我们在刚才轮训到8081服务器上的时候新建了一个session,把我们设置的name=user1给覆盖掉了,所以我们无法获取到。

每次创建的session,和下一次访问的可能不在一台服务器上,所以不能获取达到session共享从而导致session一致性问题。

那我们应该如何解决这个问题呢?

1、通过数据库存储session信息,每次从数据查询。这种方式会导致数据库压力巨大。不适用

2、通过客户端Cookie写入到本地,这样很不安全,不可采取

3、通过nginx的hash一致性做ip绑定,这样失去了灵活性

4、使用tomcat内部通信机制同步数据通信,这样开销较大,不适合大型项目

5、使用spring-session框架来解决,底层采用重写httpclient保证数据共享

6、使用token令牌代替session功能,把数据存放在redis中,每次从redis中获取数据

接下来我们说说如何使用spring-session框架解决分布式session一致性问题:

首先引入一下依赖

<!-- springboot - Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring session 与redis应用基本环境配置,需要开启redis后才可以使用,不然启动Spring boot会报错 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

编写如下代码

package config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;

public class SessionConfig {
// 冒号后的值为没有配置文件时,自动动装载的默认值
@Value("${redis.hostname:localhost}")
String host;
@Value("${redis.port:6379}")
int port;
@Value("${redis.password:123456}")
String password;

@Bean
public JedisConnectionFactory connectionFactory() {
RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration();
standaloneConfig.setHostName(host);
standaloneConfig.setPort(port);
standaloneConfig.setPassword(RedisPassword.of(password));
return new JedisConnectionFactory(standaloneConfig);

}
}

package cn.itchao.session;

import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;

import config.SessionConfig;

//初始化Session配置
public class SessionInitializer extends AbstractHttpSessionApplicationInitializer {
public SessionInitializer() {
super(SessionConfig.class);
}
}

配置文件中添加如下配置:

server:
port: 8080
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password: 123456
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 8
timeout: 0
session:
store-type: redis

需要依赖redis,请安装redis

接下来我们再次启动8080和8081服务器,测试发现







完美解决session共享问题。

利用token令牌和redis共享分布式集群方式替换Session功能。大概思路原理如下:

用户登录后,生成一个唯一令牌,令牌作为redis的key,用户信息作为key对应的value存入redis中,给设置过期时间为30分钟,然后把令牌返回给客户端,客户端保存令牌到本地Cookie中,以后用在获取用户信息的时候直接发送一个请求到后端,在请求头部传入令牌,后端通过request.getHeader("Authorization");获取到令牌,然后用令牌去redis查询对应得用户信息,如果没有查询到那么代表用户Session失效或者用户未登陆。如果查询到即返回用户信息。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息