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

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

概述:服务器会将某个用户的请求,交割tomcat集群中的某个节点,该节点以后就负责保存该用户的session
- Session绑定可以利用负载均衡的源地址Hash(ip_hash)算法实现
- 负载均衡服务器总是将来源是同一个IP的请求分发到调音台服务器上,也可以根据Cookie信息将同一个用户的请求总是分发到同一个服务器上
- 这样整个会话其间,该用户的所有请求都在同一台服务器上处理,即Session绑定在某台服务器上。保证Session总能在这台服务器上处理。这种方法称为Session绑定/粘滞
优点:不占服务端内存
缺点: 1) 增加新机器,会重新Hash,导致重新登录 2) 应用重启, 需要重新登录 3) 某台服务器宕机,该机器上 的 Session 也就不存在了,用户请求切换到其他机器后因为没有 Session 而无法完成业务处理, 这种方案不符合 系统高可用需求, 使用较少4)前端服务器不能负载均衡不然会导致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的分布式问题
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.4.5</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.9.0</version> </dependency>
<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;
@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); template.setKeySerializer(redisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); 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); 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<>(); redisScript.setLocation(new ClassPathResource("lock.lua")); redisScript.setResultType(Long.class); return redisScript; } }
|