怎么解决redis中分布式session不一致性

68次阅读
没有评论

共计 8935 个字符,预计需要花费 23 分钟才能阅读完成。

这篇文章主要讲解了“怎么解决 redis 中分布式 session 不一致性”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着丸趣 TV 小编的思路慢慢深入,一起来研究和学习“怎么解决 redis 中分布式 session 不一致性”吧!

分布式 session 不一致性解决方案

一、Session 有什么作用?

Session 是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息。【相关推荐:Redis 视频教程】

客户端在第一次访问服务端的时候,服务端会响应一个 sessionId 并且将它存入到本地 cookie 中,在之后的访问会将 cookie 中的 sessionId 放入到请求头中去访问服务器,

如果通过这个 sessionId 没有找到对应的数据, 那么服务器会创建一个新的 sessionId 并且响应给客户端。

二、分布式 Session 有什么问题?

单服务器 web 应用中,session 信息只需存在该服务器中,这是我们前几年最常接触的方式

但是近几年随着分布式系统的流行,单系统已经不能满足日益增长的百万级用户的需求,集群方式部署服务器已在很多公司运用起来

当高并发量的请求到达服务端的时候通过负载均衡的方式分发到集群中的某个服务器,这样就有可能导致同一个用户的多次请求被分发到集群的不同服务器上,就会出现取不到 session 数据的情况,于是 session 的共享就成了一个问题。

三、服务做集群一般是怎么样做的?

SpringBoot 项目,那么只要改下端口号启动几个,然后用 nginx 统一做反向代理。

SpringCloud 微服务项目,那么这个时候,可以使用 ribbon 本地负载均衡。

四、nginx 负载均衡和 ribbon 负载均衡的区别

nginx 做负载均衡是服务器端的负载均衡,统一访问一个地址,根据负载均衡算法访问决定访问那一个服务器。

ribbon 负载均衡,这是本地负载均衡(客户端负载均衡),把提供服务的客户端地址都缓存记录下来,根据本地的算法实现负载均衡。

五、Session 一致性解决方案

1. session 复制(同步)

思路:多个服务端之间相互同步 session,这样每个服务端之间都包含全部的 session

优点:服务端支持的功能,应用程序不需要修改代码

缺点:

session 的同步需要数据传输,占内网带宽,有时延

所有服务端都包含所有 session 数据,数据量受内存限制,无法水平扩展

2. 客户端存储法

思路:服务端存储所有用户的 session,内存占用较大,可以将 session 存储到浏览器 cookie 中,每个端只要存储一个用户的数据了

优点:服务端不需要存储

缺点:

每次 http 请求都携带 session,占外网带宽

数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患

session 存储的数据大小和域名 cookie 个数都受限制的

注:该方案虽然不常用,但确实是一种思路。

3. 反向代理 hash 一致性

思路:服务端为了保证高可用,有多台冗余,反向代理层能不能做一些事情,让同一个用户的请求保证落在一台服务端上呢?

方案一:四层代理 hash

反向代理层使用用户的 ip 来做 hash,以保证同一个 ip 的请求落在同一个服务端上

方案二:七层代理 hash

反向代理使用 http 协议中的某些业务属性来做 hash,例如 sid,city_id,user_id 等,能够更加灵活的实施 hash 策略,以保证同一个浏览器用户的请求落在同一个服务器上

优点:

只需要改 nginx 配置,不需要修改应用代码

负载均衡,只要 hash 属性是均匀的,多台服务端的负载是均衡的

可以支持服务端水平扩展(session 同步法是不行的,受内存限制)

缺点:

如果服务端重启,一部分 session 会丢失,产生业务影响,例如部分用户重新登录

如果服务端水平扩展,rehash 后 session 重新分布,也会有一部分用户路由不到正确的 session

session 一般是有有效期的,所有不足中的两点,可以认为等同于部分 session 失效,一般问题不大。

对于四层 hash 还是七层 hash,个人推荐前者:让专业的软件做专业的事情,反向代理就负责转发,尽量不要引入应用层业务属性,除非不得不这么做(例如,有时候多机房多活需要按照业务属性路由到不同机房的服务器)。

四层、七层负载均衡的区别

4. 后端统一集中存储

优点:

没有安全隐患

可以水平扩展,数据库 / 缓存水平切分即可

服务端重启或者扩容都不会有 session 丢失

不足:增加了一次网络调用,并且需要修改应用代码

对于 db 存储还是 cache,个人推荐后者:session 读取的频率会很高,数据库压力会比较大。如果有 session 高可用需求,cache 可以做高可用,但大部分情况下 session 可以丢失,一般也不需要考虑高可用。

总结

保证 session 一致性的架构设计常见方法:

session 同步法:多台服务端相互同步数据

客户端存储法 一个用户只存储自己的数据

反向代理 hash 一致性 四层 hash 和七层 hash 都可以做,保证一个用户的请求落在一台服务端上

后端统一存储 服务端重启和扩容,session 也不会丢失(推荐后端 cache 统一存储)

六、案例实战:SpringSession+redis 解决分布式 session 不一致性问题

步骤 1:加入 SpringSession、redis 的依赖包

dependency 
  groupId org.springframework.boot /groupId 
  artifactId spring-boot-starter-redis /artifactId 
  version 1.4.7.RELEASE /version 
 /dependency 
 dependency 
  groupId org.springframework.session /groupId 
  artifactId spring-session-data-redis /artifactId 
 /dependency

步骤 2:配置文件

#  为某个包目录下   设置日志
logging.level.com.ljw=debug
#  设置 session 的存储方式,采用 redis 存储
spring.session.store-type=redis
# session 有效时长为 10 分钟
server.servlet.session.timeout=PT10M
## Redis  配置
## Redis 数据库索引(默认为 0)spring.redis.database=0
## Redis 服务器地址
spring.redis.host=127.0.0.1
## Redis 服务器连接端口
spring.redis.port=6379
## Redis 服务器连接密码(默认为空)spring.redis.password=

步骤 3:配置拦截器

@Configuration
public class SessionConfig implements WebMvcConfigurer {
 @Override
 public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityInterceptor())
 // 排除拦截的 2 个路径
 .excludePathPatterns(/user/login)
 .excludePathPatterns(/user/logout)
 // 拦截所有 URL 路径
 .addPathPatterns( /** 
 }
}
@Configuration
public class SecurityInterceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { HttpSession session = request.getSession();
 // 验证当前 session 是否存在,存在返回 true true 代表能正常处理业务逻辑
 if (session.getAttribute(session.getId()) != null){ log.info( session 拦截器,session={},验证通过 ,session.getId());
 return true;
 }
 //session 不存在,返回 false,并提示请重新登录。 response.setCharacterEncoding( UTF-8 
 response.setContentType( application/json; charset=utf-8 
 response.getWriter().write( 请登录!!!!! log.info(session 拦截器,session={},验证失败 ,session.getId());
 return false;
 }
}

HandlerInterceptor

preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理;

postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了 Service 并返回 ModelAndView,但未进行页面渲染),有机会修改 ModelAndView

afterCompletion:在 DispatcherServlet 完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面)

步骤 4:控制器

@RestController
@RequestMapping(value =  /user)
public class UserController { Map String, User  userMap = new HashMap ();
 public UserController() {
 // 初始化 2 个用户,用于模拟登录
 User u1=new User(1, user1 , user1 
 userMap.put(user1 ,u1);
 User u2=new User(2, user2 , user2 
 userMap.put(user2 ,u2);
 }
 @GetMapping(value =  /login)
 public String login(String username, String password, HttpSession session) {
 // 模拟数据库的查找
 User user = this.userMap.get(username);
 if (user != null) { if (!password.equals(user.getPassword())) {
 return  用户名或密码错误!!! } else { session.setAttribute(session.getId(), user);
 log.info(登录成功 {} ,user);
 }
 } else {
 return  用户名或密码错误!!! }
 return  登录成功!!! }
 /**
 *  通过用户名查找用户
 */
 @GetMapping(value =  /find/{username} )
 public User find(@PathVariable String username) { User user=this.userMap.get(username);
 log.info(通过用户名 ={}, 查找出用户 {} ,username,user);
 return user;
 }
 /**
 * 拿当前用户的 session
 */
 @GetMapping(value =  /session)
 public String session(HttpSession session) { log.info( 当前用户的 session={} ,session.getId());
 return session.getId();
 }
 /**
 *  退出登录
 */
 @GetMapping(value =  /logout)
 public String logout(HttpSession session) { log.info( 退出登录 session={} ,session.getId());
 session.removeAttribute(session.getId());
 return  成功退出!! }
}

步骤 5:实体类

@Data
public class User implements Serializable{
 private int id;
 private String username;
 private String password;
 public User(int id, String username, String password) {
 this.id = id;
 this.username = username;
 this.password = password;
 }
}

步骤 6:访问测试

先登录:http://127.0.0.1:8080/user/login?username=user1 password=user1

再查询 http://127.0.0.1:8080/user/find/user1

七、剖析 SpringSession 的 redis 原理

步骤 1:分析 SpringSession 的 redis 数据结构

127.0.0.1:6379  keys *
1)  spring:session:sessions:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a 
2)  spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b 
3)  spring:session:expirations:1635413520000 
4)  spring:session:sessions:expires:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a 
5)  spring:session:expirations:1635412980000 
6)  spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b

共同点:3 个 key 都是以 spring:session: 开头的,代表了 SpringSession 的 redis 数据。

查询类型

127.0.0.1:6379  type spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
hash
127.0.0.1:6379  hgetall spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
// session 的创建时间
1)  creationTime 
2)  \xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xdb\xecu 
// sesson 的属性,存储了 user 对象
3)  sessionAttr:d3434f61-4d0a-4687-9070-610bd7790f3b 
4)  \xac\xed\x00\x05sr\x00\x1ecom.ljw.redis.controller.User\x16\ _m\x1b\xa0W\x7f\x02\x00\x03I\x00\x02idL\x00\bpasswordt\x00\x12Ljava/lang/String;L\x00\busernameq\x00~\x00\x01xp\x00\x00\x00\x01t\x00\x05user1q\x00~\x00\x03 
// 最后的访问时间
5)  lastAccessedTime 
6)  \xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xe1\xc7\xed 
// 失效时间  100 分钟
7)  maxInactiveInterval 
8)  \xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x17p

步骤 2:分析 SpringSession 的 redis 过期策略

对于过期数据,一般有三种删除策略:

定时删除,即在设置键的过期时间的同时,创建一个定时器,当键的过期时间到来时,立即删除。

惰性删除,即在访问键的时候,判断键是否过期,过期则删除,否则返回该键值。

定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

redis 删除过期数据采用的是懒性删除 + 定期删除组合策略,也就是数据过期了并不会及时被删除。

但由于 redis 是单线程,并且 redis 对删除过期的 key 优先级很低;如果有大量的过期 key,就会出现 key 已经过期但是未删除。

为了实现 session 过期的及时性,spring session 采用了定时删除 + 惰性删除的策略。

定时删除

127.0.0.1:6379  type spring:session:expirations:1635413520000
127.0.0.1:6379  smembers spring:session:expirations:1635413520000
1)  \xac\xed\x00\x05t\x00,expires:d3434f61-4d0a-4687-9070-610bd7790f3b
2)  spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b  
3)  spring:session:expirations:1635413520000  
6)  spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b

1635412980000 是时间戳,等于 2021-10-28 17:23:00,即是该可以在这个时刻过期

springsession 定时(1 分钟)轮询,删除 spring:session:expirations:[?] 的过期成员元素,例如:spring:session:expirations:1635413520000

springsesion 定时检测超时的 key 的值,根据值删除 seesion,例如 key:spring:session:expirations:1635413520000,值为(sessionId):d3434f61-4d0a-4687-9070-610bd7790f3b 的 seesion

惰性删除

127.0.0.1:6379  type spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
string
127.0.0.1:6379  get spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
127.0.0.1:6379  ttl spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
(integer) 3143
127.0.0.1:6379

访问 spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b 的时候,判断 key 是否过期,过期则删除,否则返回改进的值。

例如 访问 spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b 的时候,判断 ttl 是否过期,过期就直接删除

2)  spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b  
3)  spring:session:expirations:1635413520000  
6)  spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b

感谢各位的阅读,以上就是“怎么解决 redis 中分布式 session 不一致性”的内容了,经过本文的学习后,相信大家对怎么解决 redis 中分布式 session 不一致性这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是丸趣 TV,丸趣 TV 小编将为大家推送更多相关知识点的文章,欢迎关注!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-20发表,共计8935字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)