业务场景

在单机系统中,用户登陆之后,服务端会保存用户的会话信息,只要用户不退出重新登陆,在一段时间内用户可以一直访问该网站,无需重复登陆。用户的信息存在服务端的 session 中,session中可以存放服务端需要的一些用户信息,例如用户ID,所属公司companyId,所属部门deptId等等。

但是随着业务的发展,技术架构需要调整,原来的单机系统逐渐被更换,架构由单机扩展到分布式,甚至当下流行的微服务。虽然在用户端看来系统仍然是一个整体,但在技术端来说业务则被拆分成多个模块,各个模块之间相互独立,甚至不在同一台物理机器上,模块之间通过 RPC 进行通信。

那么原来单机只需一份的 session, 如何满足在多系统的运行下保证会话一致性呢?单独保存在任何一个系统中都不合适,而且每个单独模块系统也可能是分布式形式的,是由集群组成。那么session的分配就更复杂了。

Redis 实现

针对以上问题,我们可能会从以下几个方面想到解决的方法,每个服务端存储一份,通过同步的方式保证一致性,但是这种方式有个很明显的缺点:session的同步需要数据传输,占内网带宽,有时延,网络不稳定的时候会造成部分系统同步延迟,那么就不能保证 session 一致性。而且所有服务端都包含所有session数据,数据量受内存限制,无法水平扩展。

那么我们是否可以单独将 session 信息存储在某一个独立的介质中,介质可以是DB也可以是缓存。

考虑到如下业务:登陆的时候我们经常会给用户一个过期时间(一般移动端常设置为7天或者一个月甚至更久),到期后用户需要输入登陆信息重新登陆,即会话过期。这种到期的设置我们自然想到了Redis的 key expire功能,所以最终我们可以将Redis引入进来实现我们的这种需求。系统如下图所示:

我们只需在用户首次登陆的时候将用户信息放到 Token并缓存到 Redis 中,同时设置一个过期时间,伪代码如下:

@Override
public Map login(UserDto dto) {
Map<String, Object> restMap = new HashMap<>(); // 校验登陆信息
User user = checkLoginInfo(dto); //删除旧的token
String token = (String) redisUtils.get(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName()); if (!ObjectUtils.isEmpty(token)) {
redisUtils.delete(CacheConstants.USER_TOKEN_KEY_WEB + token);
}
// 唯一签名信息
String signStr = user.getCompanyId() + user.getUserName() + dto.getPassword() + DateUtils.now().getTime();
token = MD5Utils.md5(signStr);
// 设置用户 token
redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_WEB + token, user.getId(), LOGIN_EXPIRED_TIME);
//缓存新的token
redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName(), token, LOGIN_EXPIRED_TIME);
dto.setCompanyId(user.getCompanyId());
dto.setId(user.getId());
restMap.put("token", token);
restMap.put("userName", user.getUserName());
return restMap;
}

那么在系统中如何使用呢,我们可以定义一个拦截器 SessionInterceptor,当访问 web 接口的时候检验用户的 token 信息,判断用户是否登陆,未登录的情况下一些业务接口是无法访问的,以及在登陆的情况下拿到我们需要的用户信息,如 userId。

public class SessionInterceptor {

    @Autowired
private RedisUtils redisUtils; @Autowired
private UserService userService; @Pointcut("execution(* com.jajian.demo.web.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void controllerMethodPointcut() { } @Around("controllerMethodPointcut()")
public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
if (targetMethod.getDeclaringClass().isAnnotationPresent(NoLogin.class) || targetMethod.isAnnotationPresent(NoLogin.class)) {
return proceedingJoinPoint.proceed();
}
// 从获取RequestAttributes中获取HttpServletRequest的信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); String token = request.getHeader("token"); if(StringUtils.isEmpty(token)){
Log.debug("验证token", "token验证失败,{}", "token不存在");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
Integer userId= (Integer)redisUtils.get(CacheConstants.USER_TOKEN_KEY_WEB + token); if (null == userId) {
Log.debug("验证token", "token验证失败,{}", "token超时");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
User user = userService.getById(userId.longValue());
if (ObjectUtils.isEmpty(user)){
Log.debug("验证token", "token验证失败,{}", "用户信息不存在");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
if (user.getStatus() == UserStatusEnum.NO.getCode() || user.getDeleteFlag() == DeleteFlagEnum.YES.getCode()){
Log.debug("验证token", "token验证失败,用户信息异常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag());
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
return proceedingJoinPoint.proceed();
} }

以上实现方式简单易用,而且Redis 在分布式系统中的使用率也很高,所以无需额外的技术引入。可以支持水平扩展,数据库或缓存水平切分即可,服务端重启或者扩容都不会有session丢失的情况发生。

分布式系统中session一致性问题的更多相关文章

  1. NET Core微服务之路:再谈分布式系统中一致性问题分析

    前言 一致性:很多时候表现在IT系统中,通常在分布式系统中,必须(或最终)为多个节点的数据保持一致.世间万物,也有存在相同的特征或相似,比如儿时的双胞胎,一批工厂流水线的产品,当然,我们不去讨论非IT ...

  2. Session机制详解及分布式中Session共享解决方案

    一.为什么要产生Session http协议本身是无状态的,客户端只需要向服务器请求下载内容,客户端和服务器都不记录彼此的历史信息,每一次请求都是独立的. 为什么是无状态的呢?因为浏览器与服务器是使用 ...

  3. nginx 解决session一致性

    session 粘滞性每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题. upstream backserver {ip_hash;server ...

  4. 分布式系统session一致性的问题

    session的概念 什么是session? 服务器为每个用户创建一个会话,存储用户的相关信息,以便多次请求能够定位到同一个上下文.这样,当用户在应用程序的 Web 页之间跳转时,存储在 Sessio ...

  5. 分布式系统session一致性解决方案

    在单机系统中,不存在Session共享问题,但是在分布式系统中,我们必须实现session共享机制,使得多台应用服务器之间会话统一,如果不进行Session共享会出现数据不一致,比如:会导致请求落到不 ...

  6. nginx负载均衡中利用redis解决session一致性问题

    关于session一致性的现象及原因不是本小作文的重点,可以另行找杜丽娘O(∩_∩)O哈哈~重点是利用redis集中存储共享session的实际操作. 一.业务场景:nginx/tomcat/redi ...

  7. session一致性架构设计

    什么是session? 由于HTTP协议是无状态的协议,因此它不会去记住上一次浏览器访问服务器时的信息.同一个用户的两次操作,与两个不同用户的操作,对它来说是一样的. 这样虽然满足了互联网web应用的 ...

  8. session在什么时候创建,以及session一致性问题

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/wowwilliam0/article/d ...

  9. 【分布式】Zookeeper在大型分布式系统中的应用

    一.前言 上一篇博文讲解了Zookeeper的典型应用场景,在大数据时代,各种分布式系统层出不穷,其中,有很多系统都直接或间接使用了Zookeeper,用来解决诸如配置管理.分布式通知/协调.集群管理 ...

随机推荐

  1. Web开发小贴士 -- 全面了解Cookie

    一.Cookie的出现 浏览器和服务器之间的通信少不了HTTP协议,但是因为HTTP协议是无状态的,所以服务器并不知道上一次浏览器做了什么样的操作,这样严重阻碍了交互式Web应用程序的实现. 针对上述 ...

  2. python编程基础之十

    分支条件: 单一分支条件:if 条件 : 条件真运行... 双分支条件:if 条件 : 条件真运行else: 条件假运行... 多分支条件:if 条件1 : 条件1真运行elif 条件2 : 条件1假 ...

  3. Java工程师学习指南(初级篇)

    Java工程师学习指南 初级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...

  4. MySQL 5.7安装(多实例)最佳实践

    MySQL 5.7安装(多实例)最佳实践,接上一篇<MySQL 5.7安装最佳实践>在已有的实例上面新增一个实例,主要是为了资源合理利用:下面是具体的步骤: 1.新实例目录规划,my.cn ...

  5. Faith 信念

    Today I’d like to talk about faith. With faith, you’ll go further and never be lost. Faith is free a ...

  6. Java序列化总结(最全)

    概念 实现 Serializable 接口, 它只是一个标记接口,不实现也能够进行序列化 RMI: 远程方法调用 RPC: 远程过程调用 序列化ID 解决了序列化与反序列出现代码不一致的问题, 不一致 ...

  7. postman参数化

    1.新建csv文件 2.csv文件中输入变量名和参数 3.postman中新增接口,并设置变量 4.选择进入 5.导入参数化csv格式文件,点击run 查看运行结果

  8. Python_函数传参

    关于函数中传递参数的相关知识 其中 万能参数 第一次听说 但感觉用处不大 后面用到再详细整理

  9. Spring Security 入门 (二)

    我们在篇(一)中已经谈到了默认的登录页面以及默认的登录账号和密码. 在这一篇中我们将自己定义登录页面及账号密码. 我们先从简单的开始吧:设置自定义的账号和密码(并非从数据库读取),虽然意义不大. 上一 ...

  10. 防止CSRF跨站请求伪造

    CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站 ...