文章内容输出来源:拉钩教育Java高薪训练营
分布式集群架构场景化解决方案
-
分布式一定是集群,但集群不一定是分布式
-
分布式:把一个系统拆分为多个子系统,每个子系统负责各自的功能,独立部署,各司其职
-
集群:多个实例共同工作,最简单/常见的集群是把一个应用复制多份部署
-
一致性Hash算法
普通Hash算法
-
安全加密MD5、SHA等加密算法
-
数据存储和查找
- 顺序查找->二分查找->直接寻址->除留余数法->开放寻址->拉链法
- 查询效率高,时间复杂度接近于O(1)
- 查询效率取决于Hash算法,好的算法能够让数据平均分布,既能够节省空间又能提高查询效率
-
在分布式集群架构中的应用
-
请求的负载均衡(比如nginx的ip_hash策略)
-
nginx-1.18.0/src/http/modules/ngx_http_upstream_ip_hash_module.c
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) { hash = (hash * 113 + iphp->addr[i]) % 6271; }
-
-
分布式存储(redis、Hadoop、ElasticSearch、Mysql分库分表等)
- <key1, value1>数据存储到哪个服务器?hash(key1)%3=index
-
-
普通Hash算法存在的问题
- 服务器缩容和扩容将引起巨大影响,大量用户的请求会被路由到其他目标服务器
一致性Hash算法
- 算法思路
- 哈希环(0~2^32-1)
- 服务器的ip或主机名求hash值,对应到哈希环上的某个位置
- 客户端采用相同方式,对应哈希环上的位置
- 按顺时针方向找最近的服务器节点
- 扩容和缩容
- 小部分客户端收到影响,请求的迁移达到了最小
- 虚拟节点
- 当节点数较小时,存在数据(请求)倾斜的问题
- 虚拟节点机制:对每一个服务节点计算多个hash值,每个计算结果位置都放置一个服务节点->虚拟节点
手写实现
-
普通hash算法
int hash = Math.abs(client.hashCode()); int index = hash % serverCount; -
一致性hash算法
// 1. 服务器ip的hash值对应到hash环 String[] servers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"}; SortedMap<Integer, String> serverHashMap = new TreeMap<>(); for (String server : servers) { int serverHash = Math.abs(server.hashCode()); serverHashMap.put(serverHash, server); } // 2. 客户端ip对应 String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"}; for (String client : clients) { int clientHash = Math.abs(client.hashCode()); // 3. 顺时针查找最近的服务器 SortedMap<Integer, String> tailMap = serverHashMap.tailMap(clientHash); if (tailMap.isEmpty()) { Integer firstKey = serverHashMap.firstKey(); System.out.println("=======>>>客户端:" + client + " 被路由到:" + serverHashMap.get(firstKey)); } else { Integer firstKey = tailMap.firstKey(); System.out.println("=======>>>客户端:" + client + " 被路由到:" + serverHashMap.get(firstKey)); } } -
一致性hash算法+虚拟节点
int virtualCount = 3; for (String server : servers) { int serverHash = Math.abs(server.hashCode()); serverHashMap.put(serverHash, server); for (int i = 0; i < virtualCount; i++) { int virtualHash = Math.abs((server + "#" + i).hashCode()); serverHashMap.put(virtualHash, "---虚拟节点" + i + "映射到" + server); } }
Nginx配置一致性Hash负责均衡策略
-
ngx_http_upstream_consistent_hash负载均衡器,内部使用一致性hash算法来选择合适的后端节点-
consistent_hash $remote_addr:根据客户端ip映射
-
consistent_hash $request_uri:根据客户端请求的uri映射
-
consistent_hash $args:根据客户端携带的参数进行映射
nginx.conf
# 负载均衡的配置 upstream_carolServer { consistent_hash $request_uri; server 127.0.0.1:8080; server 127.0.0.1:8082; }
-
-
安装
-
下载zip,上传到nginx服务器并解压
-
yum unzip unzip ngx_http_consistent_hash-master
-
-
进入nginx的源码目录,执行
cd nginx-1.18.0 ./configure --add-module=/root/ngx_http_consistent_hash-master make make install
-
集群时钟同步问题
-
分布式集群中各个服务器节点都可以连接互联网
-
思路:各个节点统一从国家授时中心/时间服务器同步
-
步骤
# 使用前需要检查是否安装 rpm -qa|grep ntpdate # 没有安装则使用yum安装 yum install ntp # 修改时间 date -s '08:00:00' # 查看当前时间 date # 使用 utpdate 网络时间同步命令 ntpdate -u ntp.api.bz # 例如,从一个时间服务器同步时间 -
定时任务:每10分钟/每天等
- windows 计划任务
- linux crond
-
-
分布式集群中某一个/几个服务器节点可以访问互联网
-
思路:可以访问互联网的节点A定期进行同步,其它节点与A同步(即A作为局域网内的时间服务器)
-
步骤
-
设置节点A的时间
-
修改
/etc/ntp.conf-
如果有
restrict default ignore,注释掉 -
添加如下几行内容
# 开放局域网同步功能,172.17.0.0 是局域网网段 restrict 172.17.0.0 mask 255.255.255.0 nomodify notrap server 127.127.1.0 # local clock fudge 127.127.1.0 stratum 10 -
重启生效并配置ntpd服务开机自启动
service ntpd restart chkconfig ntpd on -
其他节点从A服务器同步
ntpdate 172.17.0.17
-
-
-
-
所有节点都不能访问互联网
- 思路:节点A手动设置,其它节点与A同步
- 步骤见上
分布式ID解决方案
-
分布式ID:分布式集群环境下的全局唯一ID
-
解决方案
-
UUID(可以使用)
UUID.randomUUID() // 66d3d1d4-7e33-46cd-9ada-42cf8a9ee904- 方便,可以作为主键(重复概率非常非常低),但是比较长且没有规律,建立索引将影响性能
-
独立数据库的自增ID(不推荐)
-
单独创建一个数据库,创建一张ID设置为自增的表。
-
其它地方需要全局唯一ID的时候,就模拟向这个表中插入一条记录,获取自增生成的ID
insert into DISTRIBUTE_ID(createtime) values (NOW()); select LAST_INSERT_ID(); -
性能和可靠性不高
-
-
SnowFlake雪花算法
-
Twitter公司推出的用于生成分布式ID的策略
-
生成一个long型的id,java中8字节,64bit
-
0:符号位
-
1-41:当前时间戳(毫秒)
-
42-51:机器id
-
52-63:序列号
-
-
互联网公司基于以上方案有一些自己的封装
-
滴滴的tinyid(基于数据库)
-
百度的uidgenerator(基于雪花算法)
-
美团的leaf(基于数据库和雪花算法)
-
-
java 实现
public synchronized long nextId() { long timestamp = System.currentTimeMillis(); if (timestamp < lastTimestamp) { System.out.println("clock is moving backwards. Rejecting requests until %d.", lastTimestamp); throw new RuntimeException("Clock moved backwards."); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; // 超过同一毫秒里的最大值(4095),等到下一毫秒 if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } else { sequence = 0; } lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; } private long tilNextMillis(long lastTimestamp) { long timestamp = System.currentTimeMillis(); while (timestamp <= lastTimestamp) { timestamp = System.currentTimeMillis(); } return timestamp; } }
-
-
Redis的Incr命令(推荐)
-
将key中存储的数字值增一,如果key不存在,那么会初始化为0
-
redis安装
-
官网下载
redis-6.0.9.tar.gz,解压安装tar -zxvf redis-6.0.9.tar.gz cd redis-6.0.9 make cd src/ make install cd .. vim redis.conf -
修改配置文件
# bind 127.0.0.1 protected-mode no -
启动
cd src/ ./redis-server ../redis.conf ps -ef|grep redis
-
-
使用jedis客户端调用redis命令
pom.xml
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>java
Jedis jedis = new Jedis("47.101.165.107", 6379); Long id = jedis.incr("id");
-
-
分布式调度问题
定时任务的场景
- 订单审核、出库
- 订单超时自动取消、支付退款
- 礼券同步、生成、发放作业
- 物流信息推送、抓取作业、退换货处理作业
- 数据积压监控、日志监控、服务可用性探测作业
- 定时备份数据
- 金融系统每天的定时结算
- 数据归档、清理作业
- 报表、离线数据分析作业
什么是分布式调度
- 运行在分布式集群环境下的调度任务
- 定时任务的拆分
定时任务与消息队列的区别
- 相同点
- 异步处理
- 应用解耦
- 两个应用之间的齿轮,中转数据
- 流量削峰
- 不同点
- 定时任务是时间驱动,MQ是事件驱动
- 定时作业更倾向于批处理,MQ倾向于逐条处理
定时任务的实现方式
-
JDK中的Timer机制和多线程机制(早期没有框架时)
-
Quartz任务调度框架
pom.xml
<dependencies> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency> </dependencies>QuartzMain.java
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(); } public static Scheduler createScheduler() throws SchedulerException { SchedulerFactory sf = new StdSchedulerFactory(); return sf.getScheduler(); } public static JobDetail createJob() { JobBuilder jobBuilder = JobBuilder.newJob(DemoJob.class); jobBuilder.withIdentity("jobName", "myJob"); return jobBuilder.build(); } /** * cron表达式由七个位置组成,空格分隔 * 1、 Seconds(秒) 0~59 * 2、 Minutes(分) 0~59 * 3、 Hours(小时) 0~23 * 4、 Day of Month(天) 1~31 * 5、 Month(月) 0~11 或 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC * 6、 Day of Week(周) 1~7 或 SUN,MON,TUE,WED,THU,FRI,SAT * 7、 Year(年) 1970~2099 可选项 * 示例: * 0 0 11 * * ?每天的11点触发一次 * 0 30 10 1 * ? 每月1号上午10点半触发执行一次 */ public static Trigger createTrigger() { return TriggerBuilder.newTrigger() .withIdentity("triggerName", "myTrigger") .startNow() // 每隔2秒 .withSchedule(CronScheduleBuilder.cronSchedule("*/2 * * * * ?")) .build(); }DemoJob.java
public class DemoJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("定时任务执行逻辑"); } }
分布式调度框架Elastic-Job
Elastic-Job介绍
- 当当网的一个分布式调度解决方案,基于Quartz二次开发,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。Lite定位是轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务;Cloud要结合Mesos以及Docker在云环境下使用。
- github地址:github.com/elasticjob
- 主要功能
- 分布式调度协调:在分布式环境中,任务能够按照指定的调度策略执行,并且能够避免同一任务多实例重复执行
- 丰富的调度策略:cron表达式执行定时任务
- 弹性扩容缩容:例如减少实例,所执行的任务能被转移到其他实例
- 失效转移:实例在任务执行失败后,会被转移到其他实例执行
- 错过执行任务重触发:自行记录错过执行的作业,并在上次作业完成后自动触发
- 支持并行调度:任务分片,将一个任务分为多个小任务在多个实例同时执行
- 作业分片一致性:当任务被分片后,保证同一分片在分布式环境中仅一个执行实例
Elastic-Job-Lite应用
-
安装Zookeeper(需要3.4.6版本以上):存储(树形节点结构)+ 通知(监听节点,取值变化/子节点变化)
tar -zxvf apache-zookeeper-3.6.2-bin.tar.gz cd apache-zookeeper-3.6.2-bin/ cd conf/ cp zoo_sample.cfg zoo.cfg cd ../bin/ ./zkServer.sh start # 启动 ./zkServer.sh status # 查看状态 ./zkServer.sh stop # 停止 -
示例程序
pom.xml
<dependency> <groupId>com.dangdang</groupId> <artifactId>elastic-job-lite-core</artifactId> <version>2.1.5</version> </dependency>ArchiveJob.java 定时任务类
public class ArchiveJob implements SimpleJob { @Override public void execute(ShardingContext shardingContext) { // 省略 } }ElasticJobMain.java
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("47.101.165.107:2181", "data-archive-job"); CoordinatorRegistryCenter coordinatorRegistryCenter = newZookeeperRegistryCenter(zookeeperConfiguration); coordinatorRegistryCenter.init(); JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration .newBuilder("archive-job", "*/2 * * * * ?", 1).build(); SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, ArchiveJob.class.getName()); JobScheduler jobScheduler = new JobScheduler(coordinatorRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build()); jobScheduler.init();
Elastic-Job-Lite轻量级去中心化的特点
- 轻量级
- All in jar, 必要依赖仅需要zookeeper
- 并非独立部署的中间件
- 去中心化
- 执行节点对等
- 定时调度自触发(没有中心调度节点)
- 服务自发现
- 主节点非固定
任务分片
-
将大的耗时作业分为多个子task(即任务分片),每一个task交给一个实例(一个实例可以处理多个task)
-
每个task执行什么逻辑由我们自己来定
-
Strategy策略可以定义这些分片如何分配到各个机器,默认是平分
-
修改代码
ElasticJobMain.java
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration .newBuilder("archive-job", "*/2 * * * * ?", 3) .shardingItemParameters("0=本科,1=研究生,2=博士").build();ArchiveJob.java
public class ArchiveJob implements SimpleJob { @Override public void execute(ShardingContext shardingContext) { int shardingItem = shardingContext.getShardingItem(); System.out.println("=========> 当前分片:" + shardingItem); //本科or研究生or博士 String shardingParameter = shardingContext.getShardingParameter(); String selectSql = "select * from resume where state='未归档' and education='" + shardingParameter+ "' limit 1"; // 省略 } }
Session共享问题
- session共享及session保持,又叫做session一致性
- 本质上说是因为http协议是无状态的协议,客户端和服务端在某次的会话中产生的数据不会被保留下来。
- 两种用于保持Http状态的技术,Cookie(客户端)和Session(服务器)
解决Session一致性的方案
-
Nginx的
IP_Hash策略(可以使用)- 同一个客户端IP的请求会被路由到同一个目标服务器,也叫做会话粘滞
- 优点
- 配置简单
- 不入侵应用,不额外修改代码
- 缺点
- 服务器重启,Session丢失
- 单点负载高
- 单点故障
-
Session复制(不推荐)
- 多个tomcat之间通过修改配置文件,达到Session之间的复制,
- 通过网络通信的组播机制实现Session同步(TCP)
- 优点
- 不入侵应用
- 便于服务器水平扩展
- 适应各种负载均衡策略
- 服务器重启或宕机不会造成Session丢失
- 缺点
- 性能低
- 内存消耗,不能存储大多数据
- 延迟高
-
Session共享,Session集中储存(推荐)
-
缓存,Redis
-
优点
- 能适应各种负载均衡策略
- 服务器重启或者宕机不会造成Session丢失
- 扩展能力强
- 适合大集群数量使用
-
缺点
- 对应用入侵,引入了和Redis的交互代码
-
使用
pom.xml
<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>application.properties
# redis spring.redis.database=0 spring.redis.host=47.101.165.107 spring.redis.port=6379SpringBootApplication
@SpringBootApplication @EnableRedisHttpSession public class SpringbootSssApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(SpringbootSssApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(SpringbootSssApplication.class); } } -
源码解析
请求通过tomcat到达servlet容器时,通过过滤器对请求做了一次封装。在Redis中取Session,取不到则创建并提交
EnableRedisHttpSession - RedisHttpSessionConfiguration - SessionRepositoryFilter#doFilterInternal
-