# 一口气说出 4 种分布式一致性 Session 实现方式，面试杠杠的\~

<https://mp.weixin.qq.com/s/eNU0WPWRHHOx5bmnZDjEhg>

### **前言**

阿粉公司有一个 Web 管理系统，使用 Tomcat 进行部署。由于是后台管理系统，所有的网页都需要登录授权之后才能进行相应的操作。

起初这个系统的用的人也不多，为了节省资源，这个系统仅仅只是单机部署。后来随着用的人越来越多，单机已经有点扛不住了，于是阿粉决定再部署了一台机器。

这时后端系统有两台服务，于是我们使用 Nginx 作为反向代理，整体架构图如下：

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729103914664-103914.jpg)

这个架构图想必大家应该比较熟悉，现在主流的 Web 系统应该都是这么部署。

经过一些调试之后，在一个夜深人静的晚上，阿粉将这套系统部署到了生产。本以为没有什么事的，很稳的交给测试小姐姐开始测试。

这一测，出了大问题！测试小姐姐反馈，登录过后，没过一会又需要登录，操作好几次都是这样。

阿粉检查了一下，系统应用，配置什么也没问题，那到底哪里出了问题？

这个时候组长刚准备下班，看到我们这里有问题，于是过来了看了一下。简单了解的一下基本情况，很快就找到了问题的原因，然后在 Nginx 端修改了下配置，重启解决了问题。

###

### **分布式一致性 Session**

解决完问题，组长坐下给阿粉解释了问题原因：**分布式一致性 Session**。

原先我们登录之后将会把用户登录信息放在 **Session** 中，用户每次操作首先先校验 **Session** 是否存在用户信息，如果不存在将会强制让用户先去登录。

原先架构的中我们只有一台应用系统，所有操作都在一台 Tomcat 上，这当然没有什么问题。

但是现在我们部署了两台系统，由于 Nginx 使用默认负载均衡策略（**轮询**），请求将会按照时间顺序逐一分发到后端应用上。

也就是说刚开始我们在 Tomcat1 登录之后，用户信息放在 Tomcat1 的 **Session** 里。过了一会，请求又被 Nginx 分发到了 Tomcat2 上，这时 Tomcat2 上 **Session** 里还没有用户信息，于是又要登录。

另外由于我们系统采用单点登录的方式，Tomcat2 登录之后会将 Tomcat1 登录信息失效，于是乎等到 Nginx 再把流量分发到 Tomcat1 时，Session 中用户登录信息已经失效，又要重新登录。

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpeg/2020/07/29/640-20200729103914808-103914.jpeg)

知道了问题，阿粉当然想知道解决办法了，于是组长教了阿粉分布式一致性 Session 四种解决办法，阿粉给大家整理了一下：

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729103915041-103915.jpg)

下面阿粉将会以阿粉跟组长对话的形式，讲解分布式一致性 **Session** 解决办法。

###

### **Session 复制**

**组长：**

如果此时 Tomcat1 **Session** 存在用户信息，而 Tomcat2 上没有存在。

这时如果我们将 Tomcat1 的 **Session** 复制到 Tomcat2 上，后面 Nginx 将请求转发到 Tomcat2 上，由于 Tomcat2 存在 **Session** ，这时就不需要再重新登录了。

架构图如下：

![img](https://firebasestorage.googleapis.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M5LMBM-KNwLIye8nLEI%2Fuploads%2FfFPu9zSZQqT5O6JVUgzB%2Ffile.gif?alt=media)一致性 Session-Session 复制

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729110540128-110540.jpg)

> Tomcat 的 **Session** 复制的配置，网上有比较多的例子，这里阿粉就不再贴了，感兴趣的同学可以自行搜索一下。

**阿粉：**

对的，这种方式挺好啊。Tomcat 就支持这种方式，我们只需要修改 Tomcat 配置就好，我们应用代码都不用修改了。

**组长：**

说的对，但是这种方式还是有很多缺点。

第一，Session 复制传输需要占用内网带宽。

第二，我们的例子就只有两台机器，这个复制性能还可以。但是假设我们有 **N** 台机器，那么每次复制都要复制给 **N-1** 台机器，如果机器很多，可能会形成网络风暴，复制性能也会呈指数级下降。

第三， Tomcat 需要保存所有的 **Session** 数据，这个方案的 **Session** 存储在内存中，容易受到机器的总内存的限制。我们没办法通过加机器的方式**水平扩展**，我们能做的方式就是加大机器内存。但是机器内存越大，价格真的很贵！！！

**所以不推荐使用这种方案。**

###

### **Session 前端存储**

**阿粉：**

恩，这个方案确实有点不靠谱\~

哎，有了！我们的 Session 里面其实就是存了用户的信息，那我现在不存 Tomcat **Session** 里，我把信息拿出来，存到浏览器的 **Cookie** 中。

这样，每个用户浏览器存储自己的 **Cookie** 信息，服务端就不需要存储，这就解决了 Session 复制方案的缺陷了。

接下来用户每次请求都把这个 **Cookie** 给我发过来，我判断 **Cookie** 里面用户信息不就好了。

架构图如下：

一致性 Session-Session 前端存储

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729103915277-103915.jpg)

**组长，欣赏看了一下我：**

对，你这个方案确实可行。

不过么，如果用这种方案，首先你要想好加密方案。

用户信息可是我们的敏感数据，不能让别人轻易的窃取或者篡改数据了。

除了这个，这个方案每次请求都要携带 **Cookie** 传输，这会占用外网的带宽，如果 **Cookie** 过大，会增大网络的开销。

另外，我们存储的数据大小，容易受到 **Cookie** 限制。

所以这种还是不怎么常用，不过也是一种思路。

我比较推荐下面两种方案。

###

### **Session  粘滞（Sticky Sessions）**

**组长：**

刚才应该看到了，我只是对 Nginx 的配置做了一些修改，然后这个问题就解决了吧。

其实这是因为我修改 Nginx 默认的负载均衡策略，使用 IP Hash 的方式。

Nginx 会使用请求者的 IP 来做 Hash，然后分发到一台机器上，这样可以保证同一 IP 的请求都落在同一台 Tomcat 上。

架构图如下：

Session 粘滞-IP Hash

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729103915376-103915.jpg)

上面这种方式我们使用 Nginx 四层负载均衡方式，其实 Nginx 还可以做到七层负载均衡方式，也就是使用 Http 协议中的一些**业务属性**来做 Hash，常见的有 *userId*,*loginId*等等。

架构图如下：

一致性 Session-Session 粘滞-七层

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729103915475-103915.jpg)

**阿粉：**

这种方案看起来挺简单的，我们只需要修改 Nginx 配置就好了，应用端配置无需改动。

只要请求来源 IP **足够的随机**，那么 IP HASH 之后两台应用上的流量将会足够随机。

另外后面如果两台机器扛不住，我们还可以水平扩展，再加机器，只要修改 Nginx 配置即可。

**组长：**

你说的这几点都很正确！

不过你有没有想过，像我们公司这种情况，所有人的出口的 IP 都是一个。那么我们公司的所有请求只会到一台机器上，那我们这种情况等于又变成单点了。

另外如果 Tomcat 重启，**Session** 由于是放置在内存内存中，这一部分的 **Session** 将会丢失，这就导致这部分用户将会重新登录。

最后，如果我们临时再加机器，修改完 Nginx 配置，重新启动之后，Nginx 将会重新计算 Hash 分发请求。

这种情况就会导致有一部分用户重新路由到一台新机器上，由于没有 **Session**，又需要重新登录了。

不过么，Tomcat 重启或者新加机器次数不会很多，所以这个问题也不大，用户体验稍差点。

今天的我们这个问题解决方案就先使用这个。

不过后面我们还是改成下面这种方式。

###

### **后端集中存储**

**组长：**

上面几种的方式我们都是把 **Session** 存储在应用内存上，应用机器只要重启，**Session** 就会丢失。

为了这个解决这个问题，我们将 **Session** 单独存起来，保存到 Redis 或者 MySQL 中。

不过由于 **Session** 需要过期失效的特性，不需要持久化保存，所以这里我建议使用 Redis 来保存。

这样架构就变成下方这样的：

一致性 Session-Session 后端存储

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/jpg/2020/07/29/640-20200729103915620-103915.jpg)

我们使用这种方案，上没有 **Session** 丢失的风险，当然前提是 Redis 不能宕机。

另外后期如果应用可以直接水平扩展。

如果后面应用的请求量很大，一台 Redis 扛不住了，那我们可以其实可以做集群扩展，根据缓存 Key 做路由。

**阿粉：**

对对，这种方式好\~

**组长：**

你不要高兴的太早，我们使用这个方案需要付出一定的代价的。

首先我们每次请求都需要调用一次 Redis ，这就增加一次网络的开销。

另外，引入 Redis，我们需要对相应的代码做出修改，这样复杂度就变高。

所以说，这个方案有利也有弊，当然对于我们的场景来说，利大于弊。

**阿粉：**

恩，好像是这样的。

**组长:**

好了，这么晚了，问题解决了，我们去撸个串，我请客！

**阿粉：**

老大，🐂！

**组长拍了拍阿粉的脑袋：**

我这一顿不是白吃哦，下个星期你把现在方式修改一下，修改成 Session 集中存储的方式。

![img](https://gitee.com/baicaihenxiao/imageDB/raw/master/uPic/gif/2020/07/29/640-103915.gif)

给你一个小提示，可以使用 **spring-session**。

**阿粉：**

好吧，吃人嘴短，下周我研究下。

###

### **总结**

最后阿粉总结一下，当我们后端 Web 应用扩展到多台后，我们就会碰到分布式一致性 Session 的问题，主流解决方案有四种：

* Session 复制：利用 Tomcat 等 Web 容器同步复制
* Session 前端存储：利用用户浏览器中 Cookie 保存 Session 信息
* Session  粘滞方案：利用 Nginx 可以做四层 Hash 或七层 Hash 的特性，保证用户的请求都落在同一台机器上
* Session 后端集中存储方案：利用 Redis 集中存储 Session，Web 应用重启或扩容，Session 也无需丢失。

上面四种方案，优先推荐第四种。

当然第四种方案需要一定的开发工作量，前期还没改造的过程可以选择 第三种方案中间过渡。

好了，后面阿粉就要使用 Session 后端存储方案改造这个工程了，后面阿粉再跟大家分享一下 **spring-session**。
