Jboss EnhancedQueueExecutor源码解读

在JDK线程池中自带的Executor遵循一种典型的生产者,消费者队列模型,即一个统一的阻塞队列,然后一个线程数组不停地消费其中的数据。其本身的处理逻辑为 coreSize->queueSize->maxSize 的增长方式,即先尝试增加 coreSize, 然后再不断地将任务放进队列中,如果队列满了,则再尝试增加 maxSize, 直至拒绝任务。

通过一些手法可以调整策略为 coreSize->maxSize->queueSize。

本文则描述一个由 jboss-threads 中提到的 EnhancedQueueExecutor,中文为增加型队列执行器。其除支持典型的executor模型外,也同样保留如 coreSize,maxSize, queueSize 这些模型。与jdk中实现相区别的是,其本身采用单个链表来完成任务的提交和线程的执行,同时采用额外的数据来存储计数类数据. 更重要的是,其默认线程策略即 coreSize->maxSize->queueSize, 同时可以根据参数调整此策略.

创建对象与ThreadPoolExecutor类似,指定相应的参数即可,如下所示:

EnhancedQueueExecutor executor = new EnhancedQueueExecutor.Builder()
        .setCorePoolSize(corePoolSize)
        .setMaximumPoolSize(maxPoolSize)
        .setKeepAliveTime(Duration.ofMinutes(5))
        .setMaximumQueueSize(1024)
        .setThreadFactory(threadFactory)
        .setExceptionHandler(uncaughtExceptionHandler)
        .setRegisterMBean(false)
        .setGrowthResistance(growthResistance) //增长因子,控制新线程创建逻辑(if >= coreSize时)
        .build();
继续阅读“Jboss EnhancedQueueExecutor源码解读”

使用FeignClient进行服务调用时的各类错误列表及快速定位

基于spring cloud 体系的微服务,服务间调用默认使用 openFeign 进行调用,但是由于源码本身的问题。针对于在调用时出现的各类信息,并没有分门别类的错误描述。当业务需要针对不同的错误进行处理时,就不能简单的通过FeignException来进行区分。笔者通过整理服务间调用时的各类异常堆栈及列表。分别列出相应的错误列表及触发时堆栈,以方便后续分别进行业务判断和处理

本应用基于ribbon进行服务查找,使用spring-cloud open-feign, 没有使用retry机制, 底层使用feign-okhttp, 因此,如果有异常与下列堆栈不一致时,请检查是否一致. 

版本: openFeign: 9.7.0, netflix ribbon: 2.2.5,  okhttp3: 3.8.1

总共的异常列表如下所示

  • 无服务时异常
  • 有服务但连接失败
  • 调用中网络错误读取数据超时
  • 响应4xx错误码
  • 响应5xx错误码
  • 解析结果错误(json反序列化失败)
  • 解析结果错误(无converter)

所有的异常触发类均由类 SynchronousMethodHandler 触发,但作用行不一样.
以下异常栈从原始cause,依次往下。如 cause1为 root cause, 最下层为业务层接收到的异常

继续阅读“使用FeignClient进行服务调用时的各类错误列表及快速定位”

在基于spring体系的业务中正确地关闭线程池

在业务代码中,特别是基于spring体系的代码中,均会使用线程池进行一些操作,比如异步处理消息,定时任务,以及一些需要与当前业务分离开的操作等。常规情况下,使用spring体系的TaskExecutor或者是自己定义ExecutorService,均可以正常地完成相应的操作。不论是定义一个spring bean,或者是使用 static Thread工具类均是能满足条件。

但是,如果需要正常地关闭spring容器时,这些线程池就不一定能够按照预期地关闭了。结果就是,当使用代码 context.close() 时,期望进程会正常地退出,但实际上进程并不会退出掉.原因就在于这些线程池中还在运行的线程。
本文描述了在基于spring boot的项目中,如何正确地配置线程池,以保证线程池能够正确的在整个spring容器周期内运行,并且在容器正常关闭时能够一并退出掉.

  • 定义bean时添加destroyMethod方法或相应生命周期方法
  • 设置线程池中线程为daemon
  • 为每个线程池正确地命名及使用ThreadFactory
  • 丢弃不再需要的周期性任务
  • 监听ContextClosedEvent,触发额外操作
继续阅读“在基于spring体系的业务中正确地关闭线程池”

spring cloud consul微服务长轮询更新

在使用spring cloud体系进行微服务开发时,一般的服务注册和发现除了 eureka 之外,还可以采用consul, zookeeper来实现。本文描述使用组件 spring-cloud-consul 时,如果保证服务变更时的实时发现和处理.

一般来说,采用zookeeper这种服务端通过连接挂在临时节点,然后消费端通过监听节点来感知服务的变更,这种实现方式对于服务端的上下线感知是最快的。因为通过socket来维系服务的健康度,当服务不可用时,socket立马即会断开.(本文不考虑服务假死,socket仍可用的情况),而监听方马上通过变更来更新server list. 当下一个请求处理时,即会正确选择合适的服务方进行处理。而且当服务方上线时,也会立马进行感知加入到服务列表中。

对于consul来实现服务注册,有很多种方式,这里我们采用 TTL check,即设置 spring.cloud.consul.discovery.heartbeat.enabled=true的情况,在这种情况下 其 默认ttl值为30,默认汇报时间为18左右.

对于消费者,即discover方,目前spring cloud consul中,采用标准的 ribbon-loadbalancer 中的 ServerList.getUpdatedListOfServers 来完成实例的更新。相应的定时任务通过 ServerListUpdater.start (默认实现为 PollingServerListUpdater) 来开启,内部采用 scheduleWithFixedDelay(默认30秒周期) 来实现.

在 consul 层,实现方为 ConsulServerList#getServers,即通过调用ConsulClient#getHealthServices 来获取健康的服务列表,对应consul的api即 /health/service/:service 来完成.

在这种情况下,存在一个间隔期,即30秒周期。如果服务方变更了,discover并不能实时感知,还需要等待下一次轮询才能感知到,对于微服务来说,这即是一个服务不可用的场景.

在官方 consul 的 API中,是存在 阻塞式调用的,即响应并不会立即返回,直到有变化或wait时间到。即长轮询概念,可以采取类似长轮询的概念来达到数据第一时间感知。目前世面大部分的配置中心同样采取类似的技术。

继续阅读“spring cloud consul微服务长轮询更新”

Spring Boot中多个application配置文件在属性覆盖时List和Map的不同处理行为

在spring boot 项目中,经常会使用profile来控制不同环境的属性设置及覆盖行为,即通过高优先级的配置文件来覆盖默认的属性,以达到测试/生产环境配置生效的目的. 常规的字符串,bean属性在覆盖时均会通过标准的merge覆盖过程,达到优先查找高优先级的配置文件,如果存在,则使用此值,并且不再继续继续处理.

但如果一个属性为List或Map属性,更或者List,Map中的值对象更是一个复杂类型时,其处理过程就比较麻烦。详见

https://github.com/spring-projects/spring-boot/issues/4313
https://github.com/spring-projects/spring-boot/issues/9137

为处理此问题,最终spring boot标准化了针对List或Map类型的处理规则。详见:

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-complex-type-merge

本文为翻译原文配置中的信息

继续阅读“Spring Boot中多个application配置文件在属性覆盖时List和Map的不同处理行为”

spring data 运行时添加JPA Repository

在特定的业务场景中,需要提供一个类似自定义实体的动态对象,并根据此对象生成相应的CRUD Repository。在这种场景中,与正常的domain对象不同,这个对象是在运行时,才定义出来,并产生相应的domain class和相应的repository class类。在业务体系中,需要将在运行时定义的对象重新加载至运行时中,并与普通的domain一样的操作。

如 普通的 AbcRepository ,可以使用如下的代码进行操作

@Autowired
private AbcRepository abcRepository;

abcRepository.findAll()

而新产生的repository,则可以通过如下的手法进行操作

val repository = (CrudRepository)applicationContext.getBean("newRepository");
repository.findAll();

本文即描述,通过 spring cloud 中自带的 RepositoryConfigurationDelegate, 使用新的自定义ClassLoader, 如何在运行时注册一个新的repository.

继续阅读“spring data 运行时添加JPA Repository”