应用服务器的高可用架构设计最为理想的是服务无状态,但实际上业务总会有状态的,以session记录用户信息的例子来讲,未登入时,服务器没有记入用户信息的session访问网站都是以游客方式访问的,账号密码登入网站后服务器必须要记录你的用户信息记住你是登入后的状态,以该状态分配给你更多的权限。那么管理session有哪些方法呢?
一、四种分布式Session管理方案
1、Session复制

session复制是早期企业应用系统使用比较多的一种服务器集群Session管理机制。应用服务器开启Web容器的的Session复制功能,在集群中的几台服务器之间同步Session对象,是的每台服务器上都保存所有用户的Session信息,这样任何一台机器宕机都不会导致Session数据的丢失,而服务器使用Session时候,也只需要在本机获取即可。如图1所示。
这种方案简单,且从本机读取session也相当快捷,但有非常明显的缺陷:只能使用在集群规模比较小的情况下(企业应用系统,使用人数少,相对比较常见这种模式),当集群规模比较大的时候,集群服务器之间需要大量的通信进行Session的复制,占用服务器和网络的大量资源,系统负担较大。而且由于用户的session信息在每台服务器上都有备份,在大量用户访问下,可能会出现服务器内存都还不够session使用的情况。
2.session会话保持(黏滞会话)

会话保持是利用负载均衡的原地址Hash算法实现,负载均衡服务器总是将来源于同一IP的请求分发到同一台服务器上,,也可以根据cookie信息将同一个用户的请求每次都分发到同一台服务器上,不过这时的负载均衡服务器必须工作在HTTP协议层上。这种会话保持也叫黏滞会话(Sticky Sessions)
在Nginx中配置的会话保持:
upstream server_dispatcher {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
这种方案虽然保证了每个用户都能准确的拿到自己的session,而且大量用户访问也不怕,但是这种会话保持不符合系统高可用的需求。这种方案有着致命的缺陷:一旦某台服务器发生宕机,则该服务器上的所有session信息就会不存在,用户请求就会切换到其他服务器,而其他服务器因为没有其对应的session信息导致无法完成相关业务。所以这种方法基本上不会被采纳。
3.利用cookie记录session

早期的企业应用系统使用C/S架构,管理session 的方法就是将session记录在客户端,每次请求服务器的时候将session放在请求中发送给服务器,服务器处理过请求后再将修改过的session返回给客户端。网站虽然没有客户端,但是可以利用浏览器支持的cookie记录session。
利用cookie记录session是存在很多缺点:比如cookie的大小存在限制能记录的信息不能超过限制;比如每次请求都要传输cookie影响性能;比如cookie可被修改或者存在破解的可能,导致cookie不能存重要信息,安全系数不够。但是由于cookie简单易用,支持服务器的线性伸缩,而且大部分的session信息相对较小,所以其实很多网站或多或少的都会使用cookie来记录部分不重要的session信息。
4.session服务器(集群)
目前最理想的服务器集群的session管理应该是session服务器,集成了高可用、伸缩性好、对保存信息大小没有限制、性能也相对很好。这种统一管理session的方式将应用服务器分离,分为无状态的应用服务器和有状态的session服务器。如下图所示:

二、SpringBoot+Redis+Nginx实现分布式Session
1、环境准备
(1)SpringBoot 2.1.8.RELEASE (2)Redis 5 (3)Nginx 1.17.8 (4)Tomcat 9
2、实现
基本原理
使用redis实现session共享是基于session集中存储的实现方案,即把session放在一个公共的redis服务器里,所有Web服务器节点都连接着这个公用redis服务器,从而在请求时从公用的redis里查询存放的session值。这就是实现了session共享。
思路
在用户登录成功时,把用户的信息设置到redis服务器里,然后每次请求时都在过滤器(或拦截器)里获取该值,若有值继续操作,没值跳转到登录页面重新登录。
关于Redis的配置这里就不赘述了,不清楚的小伙伴自行百度或Google~~
这里重点说一下登录逻辑中如何处理Session问题以及如果在之后如何验证用户身份
第一步、写一个登录拦截器(过滤器),检查用户是否登录,如果没有登录就重定向用户到登录也爱你登录去,如果登录了返回true,放行
@Slf4j
@Configuration
public class LoginInterceptor extends HandlerInterceptorAdapter {
/**
* 用户登录后在Redis保存session信息的key
*/
private static final String USER_SESSION_REDIS_KEY="USER_SESSION_REDIS_KEY";
//选择Redis数据库
private static final String REDIS_DB=0;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Request from:{}", request.getRequestURL());
//从Cookie中拿出用户登录标志的key去Redis中检查,如果有相关信息,那就说明用户登录过了,否者没有登录过,不允许访问后条管理界面,重定向到登录页面
String sessionRedisKey = CookieUtils.getCookieValue(request, USER_SESSION_REDIS_KEY);
Boolean exists = RedisUtils.getRedisUtils().exists(sessionRedisKey, REDIS_DB);
//当用于浏览需要登录的业务的时候,必须处于登录状态,即exists返回true
String url = request.getRequestURL().toString();
if (url.contains("/admin") && !exists) {
response.sendRedirect(request.getContextPath() + "/user/login.html");
return false;
}
//如果仅仅是浏览一下不需要登录的业务的话都一律放行
return true;
}
}
拦截器编写完成后注意在WebConfiguration的addInterceptor中注册一下:
@EnableWebMvc
@Configuration
public class WebMVConfigurerImpl implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器
InterceptorRegistration registration =registry.addInterceptor(new LoginInterceptor());
//拦截所有请求
registration.addPathPatterns("/**");
/**
* 排除拦截classpath:/static下的所有静态资源
*/
registration.excludePathPatterns("/static/**");
registration.excludePathPatterns("/error");
registration.excludePathPatterns("/**.*.html");
registration.excludePathPatterns("/");
/**
* 排除拦截器对登录、注销、去登录页
*/
registration.excludePathPatterns("/login");
registration.excludePathPatterns("/logout");
}
}
第二步、实现正常登录逻辑,主要就是检查用户密码是否匹配,如果检查没问题的话,就继续下一步
public class LoginController{
/**
* 用户登录后在Redis保存token信息的key
*/
private static final String USER_SESSION_REDIS_KEY="USER_SESSION_REDIS_KEY";
@RequestMapping(vlaue="/login")
public String login(@RequestParam(value = "username", defaultValue = "") String username,
@RequestParam(value = "password", defaultValue = "") String password,
HttpServletRequest request, HttpServletResponse response){
//到数据库去检查用户名和密码
User user = userService.checkUser(username, password);
if(user!=null){
//用户名密码匹配,继续登录后续流程...
//token采用用户ID+UUID并作MD5运算,token一定要保证它的随机性和唯一性
String userLoginToken = MD5Utils.md5(String.format("%d%s", user.getUserId(), UUID.randomUUID().toString().replaceAll("-", "")));
//将用户登录的token信息存放到Redis中
//会话信息,如果没有主动退出60天有效
Boolean res=redisUtil.set(userLoginToken, JSONObject.toJSONString(user), MAX_USER_LOGIN_STATUS_KEEP_TIME, REDIS_DB);
if(res!=null&&res)
//会话的token信息在redis中存放成功之后,在本地存放reids的会话信息的key
setRedisKeyCookie(request,response,USER_SESSION_REDIS_KEY,userLoginToken,MAX_USER_LOGIN_STATUS_KEEP_TIME);
//登录成功返回首页
return "index";
}
//用户名密码不匹配,返回登录页面
return LOGIN_PAGE;
}
}
第三步、用户名密码检查无误之后,设置cookie值,把作为保存Session信息在redis中的key值存入cookie,刷新浏览器的时候,过滤器可以从cookie中取到key值,进而去redis取对应的value值,即Session
public static void setRedisKeyCookie(HttpServletRequest request,
HttpServletRsponse response,
String key, //cookie的key
String value
long keepAilve){ //cookie的value
String domain = request.getServerName();
Cookie cookie = new Cookie(key, value);
if (domain.startsWith("uas.")) {
cookie.setDomain(domain.substring(4,domain.length()));
}else {
cookie.setDomain(domain);
}
cookie.setMaxAge(keepAilve);
cookie.setPath("/");
response.addCookie(uasLoginer);
}
完成这一操作,用户的session信息已经存入到redis中,可在redis中查看是否存入