什么是分布式Session

在分布式的环境下,Session共享是一个常见的问题。由于HTTP协议是无状态的,用户的信息通常存储在服务器的Session中,并通过JSESSIONID返回给服务器。而在分布式环境里不同服务器之间的Session数据无法共享,导致用户信息无法在不同服务器之间传递。

举个例子:

秒杀系统分布式架构

  1. 在上图秒杀系统分布式架构中,当Nginx对请求进行负载均衡后,可能会对应到不同的Tomcat
  2. 比如第一次秒杀请求,均衡到TomcatA ,这时Session就记录在TomcatA,第二次请求均衡到TomcatB , 就会出现问题,TomcatB会认为用户是第一次进行秒杀,会允许用户进行秒杀(假设每个用户对一个商品只能进行一次购买)
  3. 这样就会造成重复购买

解决方案

解决方案一:Session绑定/粘滞

Session绑定/粘滞图

概述:服务器会将某个用户的请求,交割tomcat集群中的某个节点,该节点以后就负责保存该用户的session

  1. Session绑定可以利用负载均衡的源地址Hash(ip_hash)算法实现
  2. 负载均衡服务器总是将来源是同一个IP的请求分发到调音台服务器上,也可以根据Cookie信息将同一个用户的请求总是分发到同一个服务器上
  3. 这样整个会话其间,该用户的所有请求都在同一台服务器上处理,即Session绑定在某台服务器上。保证Session总能在这台服务器上处理。这种方法称为Session绑定/粘滞

优点:不占服务端内存

缺点: 1) 增加新机器,会重新Hash,导致重新登录 2) 应用重启, 需要重新登录 3) 某台服务器宕机,该机器上 的 Session 也就不存在了,用户请求切换到其他机器后因为没有 Session 而无法完成业务处理, 这种方案不符合 系统高可用需求, 使用较少4)前端服务器不能负载均衡不然会导致Session绑定出现问题

解决方案二:Session复制

Session复制

概述:

  • Session 复制是小型架构使用较多的一种服务器集群 Session 管理机制
  • 应用服务器开启Web 容器的 Session 复制功能,在集群中的几台服务器之间同步 Session 对象,使每台服务器上都保存了所有用户的 Session信息
  • 这样任何一台机器宕机都不会导致 Session数据的丢失,而服务器使用 Session 时, 也只需要在本机获取即可

优点: 无需修改代码,修改Tomcat配置即可

缺点: 1)Session 同步传输占用内网带宽 2) 多台Tomcat同步性能指数级下降 3)Session占用内存,无法有效水平 扩展

解决方案三:前端存储

优点: 不占用服务端内存

缺点: 1) 存在安全风险 2) 数据大小受cookie限制 3) 占用外网带宽

解决方案四:后端集中存储

优点:安全,容易水平扩展

缺点:增加复杂度,需要修改代码

解决方案5:SpringSession 实现分布式 Session

概述:将用户的Session统一放在Redis,从而解决Session的分布式问题

<!--spring data redis 依赖, 即 spring 整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.5</version>
</dependency>
<!--pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
<!--实现分布式session, 即将Session保存到指定的Redis-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

因为是以原生的形式存放,要进行反序列化


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
* 辉学Java
* 辉学Java
* 辉学Java
*/
@Configuration
@EnableCaching //启用缓存
public class redisconfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template =
new RedisTemplate<>();
System.out.println("template=>" + template);//这里可以验证..
RedisSerializer<String> redisSerializer =
new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
//redis 默认是 jdk 的序列化是二进制,这里使用的是通用的json数据,不用传具体的序列化的对象
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer =
new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
//增加配置执行脚本
@Bean
public DefaultRedisScript<Long> script(){
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//设置要执行的lua脚本位置,lock.lua放在resources下
redisScript.setLocation(new ClassPathResource("lock.lua"));
redisScript.setResultType(Long.class);
return redisScript;
}
}