Spring Cloud微服务网关Zuul动态路由配置优化和手动触发路由刷新

一、前文必看

Spring Cloud微服务网关Zuul动态路由配置。在前文中留了两个小坑。在本文将怕它给填了,所以前一篇文章建议看一下。

二、DynamicZuulRouteLocator小优化

在前文中提到,HeartbeatEvent事件会频繁触发,每次都需要去查询数据库。而且ZuulRefreshListener监听的其余四个刷新事件也不会经常触发。所以这里就可以做一下小优化,因为系统上线稳定后,路由一般是不会经常变动的。所以我们可以把路由规则配置给缓存起来,这样就不会每次的心跳都去查询数据库。

这里有分两种情况:

  • 单机Zuul网关下的本地缓存
  • Zuul网关集群下的分布式缓存

2.1 本地缓存

直接上代码:

public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    private final ZuulProperties zuulProperties;

    private final RoutingRuleService routingRuleService;

    private static final ConcurrentHashMap<String, ZuulRoute> routesCache = new ConcurrentHashMap<>();

    public DynamicZuulRouteLocator(String servletPath, ZuulProperties properties, RoutingRuleService routingRuleService) {
        super(servletPath, properties);
        this.zuulProperties = properties;
        this.routingRuleService = routingRuleService;
    }

    @Override
    public void refresh() {
        doRefresh();
    }

    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        Map<String, ZuulProperties.ZuulRoute> allRoutes = null;
        if (routesCache.isEmpty()) {
            allRoutes = routingRuleService.findAllRoutes();
            routesCache.putAll(allRoutes);
        } else {
            allRoutes = routesCache;
        }
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        // 父类的 SimpleRouteLocator的locateRoutes()读取的是配置文件中的路由规则配置
        routesMap.putAll(super.locateRoutes());
        routesMap.putAll(allRoutes);
        LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
        routesMap.forEach((key, value) -> {
            String path = key;
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.zuulProperties.getPrefix())) {
                path = this.zuulProperties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, value);
        });
        return values;
    }

    public static void clearCache() {
        routesCache.clear();
    }
}

这是简单地使用了ConcurrentHashMap作为本地缓存。当然也可以使用caffeineGuava CacheEhcache等本地缓存框架。使用ConcurrentHashMap主要是更加方便快捷,简单明了。例如像RocketMQ也有使用了ConcurrentHashMap来缓存路由信息。

这样在数据库面前就挡了层缓存,优化了查询性能,不需要每次的心跳都去查数据库。

2.2 Redis分布式缓存

一般情况下为了高可用,甚至是高并发的情况下,我们的微服务网关都是以集群的形式在线上部署的。上面的本地缓存并不适合在分布式的环境下使用。当然也已使用Redis作为一级缓存,然后本地缓存作为二级缓存。

这时候情况就复杂起来了。这里我画了一个简单的架构图设计图:

zuul-cache-arch.jpg
有两种设计方案:

  • 方案1:Zuul集成路由规则的CRUD接口。对于路由管理或者说网关管理的接口集成在Zuul网关中,Zuul网关配置数据源。对路由CRUD的操作通过负载均衡到Zuul网关,然后更新Redis缓存。这样的话便不能使用本地缓存了,容易出现数据不一致的情况,当然如果能够维护好一级缓存和二级缓存的话,也是可以。
    • 这种设计缺点较多,首先以来了Redis,提高了整个网关的复杂度,可用性也随之降低。
    • 其次业务耦合度较高,依赖数据源,因为需要对外提供了系列接口。
  • 方案2:单独一个管理后台应用,在这里对数据源进行操作。当路由信息发生改变的时候通过WebSocketZookeeperNacos或者Consul等手段来通知刷新本地缓存的路由信息。
    • 这样的话Zuul网关专注做路由转发,把业务解耦出来,设计更加的轻量级。Apache Shenyu网关就是这样做的。

三、实现手动触发路由刷新

看到这里应该知道,对于动态路由配置,我们是要提供一系列的接口来进行维护和操作的。当我们对路由进行了增删改后,是要去刷新Zuul中的路由信息的。按照这样手动修改路由信息触发的方式可以实现的有两种:全量和增量。

接下来的实现是以上面章节所说的方案1来做的,也没有使用分布式缓存,把源码中的本地缓存替换成Redis就行了,主要是因为篇幅受限以及我们主要学习的是实现的原理。请勿直接用在生产上,仅供学习。

3.1 全量触发

OK,直接入正题。这里准备一些接口,从Controller到Service。

RoutingRuleController:

@RestController
@RequestMapping("route")
public class RoutingRuleController {

    @Autowired
    private RoutingRuleService routingRuleService;

    @PostMapping("add")
    public Object addRoute(@RequestBody RoutingRule routingRule) {
        routingRuleService.save(routingRule);
        return "success";
    }

    @PostMapping("update")
    public Object updateRoute(@RequestBody RoutingRule routingRule) {
        routingRuleService.save(routingRule);
        return "success";
    }

    @DeleteMapping("delete")
    public Object updateRoute(Long id) {
        routingRuleService.delete(id);
        return "success";
    }
}

接口:

public interface RoutingRuleService {

    Map<String, ZuulProperties.ZuulRoute> findAllRoutes();

    void save(RoutingRule routingRule);

    void delete(Long id);
}

接口实现:

@Service
public class RoutingRuleServiceImpl implements RoutingRuleService {

    @Autowired
    private RoutingRuleDao routingRuleDao;
    @Autowired
    private ApplicationEventPublisher publisher;

    @Override
    public Map<String, ZuulProperties.ZuulRoute> findAllRoutes() {
        RoutingRule routingRule = new RoutingRule();
        routingRule.setEnabled(1);
        Example<RoutingRule> example = Example.of(routingRule);
        List<RoutingRule> routingRuleList = routingRuleDao.findAll(example);

        Map<String, ZuulProperties.ZuulRoute> zuulRouteMap = new LinkedHashMap<>();

        routingRuleList.stream().filter(item -> StringUtils.isNotBlank(item.getPath()))
                .forEach(item -> {
                    ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
                    BeanUtils.copyProperties(item, zuulRoute);
                    zuulRoute.setId(String.valueOf(item.getId()));
                    zuulRouteMap.put(item.getPath(), zuulRoute);
                });
        return zuulRouteMap;
    }

    @Override
    public void save(RoutingRule routingRule) {
        RoutingRule rule = routingRuleDao.saveAndFlush(routingRule);
        publisher.publishEvent(new RefreshRouteEvent(rule));
    }

    @Override
    public void delete(Long id) {
        routingRuleDao.deleteById(id);
        RoutingRule routingRule = new RoutingRule();
        routingRule.setId(id);
        publisher.publishEvent(new RefreshRouteEvent(routingRule));
    }
}

从代码可以看到,在路由信息发生变更的时候,我们是通过Spring的事件发布,去通知Listener去完成剩下的操作的。这样可以实现业务上的解耦。

其实也不一定是要用Spring的事件发布,可以使用Disruptor框架来实现高性能内存队列;当然也可以参考Apache Shenyu里面实现的disruptor。我这里为了方便就使用了Spring的事件发布了。

定义一个事件RefreshRouteEvent:

public class RefreshRouteEvent extends ApplicationEvent {

    public RefreshRouteEvent(Object source) {
        super(source);
    }
}

还有监听器RefreshRouteListener: 这里的监听器做法有很多

  1. 像下面代码里面那样直接清空DynamicZuulRouteLocator里的本地缓存,然后发布路由刷新事件,让ZuulRefreshListener去刷新缓存。
@Component
public class RefreshRouteListener implements ApplicationListener<ApplicationEvent> {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Autowired
    private RouteLocator routeLocator;

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof RefreshRouteEvent) {
            DynamicZuulRouteLocator.clearCache();
            publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
        }
    }
}
  1. 可以在RefreshRouteListener里面把数据库里面的路由查询出来,然后设置到DynamicZuulRouteLocator的本地缓存中。

3.2 增量触发

每次修改单个路由配置就触发全量的路由加载,这很明显不符合我们程序开发的追求。但是SimpleRouteLotor的方法里面并没有添加单个或删除单个路由配置的方法。我们再看一下之前的类图:

DiscoveryClientRouteLocator.png

显然还有DiscoveryClientRouteLocatorCompositeRouteLocator,去这两个的源码会看到DiscoveryClientRouteLocator就是我们想要找的那个类。至于CompositeRouteLocator是整合Spring容器中所有的RouteLocator,它的操作就是遍历所有的RouteLocator去操作路由。

ZuulRefreshListener监听器的刷新执行的方法是org.springframework.cloud.netflix.zuul.filters.CompositeRouteLocator#refresh,可以看到是遍历所有的RouteLocator执行refresh操作。

public class CompositeRouteLocator implements RefreshableRouteLocator {
	...
	@Override
	public void refresh() {
		for (RouteLocator locator : routeLocators) {
			if (locator instanceof RefreshableRouteLocator) {
				((RefreshableRouteLocator) locator).refresh();
			}
		}
	}
}

DiscoveryClientRouteLocatorZuulProxyAutoConfiguration被注入到了Spring容器。使用的时候直接@Autowired引用就行。

所以有两种写法:

  • 使用DiscoveryClientRouteLocator
  • 修改DynamicZuulRouteLocator

这里使用第二种写法,接下来修改代码:

DynamicZuulRouteLocator添加方法removeRoute(String id)addRoute(ZuulRoute zuulRoute):

public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    ...同上

    public void removeRoute(String id) {
        for (String path : routesCache.keySet()) {
            ZuulRoute zuulRoute = routesCache.get(path);
            if (org.apache.commons.lang3.StringUtils.equals(id, zuulRoute.getId())) {
                routesCache.remove(path);
                // 刷新 SimpleRouteLocator 里的路由信息
                refresh();
            }
        }
    }

    public void addRoute(ZuulRoute zuulRoute) {
        routesCache.put(zuulRoute.getPath(), zuulRoute);
        // 刷新 SimpleRouteLocator 里的路由信息
        refresh();
    }
}

因为最终所有的路由信息是保存在SimpleRouteLocator里的private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();,所以子类修改了路由信息需要刷新到SimpleRouteLocator;

修改事件:

public class RefreshRouteEvent extends ApplicationEvent {

    private boolean isDelete = false;

    public RefreshRouteEvent(Object source) {
        super(source);
    }

    public boolean isDelete() {
        return isDelete;
    }

    public void setDelete(boolean delete) {
        isDelete = delete;
    }
}

修改service的delete方法:

@Service
public class RoutingRuleServiceImpl implements RoutingRuleService {

    ...

    @Override
    public void delete(Long id) {
        routingRuleDao.deleteById(id);
        RoutingRule routingRule = new RoutingRule();
        routingRule.setId(id);
        RefreshRouteEvent refreshRouteEvent = new RefreshRouteEvent(routingRule);
        refreshRouteEvent.setDelete(true);
        publisher.publishEvent(refreshRouteEvent);
    }
}

最后修改监听器:

@Component
public class RefreshRouteListener implements ApplicationListener<ApplicationEvent> {

    @Autowired
    private DynamicZuulRouteLocator dynamicZuulRouteLocator;

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof RefreshRouteEvent) {
            // 全量加载路由信息
            //DynamicZuulRouteLocator.clearCache();
            //publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
            // 增量修改路由信息
            RefreshRouteEvent refreshRouteEvent = (RefreshRouteEvent) event;
            RoutingRule routingRule = (RoutingRule) event.getSource();
            if (refreshRouteEvent.isDelete()) {
                dynamicZuulRouteLocator.removeRoute(String.valueOf(routingRule.getId()));
            } else {
                ZuulRoute zuulRoute = new ZuulRoute();
                BeanUtils.copyProperties(routingRule, zuulRoute);
                zuulRoute.setId(String.valueOf(routingRule.getId()));
                dynamicZuulRouteLocator.addRoute(zuulRoute);
            }
        }
    }
}

四、总结

通过两篇文章讲述了Zuul网关的动态路由配置的原理和实战,RouteLocator接口的类图并不复杂,很多Zuul的组件在ZuulServerAutoConfigurationZuulProxyAutoConfiguration里面注入到了Spring容器,可以看一下里面都有什么组件,并分析一下其功能,能够很快地理解Zuul的一些功能原理。

最后

可以关注我的微信公众号,有更多的技术干货文章
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/397390.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

云HIS定义,云HIS系统源码,云HIS建设方法,云HIS发展机制

一、重新定义HIS&#xff1a; 传统HIS是基于局域网的医院信息系统&#xff0c;云HIS全称为基于云计算的医疗卫生信息系统&#xff08;Cloud-Based Healthcare Information System&#xff09;&#xff0c;是运用云计算、大数据、物联网等新兴信息技术&#xff0c;按照现代医疗卫…

http前生今世

HTTP/0.9&#xff0c;仅支持GET方法&#xff0c;并且响应中没有HTTP头信息&#xff0c;只有文档内容。 HTTP/1.0增加了对POST方法、状态码、HTTP头信息等的支持&#xff0c;这一版本也是广泛应用的历史性版本。 HTTP/1.1引入了持久连接&#xff08;Persistent Connections&…

照片去除多余人物的方法分享之三分钟教你怎么去除

在拍摄照片时&#xff0c;有时候会遇到照片中有多余的人物&#xff0c;这会影响照片的美观度和主题表达。去除照片中多余的人物&#xff0c;需要采用一些技巧和方法。本文将介绍几种常用的去除照片中多余人物的方法。 一、使用水印云软件去除多余人物 水印云是一款功能强大的图…

[转载]31省市数字经济发展规划(2024版)

近期&#xff0c;国家统计局、国家税务总局、工业和信息化部等部门先后在国新办新闻发布会上发布2023年发展成绩单&#xff0c;其中&#xff0c;数字经济核心产业频频出现在发展成绩单中。 我国数实融合加快推进。根据国家税务总局发布的数据&#xff0c;2023年我国数字经济核…

JAVA高并发——锁的优化

文章目录 1、减少锁持有时间2、减小锁粒度3、用读写分离锁来替换独占锁4、锁分离5、锁粗化 锁是最常用的同步方法之一。在高并发的环境下&#xff0c;激烈的锁竞争会导致程序的性能下降&#xff0c;因此我们有必要讨论一些有关锁的性能的问题&#xff0c;以及一些注意事项&…

vulhub中Apache Log4j Server 反序列化命令执行漏洞复现(CVE-2017-5645)

Apache Log4j是一个用于Java的日志记录库&#xff0c;其支持启动远程日志服务器。Apache Log4j 2.8.2之前的2.x版本中存在安全漏洞。攻击者可利用该漏洞执行任意代码。 1.我们使用ysoserial生成payload&#xff0c;然后直接发送给your-ip:4712端口即可。 java -jar ysoserial-…

微软和OpenAI将检查AI聊天记录,以寻找恶意账户

据国外媒体报道&#xff0c;大型科技公司及其附属的网络安全、人工智能产品很可能会推出类似的安全研究&#xff0c;尽管这会引起用户极度地隐私担忧。大型语言模型被要求提供情报机构信息&#xff0c;并用于帮助修复脚本错误和开发代码以侵入系统&#xff0c;这将很可能会成为…

01 Qt自定义风格控件的基本原则

目录 1.继承原生控件 2.组合原生控件 3.仿写原生控件 PS:后续将继续分享开发实践中各类自定义控件的方法、思路以及组件库 1.继承原生控件 关键字&#xff1a;继承、paintEvent 这里想说的是&#xff0c;Qt的Gui框架在封装原生控件的同时&#xff0c; 也为开发者提供了各…

AGI|一篇小白都能看懂的RAG入门介绍!

目录 一、前言 二、LLM主要存在的问题 三、RAG 是什么&#xff1f; 四、RAG中的搜索器 &#xff08;一&#xff09;主要的检索技术 &#xff08;二&#xff09;知识库索引技术 五、RAG目前遇到的问题和展望 一、前言 随着近几年AIGC的发展&#xff0c;不仅是大模型自身在…

Web3探索加密世界:什么是空投?

随着加密货币行业的迅速发展&#xff0c;人们开始听说各种各样的术语&#xff0c;其中包括"空投"&#xff08;Airdrop&#xff09;。在这里&#xff0c;我们将深入探讨什么是空投&#xff0c;以及它在加密世界中扮演的角色。 什么是空投&#xff1f; 空投是指在加密…

2.18号c++

1.菱形继承 1.1 概念 菱形继承又称为钻石继承&#xff0c;是由公共基类派生出多个中间子类&#xff0c;又由多个中间子类共同派生出汇聚子类。汇聚子类会得到多份中间子类从公共基类继承下来的数据成员&#xff0c;会造成空间浪费&#xff0c;没有必要。 问题&#xff1a; …

使用 GitHub Codespaces 加速 Elixir 开发环境工作速度

文章目录 前言加速 Elixir 开发环境参考 前言 使用 Elixir 开发点小玩意的时候&#xff0c;面对经常需要走外网下载依赖 (Elixir 的镜像站 UPYUN 使用有时候也经常抽风) 的时候&#xff0c;为了避免需要不断的进行网络代理配置&#xff0c;有想到之前经常使用 GitHub Codespac…

Spring两大核心思想:IOC和AOP

目录 IOC:控制反转 Ioc概念 Ioc的优点 Spring Ioc AOP:面向切面编程 AOP的优点 Spring AOP 1.添加依赖 2.核心概念 3.通知的类型 4.切点表达式 5.公共切点 pointCut 6.切面优先级 Order 7.使用自定义注解完成AOP的开发 Spring AOP实现有几种方式&#xff1f; S…

【C++】类与对象(构造函数、析构函数、拷贝构造函数、常引用)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 类的6个默认成员函数 构造函数 特性 析构函数 特性 析构的顺序 拷贝构造函数 特性 常引用 前言 &…

家用电器液位开关有哪些

在当代家庭中&#xff0c;随着科技的进步和生活水平的提升&#xff0c;各种智能化家用电器成为家庭生活的重要组成部分。这些家用电器如空调、除湿机、加湿器、洗碗机、净水器等&#xff0c;在运作过程中往往需要对液体的高低位进行精确控制&#xff0c;以保证其正常、高效的运…

SpringBoot + Nacos 实现动态化线程池

1.背景 在后台开发中&#xff0c;会经常用到线程池技术&#xff0c;对于线程池核心参数的配置很大程度上依靠经验。然而&#xff0c;由于系统运行过程中存在的不确定性&#xff0c;我们很难一劳永逸地规划一个合理的线程池参数。 在对线程池配置参数进行调整时&#xff0c;一…

BioTech - 交联质谱 (Crosslinks) 的常见数据格式说明

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/136190750 交联质谱 (Crosslinking Mass Spectrometry&#xff0c;Crosslinks) 技术 是一种结合化学交联剂和质谱仪的方法&#xff0c;用于研究蛋…

第十篇:node处理404和服务器错误

🎬 江城开朗的豌豆:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 📝 个人网站 :《 江城开朗的豌豆🫛 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录</

Vue3+vite搭建基础架构(10)--- 使用less和vite-plugin-vue-setup-extend

Vue3vite搭建基础架构&#xff08;10&#xff09;--- 使用less和vite-plugin-vue-setup-extend 说明官方文档安装less测试less表达式安装vite-plugin-vue-setup-extend 说明 这里记录下自己在Vue3vite的项目使用less来写样式以及使用vite-plugin-vue-setup-extend直接定义组件…

【8】知识加工

一、概述 对信息抽取/知识融合后得到的“事实”进行知识推理以拓展现有知识、得到新知识。 知识加工主要包括三方面内容&#xff1a;本体构建、知识推理和质量评估。 二、本体构建 1.本体 定义&#xff1a;本体是用于描述一个领域的术语集合&#xff0c;其组织结构是层次结…