热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

利用Nacos作为配置中心动态修改线程池核心参数

背景这篇文章的主要核心原理都来自于这个开源项目dynamic-tp,可以说是对这个开源项目的源码分析,也是对这个开源项目中涉及到的技术点进行学习总结。

背景

这篇文章的主要核心原理都来自于这个开源项目dynamic-tp,可以说是对这个开源项目的源码分析,也是对这个开源项目中涉及到的技术点进行学习总结。

从这篇文章中能学到的技术点

从这篇文章中能学到的技术点,也就是从这个dynamic-tp开源项目中学习到的技术点(这里只列举了这个项目的冰山一角):

  • 利用Nacos作为配置中心,动态监听Nacos配置变更。
  • 项目启动之初获取Nacos配置,将yml文件中配置的线程池注册到IOC容器中。这里在文章的最后再介绍,利用Spring提供的扩展点。
  • .yml文件映射到具体的对象中,对比是否有变更。
  • Spring提供的扩展点学习。

那我们开始吧

以下都是对dynamic-tp这个开源项目进行了简化,首先看一下我的Nacos配置以及配置类:

spring:dynamic:tp:enabled: trueexecutors:- threadPoolName: commonExecutorcorePoolSize: 2maximumPoolSize: 8queueCapacity: 200keepAliveTime: 50allowCoreThreadTimeOut: false- threadPoolName: dynamicThreadPoolExecutor1corePoolSize: 4maximumPoolSize: 6queueCapacity: 400keepAliveTime: 50allowCoreThreadTimeOut: false

@Data@ConfigurationProperties(prefix = "spring.dynamic.tp")public class DynamicThreadPoolProperties {​/*** 是否开始动态配置*/private boolean enabled = true;​/*** 线程池基础配置*/private List executors;​}

@Datapublic class ThreadPoolProperties {​/*** 线程池的名称*/private String threadPoolName;​/*** 核心线程数*/private int corePoolSize = 1;​/*** 最大线程数*/private int maximumPoolSize = DynamicThreadPoolConst.AVAILABLE_PROCESSORS;​/*** 线程队列数*/private int queueCapacity = 1024;​/*** 是否允许核心线程超时*/private boolean allowCoreThreadTimeOut = false;​/*** 超时时间*/private long keepAliveTime = 30;​/*** Timeout unit.*/private TimeUnit timeUnit = TimeUnit.SECONDS;}

上面这个技术点就不用多说了吧,yml文件配置的内容会映射到DynamicThreadPoolProperties类中,多个的话会映射成集合的形式。

介绍完了基础的配置,那我们开始介绍核心一点东西:监听Nacos配置变更的事件

监听Nacos配置变更的方式有多种,可以使用实现 ApplicationListener的方式也可以使用注解@NacosConfigListener

下面我们就使用实现 ApplicationListener的方式:

@Componentpublic class NacosRefresher extends AbstractRefresher implements ApplicationListener {​@Overridepublic void onApplicationEvent(NacosConfigReceivedEvent nacosConfigReceivedEvent) {refresher(nacosConfigReceivedEvent.getContent());}}

当Nacos的配置发生了变更的时候会执行onApplicationEvent方法;

AbstractRefresher类提供了refresher方法,至于为啥做成一个抽象的是因为不光Nacos可以作为配置中心,zookeeper也可以。像dynamic-tp作者不光提供了Nacos,Zookeeper,还有Apollo。

上面提到了当Nacos的配置发生了变更的时候会执行onApplicationEvent方法,onApplicationEvent方法中调用了AbstractRefresher类提供的refresher方法:

@Slf4jpublic abstract class AbstractRefresher {​@Resourceprivate DynamicThreadPoolRegistry dynamicThreadPoolRegistry;​//项目启动之初就与.yml文件中的配置项进行映射了@Resourceprivate DynamicThreadPoolProperties dtpProperties;​public void refresher(String content) {if (StringUtils.isBlank(content)) {log.warn("DynamicTp refresh, empty content.");return;}//目前先支持yml解析YamlConfigParser yamlParser = new YamlConfigParser();Map yamlMap = yamlParser.doParse(content);doRefresher(yamlMap);}​protected void doRefresher(Map properties) {if (MapUtils.isEmpty(properties)) {log.warn("DynamicTp refresh, empty properties.");return;}PropertiesBinder.bindDtpProperties(properties,dtpProperties);doRefresher(dtpProperties);}​protected void doRefresher(DynamicThreadPoolProperties dtpProperties) {​dynamicThreadPoolRegistry.refresher(dtpProperties);}}

DynamicThreadPoolProperties类在项目启动之初就与.yml文件中的配置项进行映射了,目前这里是原始值。DynamicThreadPoolRegistry类就是核心类了,后面会有介绍,我们先看看refresher方法。

refresher方法中有个yml文件解析类:YamlConfigParser类:

public class YamlConfigParser {public Map doParse(String content) {​if (StringUtils.isEmpty(content)) {return Collections.emptyMap();}YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();bean.setResources(new ByteArrayResource(content.getBytes()));return bean.getObject();}}

YamlPropertiesFactoryBean是Spring提供给我们使用yml文件解析类,将yml配置content内容传进去会解析成Map结构:

这是不是也是个技术点!

紧接着下面还有一个技术点,我们看下doRefresher方法中:PropertiesBinder.bindDtpProperties(properties,dtpProperties);

properties:是从变更的yml文件中解析出来的Map,是变更之后的新内容。

dtpProperties:是项目启动之初从原始yml文件中解析出来的内容,是旧内容。

当这两个参数传入到PropertiesBinder.bindDtpProperties(properties,dtpProperties)方法中,dtpProperties对像中的值就变成了最新的。

这是springBoot给我们提供的,是不是又学到了。这也是学习开源项目的好处,会学到一些捷径。

业务开发中如果你需要对比两个对象的属性值差别,也有一个开源的工具:equator 感兴趣可以去看下,这个工具也是在dynamic-tp开源项目中学到的。

下面就到了我们的核心类:DynamicThreadPoolRegistry

&#64;Slf4j&#64;Componentpublic class DynamicThreadPoolRegistry {//这里就不用多说了&#xff0c;项目中所有配置的线程池都会注入到这个Map结构中&#xff0c;以线程池的名称为key&#64;Resourceprivate Map threadPoolExecutorMap &#61; new HashMap<>();​public void refresher(DynamicThreadPoolProperties dtpProperties) {if (Objects.isNull(dtpProperties) || CollectionUtils.isEmpty(dtpProperties.getExecutors())) {log.warn("DynamicTp refresh, empty threadPoolProperties.");return;}//遍历Map,给线程池重新设置值。dtpProperties.getExecutors().forEach(x -> {if (StringUtils.isBlank(x.getThreadPoolName())) {log.warn("DynamicTp refresh, threadPoolName must not be empty.");return;}ThreadPoolExecutor executor &#61; threadPoolExecutorMap.get(x.getThreadPoolName());//具体的设置方法refresher(executor, x);});}​private void refresher(ThreadPoolExecutor executor, ThreadPoolProperties properties) {if (properties.getCorePoolSize() <0 ||properties.getMaximumPoolSize() <&#61; 0 ||properties.getMaximumPoolSize() ) blockingQueue).setCapacity(properties.getQueueCapacity());}}}

原理就是线程池ThreadPoolExecutor自己提供的方法&#xff1a;

至此动态修改线程池的参数就讲述完了。示例中涉及到代码没有完全贴出来&#xff0c;此示例代码在github上dynamic-thread-pool

在这里你会看到一个有点陌生的队列VariableLinkedBlockingQueue,这个队列是可以改变容量的&#xff0c;我们一般创建线程池时使用的队列是LinkedBlockingQueue&#xff0c;它的默认大小是Integer.MAX_VALUE容易内存溢出&#xff0c;当然你也可以在构造LinkedBlockingQueue时指定capacity容量大小&#xff0c;但是capacity是final修饰是没有办法改变的&#xff0c;VariableLinkedBlockingQueue队列就给它完善了这一点。使用VariableLinkedBlockingQueue队列我们就可以根据自身业务发展情况动态设置队列容量的大小。当然在dynamic-tp项目中还有设计比较巧妙的队列MemorySafeLinkedBlockingQueue&#xff0c;内存安全队列。这些队列都是可以拿到自己项目中直接使用的。这两个队列在很多开源项目中都使用到了&#xff0c;设计确实很巧妙。在这个项目中学习到了很多。

到这里我们还有一个Spring提供的扩展技术点没有讲&#xff1a;

项目启动之初获取Nacos配置&#xff0c;将yml文件中配置的线程池注册到IOC容器中

我的示例中没有涉及到这一点&#xff0c;但是dynamic-tp项目中是涉及到了的。下面我们就简单介绍一下这个扩展点&#xff0c;不光Mybatis使用到这个扩展点&#xff0c;很多需要融合Spring的开源框架都会使用到&#xff1a;Nacos&#xff0c;Dubbo&#xff0c;Apollo&#xff0c;RocketMQ太多了就不列举了。

项目启动之初获取Nacos配置这个上面也介绍了一种方式使用&#64;ConfigurationProperties(prefix &#61; "spring.dynamic.tp")将配置映射到指定的对象中&#xff0c;当然这个是有条件的&#xff0c;就是在使用此对象的类必须是Spring Bean(已经在IOC容器中)。

那如果不是呢&#xff1f;当然有别的方法&#xff0c;可以在Environment中获取。既然说到这里就把方法贴出来吧&#xff1a;

&#64;Slf4jpublic class DtpBeanDefinitionRegistrar implements EnvironmentAware {​private Environment environment;​&#64;Overridepublic void setEnvironment(Environment environment) {this.environment &#61; environment;}​public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {​DynamicThreadPoolProperties dtpProperties &#61; new DynamicThreadPoolProperties();PropertiesBinder.bindDtpProperties(environment, dtpProperties);List executors &#61; dtpProperties.getExecutors();}}

利用了SpringBoot给我们提供的捷径PropertiesBinder.bindDtpProperties();&#xff0c;这样dtpProperties对象中就有值了。和上面那个方法只是入参不一样&#xff0c;上面的入参是一个Map结构&#xff0c;这里是一个Environment对象。

下面我们继续讲我们的Sping扩展点&#xff1a;将yml文件中配置的线程池注册到IOC容器中。或者说将外部配置的对象&#xff0c;再或者说指定的类&#xff0c;注入到IOC容器中&#xff0c;在不适用&#64;Bean&#xff0c;&#64;Component&#xff0c;&#64;ComponentScan时&#xff0c;我们使用 ImportBeanDefinitionRegistrar接口。我们先来一个简单的列子&#xff1a;

首先我定一个注解&#xff08;模仿Mybatis的&#64;Mapper&#xff09;&#xff1a;

&#64;Target({ElementType.TYPE})&#64;Retention(RetentionPolicy.RUNTIME)&#64;Documented&#64;Indexedpublic &#64;interface HXY {​String value() default "";}

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {​&#64;Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {//Map annotationAttributes &#61; annotationMetadata.getAnnotationAttributes(ComponentScan.class.getName());//String[] basePackages &#61; (String[]) annotationAttributes.get("basePackages");​MyClassPathBeanDefinitionScanner scanner &#61; new MyClassPathBeanDefinitionScanner(beanDefinitionRegistry, false);//指定加上&#64;HXY注解的类放入IOC容器成为Beanscanner.addIncludeFilter(new AnnotationTypeFilter(HXY.class));scanner.doScan("cn.haoxiaoyong.example");//scanner.doScan(basePackages);}}

这里要借助ClassPathBeanDefinitionScanner

public class MyClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {public MyClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {super(registry, useDefaultFilters);}&#64;Overrideprotected Set doScan(String... basePackages) {return super.doScan(basePackages);}&#64;Overridepublic void addIncludeFilter(TypeFilter includeFilter) {super.addIncludeFilter(includeFilter);}}

只要加上&#64;HXY注解的类就会注册到IOC成为Spring Bean。这也是&#64;Mapper的实现方式。

如果你不想使用注解可以更简单一点&#xff1a;

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {&#64;Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {​beanDefinitionRegistry.registerBeanDefinition("testHxyBean",new RootBeanDefinition(TestHxyBean.class)); }}

注意&#xff1a;要在启动类&#xff0c;或者配置类上加&#64;Import(MyImportBeanDefinitionRegistrar.class)

在dynamic-tp项目中使用了构造方法注入的&#xff0c;原理就是&#xff1a;拿到yml文件中配置的线程池构造参数&#xff0c;我们一般构造一个线程池都需要&#xff1a;

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue)

在yml文件中我们也配置了这些构造参数。我们还配置线程池的名称&#xff0c;然后利用BeanDefinitionBuilder填充ConstructorArgumentValues对象和MutablePropertyValues&#xff1b;最后还是要调用BeanDefinitionRegistry类的registerBeanDefinition方法注册Bean。具体的可以看下这个开源项目。至此这个扩展点也介绍完了。欢迎mark&#xff01;


推荐阅读
  • 本文详细介绍了GetModuleFileName函数的用法,该函数可以用于获取当前模块所在的路径,方便进行文件操作和读取配置信息。文章通过示例代码和详细的解释,帮助读者理解和使用该函数。同时,还提供了相关的API函数声明和说明。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
author-avatar
于昕会_445
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有