如何为Java多线程应用程序优化数据存储库

68次阅读
没有评论

共计 4107 个字符,预计需要花费 11 分钟才能阅读完成。

本篇文章为大家展示了如何为 Java 多线程应用程序优化数据存储库,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

数据存储库通常是超高要求系统的瓶颈。在这些系统中,正在执行的查询数量非常大。DelayedBatchExecutor 是一个用于减少所需查询数量的组件,通过在 Java 多线程应用程序中对所需查询进行批处理。

1 个参数的 n 个查询 Vs. n 个参数的 1 个查询

假设有一个对关系数据库执行查询的 Java 应用程序,以便在给定其唯一标识符 (id) 的情况下检索 Product 实体(row)。

查询如下所示:

SELECT * FROM PRODUCT WHERE ID =

现在,检索 n 个 Products,有如下两种方法:

执行 1 个参数的 n 个独立查询:

SELECT * FROM PRODUCT WHERE ID = SELECT * FROM PRODUCT WHERE ID = ... SELECT * FROM PRODUCT WHERE ID =

使用 IN 运算符或 ORs 的串联,对 n 个参数执行 1 个查询以便同时检索 n 个 Products

-- Example using IN OPERATOR SELECT * FROM PRODUCT WHERE ID IN (, , ..., )

后者在网络流量和数据库服务器资源 (CPU 和磁盘) 方面更为有效,因为:

往返数据库的次数为 1,而不是 n。

数据库引擎优化了 n 个参数的数据遍历过程,即每个表格可能只需要扫描 1 次,而不是 n 次。

这不仅适用于 SELECT 操作,而且适用于其他操作,例如 INSERTs,UPDATEs 和 DELETEs。实际上,JDBC  API 包括上述操作的批量处理操作。

同样的情况也适用于 NoSQL 存储库,其中大多都明确提供 BULK 操作。

DelayedBatchExecutor

需要从数据库中检索数据的 Java 应用程序,如 REST 微服务或异步消息处理器,通常以多线程应用程序 (*1) 实现,其中:

每个线程在其执行的某个时刻执行相同的查询(每个查询具有不同的参数)。

并发线程数很高(每秒数十或数百)。

在这种场景下,数据库很可能在较短的时间间隔内多次执行相同的查询。

如前所述,如果将 1 个参数的 n 个查询替换为具有 n 个参数的单个等效查询,那么则应用程序将使用较少的数据库服务器和网络资源。

好消息是它可以通过 timewindows(时间窗口)的机制来实现,如下所示:

第一个尝试执行查询的线程会打开一个时间窗口,因此其参数被存储在一个列表中,同时该线程被暂停。在时间窗口内执行相同查询的其余线程会将其参数添加到列表中,并且也会被暂停。此时,数据库上未执行任何查询。

时间窗口结束或列表已满 (先前已定义最大容量限制) 后,将使用列表中存储的所有参数执行单个查询。最后,一旦数据库提供了该查询的结果,每个线程将接收相应的结果,同时所有线程将自动恢复。

笔者构建了一个简单而轻量级的应用机制(DelayedBatchExecutor),很容易在新的或现有的应用程序中使用。它基于 Reactor 库,并且为参数列表使用超时的 Flux 缓冲发布器。

运用 DelayedBatchExecutor 的吞吐量和延迟分析

假设针对 Products 的 REST 微服务公开了一个端点,用于检索数据库中给定的  productId 的 Product 数据。在没有 DelayedBatchExecutor 的情况下,如果每秒对端点命中 200 次,则数据库每秒执行 200 个查询。如果端点使用的 DelayedBatchExecutor   配置了 50 毫秒的时间窗口且最大容量 =10 个参数,数据库每秒钟将只执行 10 个参数的 20 个查询,代价是每执行一个线程,最多在 50 毫秒内增加延时(*2)。

换句话说,为了将延时增加 50 毫秒(* 2),数据库每秒接收的查询减少了 10 倍,然而保持了系统的整体吞吐量。还不错!!

其他有趣的配置:

窗口时间 = 100 毫秒,最大容量 = 20 个参数 rarr;20 个参数的 10 个查询(查询减少 20 倍)

窗口时间 = 500 毫秒,最大容量 = 100 个参数 rarr;2 个查询 100 个参数(查询减少 100 倍)

执行中的 DelayedBatchExecutor

深入研究 Product 微服务示例。假设对于每个传入的 HTTP 请求,微服务的控制器都要求检索已有 id 的 Product(Java  Bean),因此将调用以下方法:

DAO 组件 (ProductDAO) 的 public Product getProductById(IntegerproductId) .

以下分别是有和没有 DelayedBatchExecutor 的 DAO 执行。

没有 DelayedBatchExecutor

public classProductDAO { public Product getProductById(Integer id) { Product product= ...// execute the query SELECT * FROM PRODUCT WHERE ID= // using your favourite API: JDBC, JPA, Hibernate... return product; } ... }

有 DelayedBatchExecutor

// Singleton publicclass ProductDAO { DelayedBatchExecutor2 delayedBatchExecutorProductById = DelayedBatchExecutor.define(Duration.ofMillis(50), 10, this::retrieveProductsByIds); public Product getProductById(Integer id) { Product product = delayedBatchExecutorProductById.execute(id); return product; } private List retrieveProductsByIds(List idList) { List productList = ...// execute query:SELECT * FROM PRODUCT WHERE ID IN (idList.get(0), ..., idList.get(n)); // using your favourite API: JDBC, JPA, Hibernate... // The positions of the elements of the list to return must match the ones in the parameters list. // For instance, the first Product of the list to be returned must be the one with // the Id in the first position of productIdsList and so on... // NOTE: null could be used as value, meaning that no Product exist for the given productId return productList; } ... }

首先,必须在 DAO 中创建一个 DelayedBatchExecutor 实例,在本例中为  delayedBatchExecutorProductById。需要以下三个参数:

时间窗口(在此示例中为 50 毫秒)

参数列表的最大容量(在此示例中为 10 个参数)

将使用参数列表调用的方法(详细信息见后文)。在此示例中,方法为 retrieveProductsByIds

其次,已经重构了 DAO 方法 publicProduct getProductById(Integer  productId),以简单调用 delayedBatchExecutorProductById 实例的 execute 方法。所有的“magic”都是由  DelayedBatchExecutor 完成的。

之所以 delayedBatchExecutorProductById 是 DelayedBatchExecutor2

如果 execute 方法需要接收两个参数 (例如,一个 Integer 和一个 String) 并返回 Product 实例,则定义为  DelayedBatchExecutor3

最终,retrieveProductsByIds 方法必须返回 List

并接收 List 作为参数。

如果使用的是 DelayedBatchExecutor3

就是这样。

一旦运行,执行控制器逻辑的并发线程会在某时刻调用方法 getProductById(Integerid)  ,并且此方法将返回对应的 Product。并发线程不知自己已经被 DelayedBatchExecutor 暂停并恢复了。

由数据存储库延伸的“题外话”

尽管本文与数据存储库有关,但  DelayedBatchExecutor 也可以用在其他地方,例如:对 REST 进行微服务请求。再说,用 1 个参数启动 n 个 GET 请求要比使用 n 个参数启动 1 个 GET 昂贵得多。

DelayedBatchExecutor 的优化

笔者创建了  DelayedBatchExecutor 并使用了一段时间,有效地解决了个人项目中并发线程启动的多个查询的执行问题。因此相信它对其他人可能也有用处,所以决定将其公开。

话虽如此,DelayedBatchExecutor 改进和功能扩展的空间还很大。最有趣的是能够根据执行的特定条件动态更改 DelayedBatchExecutor 参数 (窗口时间和最大容量) 的功能,以便在利用带有 n 个参数的查询时很大程度地减少延时。

上述内容就是如何为 Java 多线程应用程序优化数据存储库,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-18发表,共计4107字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)