拉勾教育学习-笔记分享の"玩转"分布式集群

555 阅读18分钟

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
将分布式架构深入剖析


百尺大厦平地起,空中楼阁刹那倾

一、 一致性Hash算法

前言

分布式⼀定是集群 —— 多个实例一起工作的集合叫做集群,而分布式就是将整个系统拆分成了不同业务功能的实例;

但集群不⼀定是分布式 —— 比如复制型的集群只是单纯复制,而非拆分功能,所以不属于分布式;

Part 1 - Hash算法应用场景

A - 应用点

  • 安全加密领域——MD5\SHA...
  • 数据存储和查询方面——Hash表...

其实Hash算法更多的用在数据和查找领域,如Hash表中的Hash算法。只要设计合理,查询时间复杂度近似为O(1)

B - 从Hash算法入手

有以下需求:

提供⼀组数据 1,5,7,6,3,4,8,对这组数据进⾏存储,然后随便给定⼀个数n,请你判断n是否存在于刚才的数据集中

1) 顺序查找法
list = List[1,5,7,6,3,4,8];
for(int every : list){
	if(every == n){
    	// TODO
    }
}

通过循环来完成,比较原始,效率也不⾼

2) 二分查找(排序后折半查找)
list = Array.sort(List[1,5,7,6,3,4,8]);

// 折半查找

效率虽提升了,但避免不了循环和多次二分

3) 直接寻址法

这个时候,我想看下5存在与否,只需要判断list.get(5) 或 array[5] 是否为空
如果为空 ---> 代表5不存在于数据集
如果不为空 ---> 代表5在数据集当中

通过⼀次查找就达到了目的,时间复杂度为O(1)

缺点: 浪费空间(如 [1,2,3,1,1,1,1,99] 中如果都开辟就浪费了将近96个空间)

4) 除留余数法(简单的Hash算法)

===》Hash表的查询效率高不高取决于Hash算法,hash算法能够让数据平均分布,既能够节省空间⼜能提⾼查询效率

hashcode其实也是通过⼀个Hash算法得来的

C - Hash算法应用场景

Hash算法在很多分布式集群产品中都有应⽤,⽐如

  • 分布式集群架构Redis
  • Hadoop
  • ElasticSearch
  • Mysql分库分表
  • Nginx负载均衡...

应用场景归纳:

  1. 请求的负载均衡(Nginx的 ip_hash策略)
    Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个目标服务器上 --> 实现会话粘滞,避免处理session共享问题
    没有ip_hash策略时,可以自定义映射关系维护ip 《====》 sessionId 的映射关系
    但缺点很明显:
    1-客户端多时,映射表数据多,浪费内存
    2-客户端上下线,目标服务器上下线,都会导致重新维护映射表,映射表维护成本很大

  1. 分布式存储
    以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器
    在进行数据存储时,<key1,value1>数据存储到哪个服务器当中呢?
    针对key进⾏hash处理hash(key1)%3=index, 使用余数index锁定存储的具体服务器节点

D - Nginx的ip_hash的C语言源码分析

(待跟进)

Part 2 - 普通Hash算法存在的问题

普通Hash算法存在⼀个问题,以ip_hash为例
假定下载用户ip固定没有发生改变,现在tomcat3出现了问题,宕机了
服务器数量由3个变为了2个,之前所有的求模都需要重新计算。

果在真实生产情况下,后台服务器很多台,客户端也有很多,那么影响是很⼤的,缩容和扩容都会存在这样的问题
大量用户的请求会被路由到其他的目标服务器处理,用户在原来服务器中的会话都会丢失。

Part 3 - 一致性Hash算法

「简介」

「问题所在」

如前所述,每⼀台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的⼀小部分数据,具有较好的容错性和可扩展性
但是......
⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。
例如系统中只有两台服务器,其环分布如下,节点2只能负责非常小的⼀段,大量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题

「解决方式」

为了解决这种数据倾斜问题,⼀致性哈希算法引入了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点
例如可以: 在服务器ip或主机名的后面增加编号来实现

为每台服务器计算三个虚拟节点,于是可以分别计算
“节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值
于是形成六个虚拟节点
当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点

Part 4 - 手写实现一致性Hash算法

(待跟进)

Part 5 - Nginx 配置一致性Hash负载均衡策略

「简介」

ngx_http_upstream_consistent_hash 模块是⼀个负载均衡器,使用⼀个内部⼀致性hash算法来选择合适的后端节点。
该模块可以根据配置参数采取不同的⽅式将请求均匀映射到后端机器,

  • consistent_hash $remote_addr:可以根据客户端ip映射
  • consistent_hash $request_uri:根据客户端请求的uri映射
  • consistent_hash $args:根据客户端携带的参数进行映

ngx_http_upstream_consistent_hash 模块是⼀个第三方模块,需要我们下载安装后使用

「下载consistent模块」

下载地址

「解压到Nginx服务器中」

yum install -y unzip

#unzip file.zip -d /root -d指解压路径 ,不写的话默认当前目录

「刷新配置并(重新)编译」

./configure --add-module=../ngx_http_consistent_hash-master

make

make install

「使用」

修改nginx配置并reload

「具体使用」

(待跟进)

二、 集群时钟同步

Part 1 - 时钟不同步导致的问题

时钟此处指服务器时间,如果集群中各个服务器时钟不⼀致势必导致⼀系列问题
举⼀个例子,电商网站业务中,新增⼀条订单,那么势必会在订单表中增加了⼀条记录,该条记录中应该会有下单时间这样的字段
往往我们会在程序中获取当前系统时间插入到数据库或者直接从数据库服务器获取时间。

如果我们的订单⼦系统是集群化部署,或者我们的数据库也是分库分表的集群化部署,然而他们的系统时钟缺不⼀致,比如有⼀台服务器的时间是昨天,那么这个时候下单时间就成了昨天,那我们的数据将会混乱!如下

Part 2 - 集群时钟同步配置

(待跟进)

三、 分布式ID解决方案

核心问题: 对于一个集群,分表之后ID不能重复,因此单纯的 主键自增 无用

Part 1 - UUID (可以用)

「定义」

UUID 是指Universally Unique Identifier,翻译为中文是通用唯⼀识别码
产⽣重复 UUID 并造成错误的情况非常低,是故大可不必考虑此问题。

「使用」

Java中得到⼀个 UUID,可以使用ava.util包提供的方法

public class MyTest {
 public static void main(String[] args) {
 System.out.println(java.util.UUID.randomUUID().toString());
 }
}

Part 2 - 独立数据库的自增ID (不推荐)

「定义」

比如A表分表为A1表A2表,那么肯定不能让A1表A2表的ID⾃增,那么ID怎么获取呢?
我们可以单独的创建⼀个Mysql数据库,在这个数据库中创建⼀张表,这张表的ID设置为自增,其他地方需要全局唯⼀ID的时候,就模拟向这个Mysql数据库的这张表中模拟插入⼀条记录,此时ID会自增
然后我们可以通过Mysql的select last_insert_id() 获取到刚刚这张表中自增生成的ID.

「使用」

例如某实例中存在下表

DROP TABLE IF EXISTS `DISTRIBUTE_ID`;
CREATE TABLE `DISTRIBUTE_ID` (
 `id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `createtime` datetime DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当分布式集群环境中哪个应用需要获取⼀个全局唯⼀的分布式ID的时候,就可以使用代码连接这个数据库实例
执行如下sql语句即可:

insert into DISTRIBUTE_ID(createtime) values(NOW()); select LAST_INSERT_ID();

1 这⾥的createtime字段⽆实际意义,是为了随便插⼊⼀条数据以⾄于能够⾃增id
2 使⽤独⽴的Mysql实例⽣成分布式id,虽然可⾏,但是性能和可靠性都不够好, 因为你需要代码连接到数据库才能获取到id,性能⽆法保障,另外mysql数据库实例挂掉了,那么就⽆法获取分布式id了
3 有⼀些开发者⼜针对上述的情况将⽤于⽣成分布式id的mysql数据库设计成了⼀个集群架构,那么其实这种⽅式现在基本不⽤,因为过于麻烦了。

Part 3 - SnowFlake 雪花算法 (推荐使用)

「定义」

Twitter推出的⼀个用于生成分布式ID的策略

雪花算法是⼀个算法,基于这个算法可以生成ID,⽣成的ID是⼀个long型,那么在Java中⼀个long型是8个字节,算下来是64bit,如下是使用雪花算法生成的⼀个ID的⼆进制形式示意:

P.S. ⼀些互联网公司也基于上述的方案封装了⼀些分布式ID生成器:
--- 滴滴的tinyid(基于数据库实现)
--- 百度的uidgenerator(基于SnowFlake)
--- 美团的leaf(基于数据库和SnowFlake)

Part 4 - 借助Redis的Incr命令获取全局唯⼀ID(推荐使用)

「定义」

Redis Incr 命令将 key 中储存的数字值增⼀。
如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行 INCR 操作。

「使用」

  1. Redis安装(略)
    注意:redis.cof 中需要: protect-mode = no
  2. Java中引入jar包
<dependency>
 <groupId>redis.clients</groupId>
 <artifactId>jedis</artifactId>
 <version>2.9.0</version>
</dependency>
  1. Java代码
Jedis jedis = new Jedis("127.0.0.1",6379);
try {
 long id = jedis.incr("id");
 System.out.println("从redis中获取的分布式id为:" + id);
} finally {
 if (null != jedis) {
 jedis.close();
 } }

四、 分布式调度问题

Part 1 - 节点任务的场景

定时任务形式:每隔⼀定时间/特定某⼀时刻执行

e.g.

订单审核、出库
订单超时⾃动取消、⽀付退款
礼券同步、⽣成、发放作业
物流信息推送、抓取作业、退换货处理作业
数据积压监控、⽇志监控、服务可⽤性探测作业
定时备份数据
⾦融系统每天的定时结算
数据归档、清理作业
报表、离线数据分析作业

Part 2 - 什么是分布式调度

1. 运行在分布式集群环境下的调度任务(同⼀个定时任务程序部署多份,只应该有⼀个定时任务在执行)
2. 分布式调度—>定时任务的分布式—>定时任务的拆分(即为把⼀个⼤的作业任务拆分为多个小的作业任务,同时执行)

Part 3 - 定时任务与消息队列的区别

「共同点」

  • 异步处理
  • 应用解耦
  • 流量削峰

「不同点」

定时任务作业是时间驱动,而MQ是事件驱动;
时间驱动是不可代替的,比如金融系统每日的利息结算,不是说利息来⼀条(利息到来事件)就算⼀下,而往往是通过定时任务批量计算;
所以————> 定时任务作业更倾向于批处理MQ 倾向于逐条处理

Part 4 - 定时任务的实现方法

定时任务的实现方式有多种。
早期没有定时任务框架的时候,我们会使用JDK中的Timer机制多线程机制(Runnable+线程休眠)来实现定时或者间隔⼀段时间执行某⼀段程序;

后来有了定时任务框架,比如大名鼎鼎的Quartz任务调度框架
使用时间表达式(包括:秒、分、时、日、周、年)配置某⼀个任务什么时间去执行

「Quartz大致回顾」

1) 引入jar
<!--任务调度框架quartz-->
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
 <groupId>org.quartz-scheduler</groupId>
 <artifactId>quartz</artifactId>
 <version>2.3.2</version>
</dependency>
2) 定时任务作业主调度程序
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzMain {

   // 创建作业任务调度器(类似于公交调度站)
   public static Scheduler createScheduler() throwsSchedulerException {
     SchedulerFactory schedulerFactory = new StdSchedulerFactory();
     Scheduler scheduler = schedulerFactory.getScheduler();
     return scheduler;
   }

   // 创建⼀个作业任务(类似于⼀辆公交⻋)
   public static JobDetail createJob() {
     JobBuilder jobBuilder = JobBuilder.newJob(DemoJob.class);
     jobBuilder.withIdentity("jobName","myJob");
     JobDetail jobDetail = jobBuilder.build();
     return jobDetail;
   }

  /**
   * 创建作业任务时间触发器(类似于公交⻋出⻋时间表)
   * cron表达式由七个位置组成,空格分隔
   * 1、Seconds(秒) 0~59
   * 2、Minutes(分) 0~59
   * 3、Hours(⼩时) 0~23
   * 4、Day of Month(天)1~31,注意有的⽉份不⾜31天
   * 5、Month(⽉) 0~11,或者 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
   * 6、Day of Week(周) 1~7,1=SUN或者 SUN,MON,TUE,WEB,THU,FRI,SAT
   * 7、Year(年)1970~2099 可选项
   *示例:
   * 0 0 11 * * ? 每天的11点触发执⾏⼀次
   * 0 30 10 1 * ? 每⽉1号上午10点半触发执⾏⼀次
   */	
   public static Trigger createTrigger() {
     // 创建时间触发器,按⽇历调度
     CronTrigger trigger = TriggerBuilder.newTrigger()
     .withIdentity("triggerName","myTrigger")
     .startNow()
     .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
     .build();
     
     // 创建触发器,按简单间隔调度
     /*SimpleTrigger trigger1 = TriggerBuilder.newTrigger()
     .withIdentity("triggerName","myTrigger")
     .startNow()
     .withSchedule(SimpleScheduleBuilder
     .simpleSchedule()
     .withIntervalInSeconds(3)
     .repeatForever())
     .build();*/
     
     return trigger;
 }
 
   // 定时任务作业主调度程序
   public static void main(String[] args) throws SchedulerException {
   // 创建⼀个作业任务调度器(类似于公交调度站)
   Scheduler scheduler = QuartzMain.createScheduler();
   // 创建⼀个作业任务(类似于⼀辆公交⻋)
   JobDetail job = QuartzMain.createJob();
   // 创建⼀个作业任务时间触发器(类似于公交⻋出⻋时间表)
   Trigger trigger = QuartzMain.createTrigger();
   // 使⽤调度器按照时间触发器执⾏这个作业任务
   scheduler.scheduleJob(job,trigger);
   scheduler.start();
   }

}
3) 定义⼀个job,需实现Job接口
  import org.quartz.Job;
  import org.quartz.JobExecutionContext;
  import org.quartz.JobExecutionException;

  public class DemoJob implements Job {
      public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
          System.out.println("我是⼀个定时任务逻辑");
      }
  }

在分布式架构环境中使用Quartz已经不能更好的满足我们需求,我们可以使用专业的分布式调度框架,这里我们推荐使用Elastic-job

Part 5 - 分布式调度框架Elastic-Job

「简介」

Elastic-Job是当当网开源的⼀个分布式调度解决方案,基于Quartz⼆次开发的
由两个相互独立的子项目 Elastic-Job-LiteElastic-Job-Cloud 组成。

我们要学习的是 Elastic-Job-Lite

-----------------------
它定位为轻量级无中心化解决方案,使用Jar包的形式提供分布式任务的协调服务
而Elastic-Job-Cloud⼦项目需要结合Mesos以及Docker在云环境下使用
-----------------------

Elastic-Job的github地址

「主要功能」

  • 分布式调度协调
    在分布式环境中, 任务能够按指定的调度策略执行,并且能够避免同⼀任务多实例重复执行
  • 丰富的调度策略
    基于成熟的定时任务作业框架Quartz cron表达式执行定时任务
  • 弹性扩容缩容
    当集群中增加某⼀个实例,它应当也能够被选举并执行任务;当集群减少⼀个实例时,它所执行的任务能被转移到别的实例来执行。
  • 失效转移
    某实例在任务执行失败后,会被转移到其他实例执行
  • 错过执行作业重触发
    若因某种原因导致作业错过执行,自动记录错过执行的作业,并在上次作业完成后自动触发。
  • 支持并行调度 + 支持任务分片
    任务分片是指将⼀个任务分为多个行任务项在多个实例同时执行。
  • 作业分片⼀致性
    当任务被分片后,保证同⼀分片在分布式环境中仅⼀个执行实例。

「Elastic-Job-Lite应⽤」

Elastic-Job依赖于Zookeeper进行分布式协调,所以需要安装Zookeeper软件(3.4.6版本以上)

1) 安装 Zookeeper (当前为单例配置)
1. 在linux平台解压下载的zookeeper-3.4.10.tar.gz
2. 进入conf目录,cp zoo_sample.cfg zoo.cfg
3. 进入bin目录,启动zk服务

启动 --> ./zkServer.sh start
停止 --> ./zkServer.sh stop
查看状态 --> ./zkServer.sh status

4. 引⼊Jar包
<!-- https://mvnrepository.com/artifact/com.dangdang/elastic-job-lite-core-->
<dependency>
 <groupId>com.dangdang</groupId>
 <artifactId>elastic-job-lite-core</artifactId>
 <version>2.1.5</version>
</dependency>
5. 定时任务实例

需求
每隔两秒钟执⾏⼀次定时任务(resume表中未归档的数据归档到resume_bak表中, 每次归档1条记录)

resume_bak和resume表结构完全⼀样 resume表中数据归档之后不删除,只将state置为"已归档"

数据表结构:

-- ----------------------------
-- Table structure for resume
-- ----------------------------
DROP TABLE IF EXISTS `resume`;
CREATE TABLE `resume` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) DEFAULT NULL,
 `sex` varchar(255) DEFAULT NULL,
 `phone` varchar(255) DEFAULT NULL,
 `address` varchar(255) DEFAULT NULL,
 `education` varchar(255) DEFAULT NULL,
 `state` varchar(255) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;

程序开发

定时任务类

import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import util.JdbcUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.Map;
public class BackupJob implements SimpleJob {
   // 定时任务每执⾏⼀次都会执⾏如下的逻辑
   @Override
   public void execute(ShardingContext shardingContext) {
       /*
        从resume数据表查找1条未归档的数据,将其归档到resume_bak表,并更新状态为已归档(不删除原数据)
       */
       // 查询出⼀条数据
       String selectSql = "select * from resume where state='未归档' limit 1";
       List<Map<String, Object>> list = JdbcUtil.executeQuery(selectSql);
       if(list == null || list.size() == 0) {
          return;
       }
       Map<String, Object> stringObjectMap = list.get(0);
       long id = (long) stringObjectMap.get("id");
       String name = (String) stringObjectMap.get("name");
       String education = (String) stringObjectMap.get("education");
       // 打印出这条记录
       System.out.println("======>>>id:" + id + " name:" + name + " education:" + education);
       // 更改状态
       String updateSql = "update resume set state='已归档' where id=?";
       JdbcUtil.executeUpdate(updateSql,id);
       // 归档这条记录
       String insertSql = "insert into resume_bak select * from resume where id=?";
       JdbcUtil.executeUpdate(insertSql,id);
    }
}

主类

import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.api.JobScheduler;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;

public class ElasticJobMain {
   public static void main(String[] args) {
     // 配置注册中⼼zookeeper,zookeeper协调调度,不能让任务重复执⾏,通过命名空间分类管理任务,对应到zookeeper的⽬录
     ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181","data-archive-job");
     CoordinatorRegistryCenter coordinatorRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
     coordinatorRegistryCenter.init();
     // 配置任务
     JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("archive-job","*/2 * * * * ?",1).build();
     SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration,BackupJob.class.getName());
     // 启动任务
     new JobScheduler(coordinatorRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).build()).init();
   }
}

JdbcUtil⼯具类

package util;

import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JdbcUtil {
    //url
    private static String url = "jdbc:mysql://localhost:3306/job?characterEncoding=utf8&useSSL=false";
    //user
    private static String user = "root";
    //password
    private static String password = "123456";
    //驱动程序类
    private static String driver = "com.mysql.jdbc.Driver";
    
    static {
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    public static Connection getConnection() {
        try {
            return DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
    
    public static void close(ResultSet rs, PreparedStatement ps, Connection con) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                if (ps != null) {
                    try {
                        ps.close();
                    } catch (SQLException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } finally {
                        if (con != null) {
                            try {
                                con.close();
                            } catch (SQLException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    
    public static void executeUpdate(String sql, Object... obj) {
        Connection con = getConnection();
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(sql);
            for (int i = 0; i < obj.length; i++) {
                ps.setObject(i + 1, obj[i]);
            }
            ps.executeUpdate();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            close(null, ps, con);
        }
    }
    
    public static List<Map<String, Object>> executeQuery(String sql, Object... obj) {
        Connection con = getConnection();
        ResultSet rs = null;
        PreparedStatement ps = null;
        try {
            ps = con.prepareStatement(sql);
            for (int i = 0; i < obj.length; i++) {
                ps.setObject(i + 1, obj[i]);
            }
            rs = ps.executeQuery();
            
            List<Map<String, Object>> list = new ArrayList<>();
            
            int count = rs.getMetaData().getColumnCount();
            
            while (rs.next()) {
                
                Map<String, Object> map = new HashMap<String,
                                        Object>();
                for (int i = 0; i < count; i++) {
                    Object ob = rs.getObject(i + 1);
                    String key = rs.getMetaData().getColumnName(i + 1);
                    map.put(key, ob);
                }
                list.add(map);
            }
            return list;
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            close(rs, ps, con);
        }
        return null;
    }
}

运行测试

(待跟进)

五、 Session共享问题

「简介」

Session共享Session保持或者叫做Session⼀致性

Part 1 - Session问题原因分析

出现这个问题的原因,从根本上来说是因为Http协议是无状态的协议
客户端和服务端在某次会话中产生的数据不会被保留下来,所以第⼆次请求服务端无法认识到你曾经来过

Http为什么要设计为无状态协议?
早期都是静态页面无所谓有无状态,后来有动态的内容更丰富,就需要有状态,出现了两种用于保持Http状态的技术,那就是CookieSession。从而出现上述不断让用户重新登录的问题,分析如下图:

Part 2 - 解决Session一致性的方案

A - Nginx 的 ip_hash 策略(可以)

同⼀个客户端IP的请求都会被路由到同⼀个目标服务器,也叫做会话粘滞

优点

  • 配置简单,不入侵应用,不需要额外修改代码

缺点

  • 服务器重启Session丢失
  • 存在单点负载高的风险
  • 单点故障问题

B - Session复制(× 不推荐)

多个tomcat之间通过修改配置文件,达到Session之间的复制

优点

  • 不⼊侵应用
  • 便于服务器水平扩展
  • 能适应各种负载均衡策略
  • 服务器重启或者宕机不会造成Session丢失

缺点

  • 性能低
  • 内存消耗
  • 不能存储太多数据,否则数据越多越影响性能
  • 有延迟性

C - Session共享,Session集中存储(√ 推荐)

Session的本质就是缓存,那Session数据为什么不交给专业的缓存中间件呢?比如Redis

优点

  • 能适应各种负载均衡策略
  • 服务器重启或者宕机不会造成Session丢失
  • 扩展能力强
  • 适合大集群数量使用

缺点

  • 对应用有入侵,引入了和Redis的交互代码

D - SpringSession ☆☆☆

Spring Session 使得基于 Redis 的 Session共享 应用起来非常之简单

1)引入jar
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> <dependency>
 <groupId>org.springframework.session</groupId>
 <artifactId>spring-session-data-redis</artifactId>
</dependency>
2)配置redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
3)添加注解

@EnableRedisHttpSession 该注解可以创建⼀个过滤器使得SpringSession替代HttpSession发挥作用,找到那个过滤器

4)代码分析

(待跟进)

5)具体使用

参考本次大作业编程题一:

需求
1)基于SpringBoot实现一个登陆功能(含有登录拦截验证)
2)使用Spring Session进行Session一致性控制
3)将工程打成war包
4)将war包部署到tomcat集群中,要求1个Nginx节点、2个Tomcat节点请求 —> Nginx(轮询策略) —> Tomcat1 / Tomcat2
5)完成测试

源码地址