文章目录
- 负载均衡的提出
- 手动实现
- 轮询实现法
- 随机法
负载均衡的提出
一个网络应用应该有服务器和客户端两个角色。在我们以前写的网络项目中,例如以前我们用C/S框架写的聊天室应用。其本质是一个服务器和多个客户端。
这些客户端和服务器是长连接,在这种单服务器的情况下,服务器成为整个应用的核心,如果服务器崩掉,客户端所有请求将会失败。如果服务器要进行升级,那就得全体用户下线,服务停止。或者说同一个服务器总有自己的最大承载用户量吧,这种肯定不能满足用户们日益增长的需求。 因此就提出了多个服务器。服务器数量相较于客户端数量还是较少的。只是不同的客户端可以连接的不同的服务器。
但是客户端怎么连接多个服务器的某一个服务器?就需要将所有服务器的相关地址信息(ip和port)配置到所有客户端上。若新增一个服务器,就必须将所有客户端的配置文件进行刷新。这个问题变的艰难。 上面两种方式,由于现在应用的客户端数量是很恐怖的,百万千万。为此,有人就想出了分流服务器,专门管理相关APP服务器信息的服务器。
一开始所有客户端只识的这个分流服务器,分流服务器与APP服务器不一样,它本身不产生也不提供相关APP的业务服务。它只是通过某种策略给客户端分配服务器地址,去找相应的APP服务器进行相关的服务。这种解法就解决了上述客户端必须将相应的服务器地址提前保存在本地,也使得增加一个客户端的问题得到解决。
但是还是出现问题了,随着RMI技术的出现和应用。客户端可以不止对一个服务器进行请求,可以换着请求不同的服务器。或者说根据服务器的健康状态进行负载均衡了(不能狂对一个服务器进行轰炸式请求你说是吧?)。
在代码层面解释就是。
在初始化设置相关ip和port时,不是固定的,而是挑选一个合适的进行连接。
手动实现
首先给出INetNode.java
接口
public interface INetNode {
String getIp();
int getPort();
}
我们知道ip和port是整个负载均衡最基础的数据了,根据以前面对对象的思想,应该是个mode类啊,为什么这里设置为接口?没错,确实是接口,因为这次框架我们准备用面向接口编程来解决,来体现与面向对象的异同。
NetNode是整个负载均衡中最基础的数据了。也为了以后提醒用户要设置ip和port。一切的最底层数据是INetNode就可以提醒用户设置了,以免忘记set值。
public class DefaultNetNode implements INetNode {
private String ip;
private int port;
public DefaultNetNode() {
}
public DefaultNetNode(String ip, int port) {
this.ip = ip;
this.port = port;
}
public void setIp(String ip) {
this.ip = ip;
}
public void setPort(int port) {
this.port = port;
}
@Override
public String getIp() {
return ip;
}
@Override
public int getPort() {
return port;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((ip == null) ? 0 : ip.hashCode());
result = prime * result + port;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DefaultNetNode other = (DefaultNetNode) obj;
if (ip == null) {
if (other.ip != null)
return false;
} else if (!ip.equals(other.ip))
return false;
if (port != other.port)
return false;
return true;
}
@Override
public String toString() {
return "[" + ip + "," + port + "]";
}
}
DefaultNetNode加hashcode和equals方法是为了以后的比较。同一个ip和port才能确定一个服务器的同一个服务。
public interface INetNodeBalance {
void addNetNode(INetNode netNode);
INetNode removeNetNode(INetNode netNode);
INetNode getNetNode();
}
INetNodeBalance定义了该负载均衡框架需要做哪些事。增加,删除和得到网络结点。面向接口编程,一个接口定下整个框架的基调。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class AbstractNetNodeBalance implements INetNodeBalance {
protected final Map<Integer, INetNode> nodePool;//protected以便包内传递使用
public AbstractNetNodeBalance() {
nodePool = new ConcurrentHashMap<>();
}
@Override
public void addNetNode(INetNode netNode) {
if (netNode == null) {
return;
}
int nodeHashCode = netNode.hashCode();
if (nodePool.containsKey(nodeHashCode)) {
return;
}
nodePool.put(nodeHashCode, netNode);
}
@Override
public INetNode removeNetNode(INetNode netNode) {
if (netNode == null) {
return null;
}
return nodePool.remove(netNode.hashCode());
}
public boolean isNodePoolEmpty() {
return nodePool.isEmpty();
}
}
我们需要先有个存这些网络节点的池子。之所以是个抽象的因为他没有完成getnode方法。它不是真正的负载均衡做法,只是抽象的大概完成对池子的维护。
轮询实现法
因为节点池是个Map,没有顺序关系,因此我们轮询法底层使用的数据结构是双向链表。
public class PollingNetNode extends DefaultNetNode {
private PollingNetNode next;
private PollingNetNode pre;
protected PollingNetNode() {
//PollingNetNode是内部使用的数据结构,因为考虑到可能会是第一个元素,因此前驱和后继都指向自己
this.next = this;
this.pre = this;
}
protected PollingNetNode(String ip, int port) {
super(ip, port);
this.next = this;
this.pre = this;
}
public PollingNetNode getNext() {
return next;
}
public void setNext(PollingNetNode next) { //set方法就是改变指向
this.next = next;
}
public PollingNetNode getPre() {
return pre;
}
public void setPre(PollingNetNode pre) {
this.pre = pre;
}
}
PollingNetNode作为工具内部的数据结构,作为轮询法最基础的数据。继承的DefaultNetNode里的ip和port就是值域。实际要形成一个双向链表。
public class PollingBalance extends AbstractNetNodeBalance {
private PollingNetNode headNode;
private PollingNetNode currentNode;
public PollingBalance() {
super();
}
@Override
public synchronized void addNetNode(INetNode netNode) {
PollingNetNode newNode = new PollingNetNode(netNode.getIp(), netNode.getPort());
if (headNode == null) { //如果加进来的是首元素
headNode = newNode;
currentNode = headNode;
super.addNetNode(newNode);
return;
}
//先修改新节点的链域值,再改头结点的
newNode.setPre(headNode.getPre()); //newNode.pre = head.pre 新节点前驱节点为头节点前驱节点
newNode.setNext(headNode); //newNode.next = head 新节点后继节点为头节点
headNode.setPre(newNode);//head.pre = newNode头节点前驱节点为新节点
newNode.getPre().setNext(newNode);//newNode.pre.next = newNode再让新节点的前一个节点的后继节点为新节点
super.addNetNode(newNode);//因为每次加的newNode,因此存在池子里的就是有双向链表关系的netNode元素
//这一行的错我找了好久,写成super.addNetNode(netNode);了
}
@Override
public synchronized INetNode removeNetNode(INetNode netNode) {
PollingNetNode target = new PollingNetNode(netNode.getIp(), netNode.getPort());
target = (PollingNetNode) super.removeNetNode(target); //都是从池子里面取的,池子取得的才是真正的引用
if (target == null) { //表明池子根本没有这个东西
return null;
}
if (isNodePoolEmpty()) {
headNode = null;
currentNode = null;
return headNode;
}
if (currentNode == target) { //如果要删除的是当前的,就要把当前的往后移动下一个
currentNode = target.getNext();
}
if (headNode == target) { //如果删除的是头结点,就要让头结点为下一个
headNode = target.getNext();
}
target.getPre().setNext(target.getNext());//target.pre.next = target.next
target.getNext().setPre(target.getPre()); //target.next.pre = target.pre
target.setPre(target);//让它指回自己不要指双向链的东西
target.setNext(target);//为了安全,否则外面就能触碰到链了
return target;
}
@Override
public synchronized INetNode getNetNode() {//怎么轮询?少了个当前node,加currentNode
INetNode result = currentNode;
if (currentNode != null) {
currentNode = currentNode.getNext();
}
return result;
}
}
PollingBalance就是我们的轮询平衡策略。
随机法
import java.util.LinkedList;
import java.util.List;
public class RandomBalance extends AbstractNetNodeBalance {
private final List<INetNode> nodeList;
public RandomBalance() {
super();
nodeList = new LinkedList<>();
}
@Override
public void addNetNode(INetNode netNode) {
super.addNetNode(netNode);
if (netNode == null ||nodeList.contains(netNode)) {
return;
}
nodeList.add(netNode);
}
@Override
public INetNode removeNetNode(INetNode netNode) {
if (netNode == null || !nodeList.contains(netNode)) {
return null;
}
nodeList.remove(netNode);
return super.removeNetNode(netNode);
}
@Override
public INetNode getNetNode() {
if (isNodePoolEmpty()) {
return null;
}
int index = ((int) (Math.random() * 10000)) % nodeList.size();
return this.nodeList.get(index);
}
}
再实现一种负载均衡的方案,面向接口编程的好处,implement 负载均衡规则(INetNodeBalance)即可。随机选取法,随机选择众多的一个。
数据结构用arraylist和linklist都行。Arraylist因为是随机数选取,O(1)下标定位。Linklist会因为服务器有可能来来去去,增删,所以linklist也行。反正都行,看需求进行选取。
还记得前面讲的这个框架是面向接口编程吗?面向接口编程的好处就在RMI里面体现!
正是通过面向接口编程,提供了若干有关负载均衡的手段!我们在框架中提供的PollingBalance和RandomBalance。甚至用户可以自定义相关负载均衡策略,只需要实现接口就行!