Java进阶:Cluster(集群)模式潜在问题及解决方案、Web服务综合解决方案

分布式与集群:分布式一定是集群,但是集群不一定是分布式。
分布式:把一个系统【拆分】成多个子系统,每个子系统负责各自的那部分功能,独立部署,各司其职。(功能拆分)
集群:多个实例共同工作,最简单/最常见的集群是把一个应用复制多分部署。(功能不一定拆分)

——————

一致性hash算法

为什么需要使用Hash?
Hash算法较多应用在数据存储和查找领域,最经典的就是Hash表,它的查询效率非常高,其中的hash算法如果设计比较好的话,那么hash表的数据查询时间复杂度可以接近于O(1).

直接寻址法,直接把数组下标和数据绑定在一起,查找的时候直接array[n]就能找到结果。
优点:速度快,O(1)
缺点:浪费空间;重复的数据无法存储。

拉链法:数组中放链表。

————————

Hash算法的应用场景:

●请求的负载均衡(比如nginx的ip_hash策略)

Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同一个目标服务器上,避免处理session共享问题。

如果没有IP_hash策略,可以维护一张映射表,存储客户端IP或者sessionid与具体目标服务器的映射关系;如果客户端很多,映射表会很大,浪费内存空间;并且客户端上下线、目标服务器上下线,都会导致重新维护映射表,映射表维护成本很大。

如果使用哈希算法,事情就简单很多,可以对ip地址或者sessionid进行计算哈希值,哈希值与服务器数量进行取模运算,得到的值就是当前请求应该被路由到的服务器编号,这样一个客户端发来的请求就可以分配到同一台服务器。

●分布式存储

普通hash算法存在的问题:
如果nginx中有一台服务器宕机,或者nginx增加一台服务器,那么用户会被路由到新的服务器,导致session丢失(重新按hash计算分配服务器)

一致性hash算法:
哈希环,在一个区间的客户ip都分配给一个节点,顺时针方向。
当节点减少时,只影响原来路由到该节点的用户,而不影响其它用户。
当节点增加时,会影响新增节点与逆时针方向上第一个节点之间的用户,而不影响其它用户。

当hash环上的节点之间距离太小时,可能产生数据倾斜问题(数据分配不均);此时可以使用虚拟节点方式解决。

算法实现:
使用SortedMap,里面装哈希值(key),服务器ip(value),sortedMap.firstKey()是第一个服务器;
SortedMap<Integer,String> hashServerMap = new SortedMap();
//使用tailMap()方法,获得的是后续的map;
//例如,之前的map的key是1,3,5,7
//传入2或3
//获得新的map的key是3,5,7
SortedMap<Integer,String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
if(integerStringSortedMap.isEmpty(){
Integer firstKey = hashServerMap.firstKey();

}else{
Integer firstKey = integerStringSortedMap.firstKey();
}

————-

Nginx配置一致性hash负载均衡策略步骤

1.github下载nginx一致性hash负载均衡模块 https://github.com/replay/ngx_http_consistent_hash
2.解压
3.cd到nginx目录,执行命令:
./configure –add-module=/root/ngx_http_consistent_hash-master
make
make install

4.在nginx.conf文件中配置:
upstream xxxServer{
consistent_hash $request_url;
server 127.0.0.1:8080;
server 127.0.0.1:8081;

}

=====================================

问题:
如果服务器时钟不一致,会导致问题。(例如同时下单的订单,存入数据库的时间错乱)
解决方法:
1.每个节点服务器都可以连接互联网时
#使用 ntpdate 网络时间同步命令
ntpdate -u ntp.api.bz #从一个时间服务器同步时间

之后可以把这个shell命令加入到定时任务中:
linux服务器下使用crond命令。

2.某些服务器不能访问互联网,以局域网内的某台服务器的时间为准,进行统一。
例如选择A服务器的时间为准:
<1>首先设置好A的时间
<2>把A配置为时间服务器(修改/etc/ntp.conf文件)
(1)如果有 restrict default igonre,注释掉它
(2)添加:
restrict 172.22.0.0 mask 255.255.255.0 nomodify notrap #放开局域网同步功能,172.22.0.0 需要配置成自己的局域网网段
server 127.127.1.0 # local clock
fudge 127.127.1.0 stratum 10 #网络时间与硬件时间同步

(3)重启生效并配置ntpd服务开机自启动
service ntpd restart
chkconfig ntpd on

(4)集群中其它服务器就可以从A服务器同步时间了
ntpdate 127.22.0.22 #A服务器的ip

=====================================

问题:
分布式服务器数据库,如果各自使用主键自增id,那么后续对数据一起进行处理时会发现id重复,带来问题。

解决方法1:
数据库使用uuid,重复几率非常小。
问题:
uuid长度较长,没有规律,让uuid做主键建立索引时,效率不高。

解决方法2:
单独建立一个数据库表,其中的ID字段自增(再加另一个字段用于插入数据,例如createTime);当需要ID时,insert这个数据库表,然后select自增后的ID。
例如:
insert into distribute_id(createTime) values(now());
select LAST_INSERT_ID();
问题:
多使用了数据库连接,效率低;如果该数据库故障,则会影响所有流程。【不推荐使用】。

解决方法3:
雪花算法,基于这个算法生成的ID是long型,在java中一个long型是8个字节,算下来是64bit。

其中,符号位1bit(固定为0,正数),时间戳41bit,机器id10bit,序列号12bit。
某个机器在某个毫秒可以产生4000多个ID。(2^12=4096)
41位2进制用来记录时间戳,可以用70年左右。(2^41毫秒)

*大公司会封装自己的ID生成算法。

——————————

雪花算法源码笔记:
1.使用时传入服务器数;进行机器ID长度校验,如果服务器数过多(大于2^10),则报错,无法实现。
2.当前时间戳与上次时间戳校验,如果当前时间戳小于上次时间戳,抛异常(需要手动调节当前时间);如果当前时间戳等于上次时间戳,那么序列号+1,否则,将序列号赋值为0,从0开始。
如果同一个时间戳内生成的序号超过了上限,那就等到下一毫秒。

——————

解决方法4:借助redis的incr命令获取全局唯一ID
redis的incr命令将key中存储的数字+1;如果key不存在,那么key的值会先被初始化为0,然后再执行INCR操作。

——————-

redis安装:
1.官网下载redis-3.2.10.tar.gz(或者更新版本)
2.解压redis
3.cd到解压目录,对解压后的redis进行编译
make
4.然后cd进入src目录,执行make install
5.修改解压目录中的配置文件redis.conf,关掉保护模式,然后其它服务器才能访问这个redis
#bind 127.0.0.1
protected-mode no

6.在src目录下执行./redis-server ../redis.conf 启动redis服务

7.java代码中,使用jedis客户端调用redis的incr命令获得一个全局的id
(1)引入jedis客户端的jar
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>

(2)然后使用代码,从redis获取id
Jedis jedis = new Jedis("111.222.222.222",6379);
//如果key不存在,它会初始化为0;如果已存在,会将值+1并返回。
Long id = jedis.incr("id");

*redis是单线程的,所以id不会重复;而且速度很快。

——————

总结
uuid,可以用
独立数据库表,不推荐
雪花算法,可以用
redis,推荐使用;需要搭建redis集群,做好持久化。

=====================================

调度->定时任务
分布式调度->在分布式集群环境下定时任务
Elastic-job(当当网开源的分布式调度框架)

定时任务的应用场景:
订单审核、出库
订单超时自动取消、支付退款
定时备份数据

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

什么是分布式调度:
(1)运行在分布式集群环境下的调度任务(同一个定时任务要部署多份,但是只应该有一个定时任务在执行)
(2)分布式调度->定时任务的分布式->定时任务的拆分(即把一个大的作业任务拆分为多个小的作业任务,同时执行)

——————————

//设置定时任务步骤(Quartz):
1.创建任务调度器
Scheduler scheduler = QuartzMan.createScheduler();
2.创建一个任务
JobDetail job = QuartzMan.createJob();
3.创建任务的时间触发器
Trigger trigger = QuartzMan.createTrigger();
4.使用任务调度器开始定时任务
scheduler.scheduleJob(job,trigger);
scheduler.start();

———————-

设置定时任务的时间表达式:
0 0 11 * * ?
秒 分 时 日 月 星期 (年)

————

其中,年可选(可不填)
月从0-11
星期是1-7,或者SUN,MON…
*是每个
?是不确定
日与星期不能同时设置(对不上没法处理)

—————————–

Quartz任务在分布式下不太好用。
分布式调度下,推荐使用Elastic-Job
Elastic-Job是当当网开源的一个分布式调度解决方案,基于Quartz二次开发的,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。

Elastic-Job-Lite:轻量级无中心化解决方案

Elastic-Job-Lite依赖于ZooKeeper进行分布式协调,所以使用时需要:
1.引入jar包
2.安装ZooKeeper

ZooKeeper:分布式协调服务。

使用Elastic-Job-Lite的效果:
先启动一个定时任务实例,这个定时任务开始运行;
再启动一个定时任务实例,此时这个定时任务开始运行,并且上一个定时任务会停止运行;(这里不一定)
关闭第二个定时任务实例(假设它挂掉了),此时第一个定时任务会开始运行,继续第二个定时任务的工作。
原因:
每个定时任务都需要链接zookeeper,建立leader节点(类似获取锁)。
第二个定时任务实例会链接zookeeper,尝试建立leader节点,如果建立成功,则认为第二个定时任务实例是leader,并代替第一个定时任务实例执行任务;如果失败,则没有变化。
当增加或减少一个定时任务实例时,都会进行重新选举,重新确定leader节点。

——————————

Elastic-Job-Lite轻量级去中心化特点:
轻量级:
1.使用轻便,一个jar包,一个zookeeper。
2.并非独立部署的中间件,就是jar程序。
去中心化:
1.执行节点对等
2.定时调度自触发(没有中心调度节点分配)
3.服务自发现(上下线服务时,自动发现,不需要做额外操作)
4.主节点非固定(主节点不分发任务)

—————————

任务分片:
把一个任务分成多片,每片处理一部分任务。
编码步骤:
1.自己设置好要分几片
2.需要自己定好每片执行哪部分任务,可以在启动任务时调用方法传入参数,shardingItemParameters("0=a,1=b,2=c");
然后在具体任务类时从ShardingContext对象中取出参数,String param = shardingContext.getShardingParameter();
如果是第0片任务,则param=a;如果是第1片任务,则param=b;如果是第2片任务,则param=c

————–

此时,如果启动一个定时任务实例,则该实例上运行0,1,2这三片任务;
如果再启动一个实例,那么3片任务会被重新分配;(例如第一个运行1任务,第二个运行0,2任务)
如果关闭一个实例,那么3片任务也会被重新分配。

————–

注意:
1.分片项也是一个job配置,修改配置,重新分片,在下一次定时任务执行前会重新分片算法,重新进行分片。
2.如果所有的节点挂掉只剩下一个节点,那么每片任务都会集中到这个节点上;保证了高可用性。

=====================================

问题:
可能造成无法登陆的现象。
原因:
HTTP是无状态协议;
nginx把请求转发到了没有登陆session的服务器,服务器认为没有登陆,重定向到登陆页面。

解决方法:
●Nginx的IP_Hash策略(可以使用)
同一个客户端IP的请求都会被路由到同一个服务器。
优点:配置简单
缺点:服务器重启session丢失;存在单点负载高的风险;服务器故障session丢失。

●session复制(不推荐)
通过配置,让多个tomcat之间的session保持同步
原理:
tomcat集群,组播
优点:不入侵应用;便于服务器水平扩展;能适应各种负载均衡策略;服务器重启或宕机不会造成session丢失。
缺点:性能低;有延迟;消耗内存;不能存储太多数据,否则数据越多越影响性能

●session集中存储(推荐)
使用缓存中间件存储session,例如redis
优点:能适应各种负载均衡策略;服务器重启或宕机不会造成session丢失;扩展能力强;适合大集群数量使用
缺点:对应用有入侵,引入了和redis的交互代码。
使用方法:
注解@EnableRedisHttpSession

原理:
服务器收到请求前,会有一个filter对请求进行包装,从redis中取出session设置给请求头,然后再访问服务器。

实际上,那个filter是SessionRepositoryFilter,它会创建一个SessionRepositoryRequestWrapper对象,这个对象对HttpServletRequest进行包装,本质上也是一个HttpServletRequest;
SessionRepositoryRequestWrapper类中重写了getSession()等方法,当操作session时进行一些必要的步骤,其中创建一个RedisSession对象,这个对象实现了Session类;
RedisSession中也重写了方法,当从根据key从session中获取value时,进行一些必要的步骤;
filter会将session中的key与value装入map,然后按照key是sessionId、value是map的格式保存到redis中;
从redis中取出session时,实际上是取出了map,然后创建一个session对象,把map的key与value设置给这个session;
并没有直接把session对象存入redis,因为session对象(RedisSession)没有实现序列化接口,无法直接保存。
(如果手写一个实现了Serializable接口的Session对象,就可以直接存入redis了,本人亲测)
然后filter将SessionRepositoryRequestWrapper当做HttpServletRequest,使用doFilter()方法传给下一个步骤。

简单的说,需要自定义一个request(extends HttpServletRequestWrapper),自定义一个session(implements Session),写一个Filter;
然后Filter收到客户端请求时,从请求头获取jsessionid,根据jsessionid,从redis中获取session的key与value(map),赋值给session,再把session赋值给request,然后放行,供后续流程使用;
如果客户端请求头没有jsessionid,或者从redis中没有获取到session的key与value(map),则新建一个session,把jsessionid设置到cookie,供客户端下次使用;
注意,服务器给session增加key-value后,要更新redis中对应session的key与value(map)。

//可以查看源码,找到SessionRepositoryFilter进行分析。

原文链接:https://blog.csdn.net/BHSZZY/article/details/109839520

原创文章,作者:优速盾-小U,如若转载,请注明出处:https://www.cdnb.net/bbs/archives/16778

(0)
上一篇 2022年11月16日
下一篇 2022年11月16日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

优速盾注册领取大礼包www.cdnb.net
/sitemap.xml