数据存储库通常是要求非常苛刻的系统中的瓶颈。在这些系统中,正在执行的查询数量非常大。DelayedBatchExecutor是一个组件,用于在Java多线程应用程序中通过批处理所需查询来减少所需查询的数量。1查询n个参数Vs。1查询n个参数假设有一个Java应用程序对关系数据库执行查询以检索给定其唯一标识符(id)的Product实体(行)。查询如下所示:SELECT*FROMPRODUCTWHEREID=现在,要检索n个产品,有两种方法:使用1个参数执行n个独立查询:SELECT*FROMPRODUCTWHEREID=SELECT*FROMPRODUCTWHEREID=...SELECT*FROMPRODUCTWHEREID=使用IN运算符或OR的串联,对n个参数执行1个查询以同时检索n个产品——使用INOPERATORSELECT*FROMPRODUCTWHEREIDIN(,,...,)的示例,后者在网络流量和数据库服务器资源(CPU和磁盘)方面效率更高,因为:到数据库的往返次数是1而不是n。数据库引擎优化了n个参数的数据遍历过程,即每张表可能只需要扫描一次,而不是n次。这不仅适用于SELECT操作,也适用于其他操作,例如INSERT、UPDATE和DELETE。实际上,JDBCAPI包含了对上述操作的批处理操作。这同样适用于NoSQL存储库,其中大部分明确提供BULK操作。DelayedBatchExecutor需要从数据库中检索数据的Java应用程序,例如REST微服务或异步消息处理器,通常作为多线程应用程序(*1)实现,其中:每个线程在其执行过程中的某个时刻执行相同的查询(每个查询有不同的参数)。并发线程数很高(每秒数十或数百)。在这种情况下,数据库很可能在很短的时间间隔内多次执行同一个查询。如前所述,如果将1个参数的n个查询替换为具有n个参数的单个等效查询,则应用程序将使用更少的数据库服务器和网络资源。好消息是可以通过timewindows(时间窗口)的机制来实现,如下:第一个尝试执行查询的线程打开一个时间窗口,因此它的参数存储在一个列表中,而线程被挂起.在时间窗口内执行相同查询的其余线程将把它们的参数添加到列表中,并且也将被暂停。此时,没有对数据库执行任何查询。在时间窗口结束或列表已满(先前定义的最大容量限制)后,将使用存储在列表中的所有参数执行单个查询。最后,一旦数据库提供查询结果,每个线程都会收到相应的结果,同时所有线程都会自动恢复。作者构建了一个简单轻量级的应用程序机制(DelayedBatchExecutor),无论是在新的还是现有的应用程序中都易于使用。它基于Reactor库,并使用超时的Flux缓冲发布器作为参数列表。使用DelayedBatchExecutor进行吞吐量和延迟分析假设产品的REST微服务公开了一个端点,用于检索数据库中给定产品ID的产品数据。如果没有DelayedBatchExecutor,如果每秒有200次到端点的命中,则数据库每秒执行200次查询。如果端点使用的DelayedBatchExecutor配置了50毫秒的时间窗口和最大容量=10个参数,则数据库每秒只会执行20个10个参数的查询,代价是每个执行的线程都会增加延迟最多50毫秒(*2)。换句话说,将延迟增加50毫秒(*2),数据库每秒接收的查询减少10倍,但仍保持系统的整体吞吐量。不错!!其他有趣的配置:窗口时间=100毫秒,最大容量=20个参数→10个查询,20个参数(查询减少20倍)窗口时间=500毫秒,最大容量=100个参数→2个DelayedBatchExecutor在查询100个参数(100倍查询减少)执行深入了解产品微服务示例。假设对于每个传入的HTTP请求,微服务的控制器需要检索已经具有id的Product(JavaBean),因此将调用以下方法:DAO组件(ProductDAO)的publicProductgetProductById(IntegerproductId)。以下是使用和不使用DelayedBatchExecutor的DAO执行。没有DelayedBatchExecutorpublicclassProductDAO{publicProductgetProductById(Integerid){Productproduct=...//executethequerySELECT*FROMPRODUCTWHEREID=//usingyourfavouriteAPI:JDBC,JPA,Hibernate...returnproduct;}...}有DelayedBatchExecutor//SingletonpublicclassProductDAO{DelayedBatchExecutor2delayedProductExinedBatchExecuty(Duration.ofMillis(50),10,this::retrieveProductsByIds);publicProductgetProductById(Integerid){Productproduct=delayedBatchExecutorProductById.execute(id);returnproduct;}privateListretrieveProductsByIds(ListidList){ListproductList=...//执行查询:SELECT*FROMPRODUCTWHEREIDIN(idList.get(0),...,idList.get(n));//usingyourfavouriteAPI:JDBC,JPA,Hibernate...//返回列表的元素的位置必须与参数列表中的元素匹配。//例如,要返回的列表的第一个产品必须是具有//产品ID列表的第一个位置的那个……//注意:null可以用作值,意味着给定的产品不存在roductIdreturnproductList;}...}首先,必须在DAO中创建一个DelayedBatchExecutor的实例,此时delayedBatchExecutorProductById需要以下三个参数:时间窗口(本例中为50ms)参数列表的最大容量(本??例中示例)中的10个参数将与参数列表一起调用(有关详细信息,请参见下文)。在这个例子中,方法是retrieveProductsByIds其次,DAO方法publicProductgetProductById(IntegerproductId)已经被重构为简单地调用delayedBatchExecutorProductById实例的execute方法。所有的“魔法”都是由DelayedBatchExecutor完成的。delayedBatchExecutorProductById是DelayedBatchExecutor2的原因如果execute方法需要接收两个参数(比如一个Integer和一个String)并返回一个Product实例,则定义为DelayedBatchExecutor3最后,retrieveProductsByIds方法必须返回一个List,接收一个List作为范围。如果您使用的是DelayedBatchExecutor3,就会出现这种情况。一旦运行,执行控制器逻辑的并发线程将在某个时候调用方法getProductById(Integerid),该方法将返回相应的产品。并发线程并不知道自己已经被DelayedBatchExecutor挂起和恢复。数据仓库扩展的“题外话”虽然本文是关于数据仓库的,但DelayedBatchExecutor也可以用在其他地方,例如:向REST发出微服务请求。此外,以1个参数开始n个GET请求比以n个参数开始1个GET请求要昂贵得多。DelayedBatchExecutor的优化作者创建了DelayedBatchExecutor并使用了一段时间,有效解决了个人项目中并发线程启动多个查询的执行问题。相信它可能对其他人有用,我决定将其公开。话虽如此,DelayedBatchExecutor还有很大的改进和功能扩展空间。最有趣的是能够根据执行的特定条件动态更改DelayedBatchExecutor参数(窗口时间和最大容量),以便在使用带有n个参数的查询时最大限度地减少延迟。
