重试
重试背后的动机是通过立即重试失败的操作来消除暂时的失败。此重试对调用功能透明地发生。
使用MicroProfile Fault Tolerance实现技术动机重试很简单。@Retry如果发生异常,注释将导致方法调用最多重新执行三次。我们可以使用注释值进一步配置行为,例如延迟时间或异常类型。
与断路器类似,@Fallback如果在最大重试次数后调用仍然失败,也可以定义行为。
隔板
与船上的隔间类似,隔板旨在将软件功能划分为可单独失效的部分,而不会导致整个应用程序无响应。它们可以防止错误进一步级联,同时应用程序的其余部分保持正常运行。
在企业Java中,通过定义多个池(例如数据库连接池或线程池)来应用Bulkhead模式。对于多个线程池,如果另一个线程池当前用尽,我们可以确保应用程序的特定部分不受影响。
但是,企业Java应用程序不应该启动或管理自己的线程; 相反,他们必须使用平台功能来提供托管线程。为此,Java EE附带一个ManagedExecutorService提供容器管理线程的线程,通常基于单个线程池。
由Java EE专家Adam Bien提供的Porcupine库支持进一步定义可以单独配置的容器管理线程池。以下显示了两个专用ExecutorService的检索和创建工具的定义和用法:
|
@Dedicated(<span style=“color:#00bb00”>“instruments-read”</span><span style=“color:black”>)
ExecutorService readExecutor;
@Inject
@Dedicated(</span><span style=“color:#00bb00”>“instruments-write”</span><span style=“color:black”>)
ExecutorService writeExecutor;
</span><span style=“color:#0000aa”><em>// usage within method body …</em></span><span style=“color:black”>
CompletableFuture.supplyAsync(() -> instrumentCraftShop.getInstruments(), readExecutor);
</span><span style=“color:#0000aa”><em>// usage within method body …</em></span><span style=“color:black”>
CompletableFuture.runAsync(() -> instrumentCraftShop.craftInstrument(instrument), writeExecutor)
</span></span>
通常,这些执行程序服务可以在我们的整个应用程序中使用。但是,在使用HTTP资源时需要考虑另一种情况。
应用程序服务器通常使用单个线程池来处理传入请求的HTTP请求线程。当然,单个请求线程池使我们很难在我们的应用程序中构建多个隔板。
因此,我们可以使用异步JAX-RS资源来立即管理对专用执行程序服务的传入请求的处理:
|
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
<strong>public</strong> <strong>class</strong> InstrumentsResource {
@Inject
InstrumentCraftShop instrumentCraftShop;
@Inject
@Dedicated(</span><span style=“color:#00bb00”>“instruments-read”</span><span style=“color:black”>)
ExecutorService readExecutor;
@Inject
@Dedicated(</span><span style=“color:#00bb00”>“instruments-write”</span><span style=“color:black”>)
ExecutorService writeExecutor;
@GET
<strong>public</strong> CompletionStage<List<Instrument>> getInstruments() {
<strong>return</strong> CompletableFuture
.supplyAsync(() -> instrumentCraftShop.getInstruments(), readExecutor);
}
@POST
<strong>public</strong> CompletionStage<Response> createInstrument(@Valid @NotNull Instrument instrument) {
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 <strong>return</strong> CompletableFuture.runAsync(
() -> instrumentCraftShop.craftInstrument(instrument), writeExecutor)
.thenApply(c -> Response.noContent().build())
.exceptionally(e -> Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.header(</span><span style=“color:#00bb00”>“X-Error”</span><span style=“color:black”>, e.getMessage())
.build());
}
}
</span></span>
检索和创建工具的传入请求将传递到单独的线程池。从JAX-RS 2.1开始,返回一个CompletionStage或兼容的类型就足以将JAX-RS资源声明为异步。异步处理完成后,请求将被暂停并恢复。
如果分别用于检索或创建工具的这两个功能中的一个将被重载并且池中的线程用完,则另一个将不受此影响。共享请求线程池不太可能用完线程,因为请求的处理会立即传递给其他受管线程。
我们使用Porcupine库而不是MicroProfile Fault Tolerance的Bulkhead功能的原因是:前者使我们能够直接访问和控制我们与异步JAX-RS资源连接的托管执行程序服务。
背压
负载较重的应用程序可以通过向客户通知其当前状态来应用背压。这可以通过多种方式实现,例如,通过向响应添加元数据或者通过返回失败响应来更加彻底地添加元数据。
如果我们认为应用程序保持响应更为重要,尤其是它能够在其服务级别协议(SLA)内响应而不是延迟响应,那么我们将希望实现背压。关于满足整个系统的SLA,立即响应错误以使客户端可以调用不同的应用程序或实例,而不是消耗所有SLA时间并且仍然无法正常运行可能更有帮助处理请求。
为了指示我们的执行程序服务在所有线程都忙的情况下立即拒绝超过执行程序等待队列的调用,我们需要进一步配置行为。ExecutorConfigurator是Porcupine使用的托管bean,我们可以使用CDI专门使用它:
|
<strong>public</strong> <strong>class</strong> CustomExecutorConfigurator <strong>extends</strong> ExecutorConfigurator {
@Override
<strong>public</strong> ExecutorConfiguration forPipeline(String name) {
<strong>if</strong> (<span style=“color:#00bb00”>“instruments-read”</span><span style=“color:black”>.equals(name))
<strong>return</strong> <strong>new</strong> ExecutorConfiguration.Builder()
.abortPolicy()
.queueCapacity(4)
.build();
<strong>return</strong> <strong>new</strong> ExecutorConfiguration.Builder()
.abortPolicy()
.build();
}
}
</span></span>
覆盖方法forPipeline用于为限定名称构造执行程序服务。该abortPolicy调用将指示底层的线程池立即拒绝与超过资源的新的调用RejectedExecutionException。这是我们的目的所期望的行为。
为了通知客户端我们的应用程序不可用,我们将此异常映射到HTTP 503响应:
|
<strong>public</strong> <strong>class</strong> RejectedExecutionHandler implements ExceptionMapper<RejectedExecutionException> {
@Override