阅读完这篇文章后,你会对API网关的实现有一定的了解,并且以本文中的代码为基础,你将会很容易的实现一个功能齐全的API网关系统。于此同时你还会初步了解IOC、限流、淘汰算法以及负载均衡在java中的实现。本文涉及项目的github链接如下:micro-gateway。
一、API网关介绍
API网关说白了就是一个接口调用的分发系统,它的出现得益于微服务架构的流行。API网关的作用就是可以对服务器的信息进行封装,使得调用端对于系统各个模块在不同服务器上的调用无感。API网关作为一个接口调用的中转点,系统上所有接口调用都通过网关进行,并由网关对后台服务进行实际调用并返回给调用端。
为了理解的更清楚,我们假设有这么一个系统,它分为商品模块以及权限模块。当不使用网关时他的调用情况如下:
我们可以发现,当调用者需要调用两个模块的接口时需要单独调用各个模块并且在没有负载均衡功能的情况下需要指定各个服务器的ip。这种方式无疑会增加调用者的调用难度以及系统的复杂性。并且对于调用的流量把控也比较困难。
使用网关后调用示例:
使用网关后,调用者可以不必关心调用的服务器是哪台,因为网关已经把服务器区分的工作完成了,由于流量全部打到API网关上,对流量的把控也更加方便。此种方式的使用对于服务器的扩缩容也很方便,因为网关完全屏蔽了调用者对服务器的感知。服务的上线,下线可以完全由网关来控制。
二、自定义实现API网关 --micro-gateway
如下是本次API网关设计的架构图:
抱着学习以及深度挖掘原理的目的,本项目除了jar包管理使用Maven、http服务器建立使用netty外,其他模块全部为自定以实现。接下来将分小节分别讲述各个模块如IOC、限流、负载均衡、filter调用链等的实现方法。各位可按需浏览
1. IOC模块实现
什么是IOC?
IOC(Inversion of Control)即控制反转,它其实不是什么技术,而是一种设计思想。在Spring中IOC指的是由框架来控制对象的生命周期以及他们之间的依赖关系。还有一个词叫 DI(Dependency Injection) 即依赖注入。IOC依赖于DI完成对象的构建过程。 说概念性的东西永远不如直接上手代码。Talk is cheap,and I will show you the code.
若理解代码有困难的话,可以先看一下流程图
1.1 IOC、DI执行流程图
1.2 代码层面讲解
在Spring中说IOC,其实说的是IOC容器,即一个放置所有bean的地方,有了这个IOC容器,我们才能根据给出的依赖关系使用DI去完成bean的最终生成。首先,这里先给出几个注解类:
@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleComponent {
String name() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SimpleConfiguration {
String[] value() default {};
}
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SimpleBean {
String name() default "";
}
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SimpleAutowired {
String name() default "";
}
看上面这几个类的名字,应该可以发现,这里参照了Spring的注解方式。在这里我们使用SimpleConfiguration注解来标志配置类,在这个类里我们可以配合SimpleBean注解完成bean的生成,示例代码如下:
@SimpleConfiguration
public class DefaultConfiguration {
@SimpleBean
public BasicFilter basicFilter(){
return new DefaultFilterImpl();
}
@SimpleBean
public BasicParamHandler basicParamHandler(){
return new DefaultBasicParamHandlerImpl();
}
}
其实注解这个东西就是起个标记的作用。前面我们提到过,像Spring中IOC指的是IOC容器,就是存放所有bean的地方。像上面的这个示例配置类,它里面生成的Bean其实就被存放到了我们自定义实现的IOC容器中,我们称它为BeanFactory即bean工厂。
在放出这个BeanFactory的代码之前,我们需要先考虑一件事,那就是这个bean应该以什么形式存放到IOC容器中呢?换一种说法就是,我们需要对bean的元数据进行抽象,这样我们也能更加容易理解。在Spring中,存放Bean元数据的类是BeanDefinition,在这里我们对其进行简单的实现:
/**
* 这里的注解使用的是lombok,使用后我们可以不用显示
* 的写出Getter/Setter以及构造方法,代码更加简洁
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor(staticName = "of")
public class BeanDefinition {
/**
* 默认为类的全限定名 如com.test.Student
* 当@SimpleBean注解指定name时,使用@SimpleBean的name属性值作为beanName
* 全局唯一
*/
@NonNull
private String beanName;
@NonNull
private String className;
private Object bean;
private Map<String ,Object> propertyMap;
}
有了这个类后,我们便能根据@SimplConfigure以及@SimpleBean注解完成bean的生成,这个生成的过程我们放在BeanFactory中完成,以下是BeanFactory的代码:
/**
* bean生成、缓存工厂类
* @author lcb
* @date 2020/5/13
*/
public class BeanFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(BeanFactory.class);
private BeanFactory() {}
/**
* 内部静态类保证全局唯一
*/
private static class BeanFactorySingle {
private static BeanFactory beanFactory = new BeanFactory();
}
/**
* beanCacheMap缓存所有的bean对象
*/
private static Map<String, BeanDefinition> beanCacheMap = new HashMap<>();
/**
* bean的初始化,扫描所有带有@SimpleConfiguration的类,
* 并利用反射调用其中被@SimpleBean注解标记的方法完成bean的生成。
* @param packageName 要扫描的包名
* @throws Exception
*/
public void init(String packageName) throws Exception {
LOGGER.info("开始加载bean,basePackage->{}",packageName);
if (beanCacheMap.size() == 0) {
Reflections reflections = new Reflections(packageName);
Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(SimpleConfiguration.class);
for (Class<?> clazz : classSet) {
LOGGER.info("加载配置类 ->{}",clazz.getName());
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(SimpleBean.class)) {
Type type = method.getGenericReturnType();
SimpleBean simpleBean = method.getAnnotation(SimpleBean.class);
String className = type.toString().split(" ")[1];
LOGGER.info("--------生成Bean 类名->{}",className);
Object o = clazz.newInstance();
Object bean = method.invoke(o);
String beanName = "".equals(simpleBean.name()) ? className : simpleBean.name();
registry(beanName, className, bean, null);
}
}
}
}
}
public static BeanFactory getInstance(){
return BeanFactorySingle.beanFactory;
}
}
在BeanFactory中我们使用一个map来保存所有的bean对象,这个map也可以认为是我们前面提到过的IOC容器。在init方法中,我们扫描所有带有SimpleConfigure的类,并利用反射执行这些类中被@SimpleBean标记的方法。并把方法执行的返回值当做bean对象存入beanCacheMap中,并默认把方法的返回对象的权限定名当做beanName。
registry方法其实就是将bean对象构造成一个BeanDefintion对象存入beanCacheMap中。很简单,各位可以试着自己实现,或在github上下载项目源码查看。
beanFactory的init方法执行后效果如下:
有了BeanFactory即 IOC 容器后,我们需要考虑如何实现 DI 即依赖注入了。前面给了两个注解@SimpleComponent以及@SimpleAutowired的实现,以下是这两个注解使用的示例:
其实依赖注入在代码上的实现与BeanFactory的思想差不多,都是利用反射来完成对象的生成以及属性值得注入,具体代码如下:
/**
* 依赖注入工厂类
*
* @author lcb
* @date 2020/5/13
*/
public class AutowireFactory {
/**
* bean依赖关系记录
*/
private static final HashMap<String, Set<BeanDefinition>> supplyAndDemandMap = new HashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(AutowireFactory.class);
private static final Set<BeanDefinition> beanDefinitionCacheMap = new HashSet<>();
public void init(String packageName) throws Exception {
LOGGER.info("开始自动注入流程。。。");
getSupplyAndDemandMap(packageName);
loadBean();
}
/**
* 记录bean的供需关系,即优先级关系
* @param packageName
*/
public void getSupplyAndDemandMap(String packageName) {
Reflections reflections = new Reflections(packageName);
Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(SimpleComponent.class);
for (Class<?> clazz : classSet) {
Field[] fields = clazz.getDeclaredFields();
Set<BeanDefinition> set = new HashSet<>();
SimpleComponent simpleComponent = clazz.getAnnotation(SimpleComponent.class);
String demandBeanName = "".endsWith(simpleComponent.name()) ? clazz.getName() : simpleComponent.name();
LOGGER.info("扫描到需自动注入的类,className->{},beanName->{}", clazz.getName(), demandBeanName);
beanDefinitionCacheMap.add(BeanDefinition.of(demandBeanName, clazz.getName()));
for (Field field : fields) {
if (field.isAnnotationPresent(SimpleAutowired.class)) {
SimpleAutowired simpleAutowired = field.getAnnotation(SimpleAutowired.class);
Type type = field.getGenericType();
String clazzName = type.toString().split(" ")[1];
String beanName = "".equals(simpleAutowired.name()) ?
type.toString().split(" ")[1] : simpleAutowired.name();
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClassName(clazzName);
beanDefinition.setBeanName(beanName);
set.add(beanDefinition);
LOGGER.info("------- 加载前提依赖 className->{},beanName->{}", clazzName, beanName);
}
}
supplyAndDemandMap.put(demandBeanName, set);
}
}
/**
* 循环生成bean
* @throws Exception
*/
private void loadBean() throws Exception {
for (BeanDefinition beanDefinition : beanDefinitionCacheMap) {
loadBeanCore(beanDefinition);
}
}
/**
* 递归保证依赖优先级
*
* @param beanDefinition beanDefinition
* @throws Exception e
*/
private void loadBeanCore(BeanDefinition beanDefinition) throws Exception {
if (!supplyAndDemandMap.containsKey(beanDefinition.getBeanName()) || supplyAndDemandMap.get(beanDefinition.getBeanName()).size() < 1) {
registerBean(beanDefinition);
return;
}
Set<BeanDefinition> beanDefinitions = supplyAndDemandMap.get(beanDefinition.getBeanName());
for (BeanDefinition bd : beanDefinitions) {
if (BeanFactory.getInstance().getBean(bd.getBeanName()) == null) {
loadBeanCore(beanDefinition);
}
}
registerBean(beanDefinition);
}
/**
* 向BeanFactory即IOC容器中注册bean
* @param beanDefinition
* @throws Exception
*/
private void registerBean(BeanDefinition beanDefinition) throws Exception {
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass(beanDefinition.getClassName());
Object bean = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
Map<String, Object> fieldMap = new HashMap<>();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(SimpleAutowired.class)) {
SimpleAutowired simpleAutowired = field.getAnnotation(SimpleAutowired.class);
Type type = field.getGenericType();
String beanName = "".equals(simpleAutowired.name()) ?
type.toString().split(" ")[1] : simpleAutowired.name();
Object fieldBean = BeanFactory.getInstance().getBean(beanName);
field.set(bean, fieldBean);
fieldMap.put(field.getName(), fieldBean);
} else {
fieldMap.put(field.getName(), field.get(bean));
}
}
BeanFactory.getInstance().registry(beanDefinition.getBeanName(), beanDefinition.getClassName(), bean, fieldMap);
LOGGER.info("bean ->{} 加载成功", beanDefinition.getBeanName());
}
}
以上是AutowireFactory类的全部代码,可以发现它的实现比BeanFactory的实现复杂了一些。其实他的复杂性主要是需要确定bean加载的优先级造成的。
在实现中,使用一个map即supplyAndDemandMap来记录某个bean的供需关系。在beanDefinitionCacheMap记录所有的BeanDefinition,方便在完成bean优先级关系的确定后执行bean在IOC容器中的注册。
我们利用supplyAndDemandMap以及递归来保证bean按照正确的优先级进行加载,关键代码在方法loadBeanCore中。
接下来我们看看再加入依赖注入的方法后,执行的效果:
如此以来我们便完成了IOC模块的实现。
2. 负载均衡实现
负载均衡是什么?
负载均衡(Load Balance)其意思就是分摊到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。
说白了就是将流量按照一定的策略分到不同的服务器上,以我们上面提到过的商品模块为例:
我们将商品服务部署在了三台机器上,那么我们肯定要将接口调用的请求以一定的策略分到三台服务器上,不可能只使用其中的一台,其他两台闲着,那么就失去了集群部署的初衷。
负载均衡的策略都有什么?
负载均衡的策略大多提到的有这几种,轮询、加权轮询、哈希、随机、加权随机。
2.1 常见负载均衡策略的简单实现
开始写代码之前,先说明一下统一的HostEntity格式:
public class HostEntity {
private String host;
private int weight;
public HostEntity(String host, int weight) {
this.host = host;
this.weight = weight;
}
public HostEntity(String host) {
this.host = host;
this.weight = 1;
}
public String getHost() {
return host;
}
public int getWeight() {
return weight;
}
}
轮询
轮询,字面意思理解就是将所有的host排列起来,遍历的访问就可以。代码如下:
public class RoundRobin {
private static final List<HostEntity> hostCache = new ArrayList<>();
private int index = 0;
static {
hostCache.add(new HostEntity("192.168.254.2"));
hostCache.add(new HostEntity("10.10.1.3"));
hostCache.add(new HostEntity("192.168.254.3"));
hostCache.add(new HostEntity("10.10.1.4"));
hostCache.add(new HostEntity("192.168.254.4"));
hostCache.add(new HostEntity("10.10.1.5"));
}
public String getHost(){
String host = hostCache.get(index).getHost();
index = index>=hostCache.size()-1?0:index+1;
System.out.print("index->"+index);
return host;
}
public static void main(String[] args) {
RoundRobin roundRobin = new RoundRobin();
for(int i =0;i<10;i++){
System.out.println(" || host->"+roundRobin.getHost());
}
}
}
运行结果如下:
加权轮询
加权轮询与轮询相似,不同的是各个host出现的概率需要按照权重进行控制,代码如下:
public class WeightedRoundRobin {
private static final List<HostEntity> hostCache = new ArrayList<>();
private int index = 0;
static {
add(new HostEntity("192.168.254.2",1));
add(new HostEntity("10.10.1.3",3));
add(new HostEntity("192.168.254.3",2));
add(new HostEntity("10.10.1.4",1));
add(new HostEntity("192.168.254.4",3));
add(new HostEntity("10.10.1.5",1));
}
private static void add(HostEntity hostEntity){
for(int i =0;i<hostEntity.getWeight();i++){
hostCache.add(hostEntity);
}
}
public String getHost(){
String host = hostCache.get(index).getHost();
index = index>=hostCache.size()-1?0:index+1;
System.out.print("index->"+index);
return host;
}
public static void main(String[] args) {
WeightedRoundRobin weightedRoundRobin = new WeightedRoundRobin();
for(int i =0;i<10;i++){
System.out.println(" || host->"+weightedRoundRobin.getHost());
}
}
}
可以发现加权轮询与轮询方式很相似,差别就是在hostCache中添加hostEntity的个数。 执行结果如下:
哈希
哈希方式与其他集中不同的地方其实是index的取值方式,代码如下:
public class Hash {
private static final List<HostEntity> hostCache = new ArrayList<>();
static {
hostCache.add(new HostEntity("192.168.254.2"));
hostCache.add(new HostEntity("10.10.1.3"));
hostCache.add(new HostEntity("192.168.254.3"));
hostCache.add(new HostEntity("10.10.1.4"));
hostCache.add(new HostEntity("192.168.254.4"));
hostCache.add(new HostEntity("10.10.1.5"));
}
public String getHost(String formIp){
int index = formIp.hashCode()%hostCache.size();
index = index<0?-index:index;
return hostCache.get(index).getHost();
}
public static void main(String[] args) {
Hash hash = new Hash();
System.out.println(hash.getHost("140.142.98.20"));
System.out.println(hash.getHost("202.156.10.25"));
System.out.println(hash.getHost("140.142.98.21"));
System.out.println(hash.getHost("202.156.10.1"));
}
}
将重点放在getHost方法上即可,我们可以发现,他根据访问的ip将请求哈希到不同的host上。执行结果如下:
随机以及加权随机
随机以及加权随机与轮询以及加权轮询很相似,只是他们index的取值方式不同,接下来我们只放出getHost方法代码:
public String getHost(){
int index = new Random().nextInt(hostCache.size());
return hostCache.get(index).getHost();
}
以上是几种负载均衡策略的简单实现,接下来我们看看在micro-gateway中是如何实现的。
2.2 负载均衡在micro-gateway中的实现
其实在上面的实现中我们可以发现,负载均衡的实现可以抽象成两个方法,即加载数据方法以及获得host的方法,所以在micro-gateway方法中,为了拓展性我们首先抽象出一个接口BasicLBStrategyHandler,代码如下:
/**
* 负载均衡策略 使用loadData加载数据,getHost实现具体负载均衡策略
*/
public interface BasicLBStrategyHandler {
HostEntity getHost();
/**
* 加载数据,可以从数据库中或者任何其他地方
* @param apiName 接口名
* @throws Exception
*/
void loadData(String apiName) throws Exception;
}
在第一节负载均衡的简单实现中,可以发现所有的host存储在一个list中,所以在micro-gateway中,还加了一个抽象类,为了将这个list提出来。代码如下:
/**
* 提供hostEntities 初步实现加载数据功能
*/
public abstract class AbstractLBStrategyHandler implements BasicLBStrategyHandler {
public List<HostEntity> hostEntities;
public void loadData(List<HostEntity> hostEntities) {
this.hostEntities = hostEntities;
}
}
对于负载均衡,micro-gateway中给了默认实现,即随机法。代码如下:
/**
* 默认实现 示例
*/
public class DefaultLBStrategyHandler extends AbstractLBStrategyHandler {
/**
* 默认随机法
* @return 服务器地址
*/
@Override
public HostEntity getHost() {
Random random = new Random();
int pos = random.nextInt(hostEntities.size());
return hostEntities.get(pos);
}
@Override
public void loadData(String apiName) throws Exception{
// HostEntity hostEntity = new HostEntity("localhost",8888,"test",0);
HostEntity hostEntity1 = new HostEntity("localhost",8889,"",0);
List<HostEntity> list = new ArrayList<>();
// list.add(hostEntity);
list.add(hostEntity1);
this.hostEntities = list;
}
}
3.限流模块实现
3.1 限流介绍
在一个高并发系统中,保护系统有三把利器,缓存、限流以及降级。我们把重点放到限流的解释上。
什么是限流
顾名思义,限制流量。我们要搭建一个服务器的时候,需要考虑很多因素,比如服务器的请求处理的并发性能。我们知道,某一个服务器的资源总是有限的。他在同一时间能处理的请求的阈值大约是固定的。如果请求数超过这个阈值那么影响的不仅是超过这个阈值数的请求,他带来的影响可能是全局的。
限流就是要控制请求数在这个阈值之下。比如一个服务器在同一时间最多能支持1000个请求并发,那么限流模块就要使得在服务器已经在处理1000个请求时,对后续的请求进行一定的等待或者拒绝策略。
限流的算法有什么?
限流的算法常见的有这几种:计数器算法、滑动窗口算法、令牌桶算法。
计数器算法
计数器算法,简单来说就是对于某个接口,给定一个计数器,当有请求过来时计数器加一,当后续请求进来发现计数器的值大于阈值并且与第一个请求的时间间隔在规定的时间内,则判定为需要进行限流。但是这个算法有个明显的缺点,如下图:
比如1分钟内最多支持300请求,但是限流器在2分钟的边界时,两边各有200请求那么其实在这个时间段内是有400请求的。那么为了解决这个问题,就有了滑动窗口算法。
滑动窗口算法
滑动窗口算法与计数器算法的区别就是,他将这个时间间隔的粒度划分的更小,比如1分钟,划定窗口算法将可以将这个1分钟划分为6格,那么每一个格子可以有自己的计数器。对于1分钟处理请求数为600的服务起来说,1分钟如果划分为6格,每格最大出力100个请求,即时在边界处最大请求也只是200。
通过上面的图片我们也可以发现,在滑动窗口算法中,时间粒度越小,即格子划分的越多,限流效果越精确。
令牌桶算法
令牌桶的算法很巧妙,它规定每个请求要执行的时候必须要从令牌桶中拿到一个令牌,令牌桶会以一定的速率生成令牌,当达到最大值后生成的令牌丢弃。
我们本次micro-gateway中使用的限流算法为令牌桶算法
3.2 micro-gateway中限流算法的实现
首先我们将此算法抽象为两个方法,acquire方法拿令牌,addToken方法往桶中添加令牌,实现如下:
public interface BasicRateLimiter {
boolean acquire();
void addToken(final ExecutorService executorService);
}
接下来我们来看一下令牌桶算法的具体实现:
public class DefaultRateLimiter implements BasicRateLimiter {
private volatile int token;//剩余token
private final int allToken;//token最大值
private final int waitTime;//为拿到令牌等待后等待超时时间
private static long tokenOffsetAddr;//token属性 相对于DefaultRateLimiter对象的相对偏移地址 用来作为CAS更新参数
private static Unsafe unsafe;
private final Object lock = new Object();//锁对象
static {//Unsafe实例应该使用反射方式获得
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe =(Unsafe) field.get(null);
tokenOffsetAddr = unsafe.objectFieldOffset(DefaultRateLimiter.class.getDeclaredField("token"));
}catch (Exception e){
e.printStackTrace();
}
}
public DefaultRateLimiter( int waitTime) {
this.allToken = this.token = 100;
this.waitTime = waitTime;
}
/**
* 或的令牌方法
* @return true获得令牌成功,false失败
*/
@Override
public boolean acquire() {
int temp = token;//记录当前令牌数 以便CAS更新
if(temp<=0){
temp = allToken;
}
long rest = waitTime;//记录未取得令牌时 剩余的等待时间
long futureTime = System.currentTimeMillis()+waitTime;//超时时间
while (temp>0){//当temp >0时 即令牌桶中有令牌 执行自旋操作
//CAS更新令牌数 成功直接返回true 失败进行下一次尝试
if(unsafe.compareAndSwapInt(this,tokenOffsetAddr,temp,temp-1)){
return true;
}
temp = token;
if(temp<=0&&rest>0){//如果令牌桶中没有令牌了,并且等待时间大于0
synchronized (lock){//执行等待
try {
lock.wait(rest);//此处可被addToken中的lock.notifyAll()唤醒
}catch (Exception e){
e.printStackTrace();
}
}
rest = futureTime - System.currentTimeMillis();
}
}
return false;
}
/**
* 往令牌桶中增加令牌
* @param executorService
*/
@Override
public void addToken(final ExecutorService executorService) {
executorService.execute(()->{
synchronized (lock){
if(token<allToken){
this.token = this.token+1;
}
lock.notifyAll();
}
});
}
}
对于代码解释可看上面代码中的注解,很假简单,对于更新令牌数使用CAS方式。对于addToken方法,大家可以注意到,我们的入参是ExecutorService,一个线程执行器。那么为什么要这么设计呢?
其实对于往令牌桶中增加令牌,我们可以使每一个限流对象持有一个线程来对令牌数进行更新,但是这样无疑会增加系统开销。所以在这里我们使用一个统一的线程来执行这个添加操作,具体代码如下:
public class RateLimiterHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterHandler.class);
/**
* 缓存所有的限流对象,key为api名称
*/
private static final Map<String, BasicRateLimiter> map = new ConcurrentHashMap<>();
/**
* 定时线程执行器,用来更新每个限流实例中的令牌数
*/
private static final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(4);
/**
* 作为addToken方法的入参
*/
private static final ExecutorService executorService = new ThreadPoolExecutor(5, 200,
60L, TimeUnit.SECONDS, new SynchronousQueue<>());
/**
* 内部静态类保证单例
*/
private static class RateLimiterSingleton{
private static RateLimiterHandler rateLimiterHandler = new RateLimiterHandler();
}
static {
LOGGER.info("rateLimiterHandler begin work");
init();
}
/**
* 初始化方法,定时遍历map中的每个rateLimiter对象,并执行其addToken方法进行添加令牌操作
*/
private static void init(){
scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{
map.values().forEach(basicRateLimiter -> {
basicRateLimiter.addToken(executorService);
});
},1000,10,TimeUnit.MILLISECONDS);
}
private RateLimiterHandler() {
}
public static RateLimiterHandler getInstance(){
return RateLimiterSingleton.rateLimiterHandler;
}
public void add(String key, BasicRateLimiter rateLimiter){
map.put(key,rateLimiter);
}
public BasicRateLimiter get(String key){
return map.getOrDefault(key,null);
}
public void remove(String key){
map.remove(key);
}
}
我们使用一个定时线程来执行限流实例的遍历操作,并以ExcetorService为参数,传入addToken中,保证每个addToken方法的执行不会影响到其他限流实例的addToken方法执行。
4.淘汰算法实现
4.1 淘汰算法介绍
淘汰算法是啥,为什么要有它?
首先,我们知道不同与数据库的存储,直接操作内存的速度是很快的,所以对于高并发的系统来说,缓存是实现其高性能的利器,一般来说,缓存使用的是内存空间。但是,内存的空间是有限的,无论内存有多大,随着时间的推移,如果没有一定的策略去清理内存,那么他一定会被撑爆,对系统造成不可用的危险。
淘汰算法便是为了解决这一问题而生的。淘汰算法有很多种,比如LFU(最不经常使用算法)、LRU(最近最少使用算法)、ARC(自适应缓存替换算法)、FIFO(先进先出算法)、MRU(最近最常使用算法)等。
4.2 淘汰算法在micro-gateway中的实现
首先说明一下,在micro-getway中,哪个地方用到了淘汰算法。在micro-gateway中,我把接口抽象成了HttpMainBody类,如下:
@Data
public class HttpMainBody {
/**
* api
*/
private String api;
/**
* 请求方法
*/
private HttpMethod method;
/**
* 对应的策略获取器
*/
private BasicLBStrategyHandler basicLBStrategyHandler;
/**
* 限流对应的key
*/
private String rateLimitKey;
}
可以看到,这个类中含有请求的url以及对应的负载均衡策略实例以及限流对象对应的key(在RateLimiterHandler中根据key获得RateLimiter对象)。
我们的淘汰算法主要是应用在这个HttpMainBody的缓存上。他的初始化可以是从数据库中获得必要的信息,之后生成HttpMainBody对象后我们便可以在后面的请求中直接从内存中读取某个请求的对应的服务器的信息以及限流信息等,极大地加快了请求转发的速度。
接下来,看一下管理这个HttpMainBody的类:
/**
* HttpMainBody 缓存池 使用LRU淘汰策略
* @author lcb
* @date 2020/5/13
*/
public class ApiCacheHandler {
/**
* 构造方式私有化防止被显示调用
*/
private ApiCacheHandler(){
}
private static final Logger LOGGER = LoggerFactory.getLogger(ApiCacheHandler.class);
/**
* 保存所有的HttpMainBody对象
*/
private static final Map<String, HttpMainBody> cacheMap = new MyLRUCache<>();
/**
* 内部静态类保证单例模式
*/
private static class ApiCacheHandlerSingle{
private static final ApiCacheHandler apiCacheHandler = new ApiCacheHandler();
}
/**
* 淘汰算法实现的中心,集成LinkedHashMap 重写removeEldestEntry方法
* @param <K>
* @param <V>
*/
private static class MyLRUCache<K,V> extends LinkedHashMap<K,V>{
private final int maxCapacity;
public MyLRUCache(){
super(10,0.75f,true);
this.maxCapacity = 50;
}
public MyLRUCache(int initCapacity,int maxCapacity){
super(initCapacity,0.75f,true);
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
boolean ifRemove = size() > maxCapacity;
if(ifRemove){
LOGGER.info("LRU 策略执行 -> {}",eldest.getKey());
HttpMainBody value = (HttpMainBody) eldest.getValue();
//执行LRU时,删除对应的RateLimiter对象
RateLimiterHandler.getInstance().remove(value.getRateLimitKey());
}
return ifRemove;
}
}
public static ApiCacheHandler getInstance(){
return ApiCacheHandlerSingle.apiCacheHandler;
}
public synchronized void addHttpMainBody(HttpMainBody httpMainBody){
cacheMap.put(httpMainBody.getApi(),httpMainBody);
}
/**
* 根据RequestMetadata对象获得HttpMainBody对象,不存在则创建
* @param metadata
* @return
* @throws Exception
*/
public HttpMainBody getHttpMainBody( RequestMetadata metadata) throws Exception{
if(cacheMap.containsKey(metadata.getUri())){
return cacheMap.get(metadata.getUri());
}
LOGGER.info("未找到api ->{} 对应的请求元数据 ,进行初始构造。。。",metadata.getUri());
HttpMainBody httpMainBody = new HttpMainBody();
httpMainBody.setApi(metadata.getUri());
httpMainBody.setMethod(metadata.getMethod());
BasicLBStrategyHandler lbStrategyHandler = new DefaultLBStrategyHandler();
lbStrategyHandler.loadData(metadata.getUri());
httpMainBody.setBasicLBStrategyHandler(lbStrategyHandler);
BasicRateLimiter basicRateLimiter = new DefaultRateLimiter(1000*2);
httpMainBody.setRateLimitKey(metadata.getUri());
RateLimiterHandler.getInstance().add(httpMainBody.getRateLimitKey(),basicRateLimiter);
addHttpMainBody(httpMainBody);
return httpMainBody;
}
}
在mocro-gateway中,我选择使用LRU即最近最少使用算法,因为在java中利用LinkedList这个集合类可以很轻松的实现LRU。
三、执行效果
我们的测试服务接口如下:
网关调用代码如下:
执行效果如下:
Post请求:
网关打印日志
postman返回结果
GET请求:
网关打印日志
postman返回结果