在xxl-rpc中提供了5中负载均衡的算法,包括轮询、随机、LRU、LFU、一致性HASH可供用户选择。
包位置:
/**
* 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;
* a、virtual node:解决不均衡问题
* b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围
*
* @author xuxueli 2018-12-04
*/
public abstract class XxlRpcLoadBalance {
// addressSet 是 serviceKey 的地址集合
public abstract String route(String serviceKey, TreeSet addressSet);
}
随机算法很简单,使用了随机类 Random生成随机数。
public class XxlRpcLoadBalanceRandomStrategy extends XxlRpcLoadBalance {
private Random random = new Random();
@Override
public String route(String serviceKey, TreeSet addressSet) {
// arr 将set 转成数组
String[] addressArr = addressSet.toArray(new String[addressSet.size()]);
// random random 长度
String finalAddress = addressArr[random.nextInt(addressSet.size())];
return finalAddress;
}
}
轮询的意思就是,轮着来。使用次数% 服务提供者的数量,而且使用次数count是加加操作,这样就得到的结果就是挨着的,比如说有1,2,3个服务提供者,这个算法就能够保证这次是1,下次是2,再下次是3。
/**
* round
*
* @author xuxueli 2018-12-04
*/
public class XxlRpcLoadBalanceRoundStrategy extends XxlRpcLoadBalance {
// 用于记录某个服务 次数的 通过这个次数 % 服务提供者数量 得到 选择哪个位置的机器
private ConcurrentMap routeCountEachJob = new ConcurrentHashMap();
private long CACHE_VALID_TIME = 0;
private int count(String serviceKey) {
// cache clear 过段时间就要清一下 map
if (System.currentTimeMillis() > CACHE_VALID_TIME) {
routeCountEachJob.clear();
CACHE_VALID_TIME = System.currentTimeMillis() + 24*60*60*1000;
}
// count++
Integer count = routeCountEachJob.get(serviceKey);
// 超过 1000000就要 初始化 ,初始化的时候使用随机数,防止第一次老往第一台机器上扔
count = (count==null || count>1000000)?(new Random().nextInt(100)):++count; // 初始化时主动Random一次,缓解首次压力
routeCountEachJob.put(serviceKey, count);
return count;
}
@Override
public String route(String serviceKey, TreeSet addressSet) {
// arr
String[] addressArr = addressSet.toArray(new String[addressSet.size()]);
// round
String finalAddress = addressArr[count(serviceKey)%addressArr.length];
return finalAddress;
}
}
LRU意思是最近使用最少,xxl作者是通过java LinkedHashMap 来实现的LRU,当在创建对象的时候将LinkedHashMap将accessOrder 值设置为true就可以了。
/**
* lru
*
* @author xuxueli 2018-12-04
*/
public class XxlRpcLoadBalanceLRUStrategy extends XxlRpcLoadBalance {
// 存储 serviceKey 与 address
private ConcurrentMap> jobLRUMap = new ConcurrentHashMap>();
private long CACHE_VALID_TIME = 0;
public String doRoute(String serviceKey, TreeSet addressSet) {
// cache clear 过段时间清空 map
if (System.currentTimeMillis() > CACHE_VALID_TIME) {
jobLRUMap.clear();
CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
}
// init lru
LinkedHashMap lruItem = jobLRUMap.get(serviceKey);
if (lruItem == null) { /// 初始化
/**
* LinkedHashMap
* a、accessOrder:ture=访问顺序排序(get/put时排序)/ACCESS-LAST;false=插入顺序排期/FIFO;
* b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法;
*/
lruItem = new LinkedHashMap(16, 0.75f, true){
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if(super.size() > 1000){ // size 超过1000
return true;
}else{
return false;
}
}
};
jobLRUMap.putIfAbsent(serviceKey, lruItem);
}
// put new
for (String address: addressSet) { // 添加新的
if (!lruItem.containsKey(address)) {
lruItem.put(address, address);
}
}
// remove old 将不存在记录
List delKeys = new ArrayList<>();
for (String existKey: lruItem.keySet()) {
if (!addressSet.contains(existKey)) {
delKeys.add(existKey);
}
}
// 移除不存在的
if (delKeys.size() > 0) {
for (String delKey: delKeys) {
lruItem.remove(delKey);
}
}
// load
String eldestKey = lruItem.entrySet().iterator().next().getKey();
String eldestValue = lruItem.get(eldestKey);
return eldestValue;
}
@Override
public String route(String serviceKey, TreeSet addressSet) {
String finalAddress = doRoute(serviceKey, addressSet);
return finalAddress;
}
}
原理就是将所有提供该服务的机器按照hash值(节点)从小到大的顺序排列到 hash环上(这个环就是想象中的,你把一个线性表收尾连在一起不都可以当作一个环),然后计算出这个服务key的hash值,也按照循序放到hash环上,然后顺时针取挨着你最近的那个节点就行了 ,这样每次这个服务都去找这个节点。
/**
* consustent hash
*
* 单个JOB对应的每个执行器,使用频率最低的优先被选举
* a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数
* b、LRU(Least Recently Used):最近最久未使用,时间
*
* @author xuxueli 2018-12-04
*/
public class XxlRpcLoadBalanceConsistentHashStrategy extends XxlRpcLoadBalance {
private int VIRTUAL_NODE_NUM = 5;
/**
* get hash code on 2^32 ring (md5散列的方式计算hash值)
* @param key
* @return
*/
private long hash(String key) {
// md5 byte
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not supported", e);
}
md5.reset();
byte[] keyBytes = null;
try {
keyBytes = key.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unknown string :" + key, e);
}
md5.update(keyBytes);
byte[] digest = md5.digest();
// hash code, Truncate to 32-bits
long hashCode = ((long) (digest[3] & 0xFF) << 24)
| ((long) (digest[2] & 0xFF) << 16)
| ((long) (digest[1] & 0xFF) << 8)
| (digest[0] & 0xFF);
long truncateHashCode = hashCode & 0xffffffffL;
return truncateHashCode;
}
public String doRoute(String serviceKey, TreeSet addressSet) {
// ------A1------A2-------A3------
// -----------J1------------------
TreeMap addressRing = new TreeMap();
for (String address: addressSet) {// 添加虚拟的节点 ,防止 节点少的时候发生故障,然后造成雪崩事故。
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
long addressHash = hash("SHARD-" + address + "-NODE-" + i);
addressRing.put(addressHash, address);
}
}
// 获得serviceKey的hash值
long jobHash = hash(serviceKey);
// tailMap 方法就是获取 key 大于 参数的所有值
SortedMap lastRing = addressRing.tailMap(jobHash);
if (!lastRing.isEmpty()) {
// 找到了就取lastRing 挨着 它最近的那个, 也就是第一个 ,正好符合一致性hash 顺时针取挨着最近的要求的 要求
return lastRing.get(lastRing.firstKey());
}
// 没有获取到,说明这个hash值是最大的了,后面没有了 ,就取最小的那个。也就是整个hash 环最小的那个
return addressRing.firstEntry().getValue();
}
@Override
public String route(String serviceKey, TreeSet addressSet) {
String finalAddress = doRoute(serviceKey, addressSet);
return finalAddress;
}
}
LFU(Least Frequently Used)的意思就是在一段时间内根据数据历史访问频率来淘汰数据,数据访问频率越低就容易被淘汰,也就是说数据访问频率越高,就越不容易被淘汰。
这里作者按照访问频率来取出访问最低的那个,要是都是从0 开始,没有中途加入的话,效果跟轮询算法一样,但是你要是中途有加入的话,就会一直访问 新加入的那个,知道追赶上频率倒数第二名,然后两个再交替访问,其实最终会实现轮询效果。
/**
* lru
*
* @author xuxueli 2018-12-04
*/
public class XxlRpcLoadBalanceLFUStrategy extends XxlRpcLoadBalance {
// key : 服务key value: 机器:使用次数
private ConcurrentMap> jobLfuMap = new ConcurrentHashMap>();
private long CACHE_VALID_TIME = 0;
public String doRoute(String serviceKey, TreeSet addressSet) {
// cache clear 过段时间就清空
if (System.currentTimeMillis() > CACHE_VALID_TIME) {
jobLfuMap.clear();
CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
}
// lfu item init
HashMap lfuItemMap = jobLfuMap.get(serviceKey); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList;
if (lfuItemMap == null) {
lfuItemMap = new HashMap();
jobLfuMap.putIfAbsent(serviceKey, lfuItemMap); // 避免重复覆盖
}
// put new
for (String address: addressSet) {
// 之前没有过 或者是调用次数 大于1000000 就进行初始化
if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) {
lfuItemMap.put(address, 0);
}
}
// remove old 移除那种不存在的
List delKeys = new ArrayList<>();
for (String existKey: lfuItemMap.keySet()) {
if (!addressSet.contains(existKey)) {
delKeys.add(existKey);
}
}
if (delKeys.size() > 0) {
for (String delKey: delKeys) {
lfuItemMap.remove(delKey);
}
}
// load least userd count address 按照次数进行排序
List> lfuItemList = new ArrayList>(lfuItemMap.entrySet());
Collections.sort(lfuItemList, new Comparator>() { // 根据次数 从小到大排序
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getValue().compareTo(o2.getValue());
}
});
Map.Entry addressItem = lfuItemList.get(0);
String minAddress = addressItem.getKey();
addressItem.setValue(addressItem.getValue() + 1);
return minAddress;
}
@Override
public String route(String serviceKey, TreeSet addressSet) {
String finalAddress = doRoute(serviceKey, addressSet);
return finalAddress;
}
}