今天要给大家介绍一个新的来自Rumba Commons Mini的工具——ParallelExecutor,正如其名字所表达的,它提供并行计算执行器的功能。
让我们通过一个简单的例子来了解ParallelExecutor的工作原理。假设某家全国性的大型连锁经营企业,旗下有分布于全国的连锁门店100,000家,现在需要我们统计今年所有门店总销售额。问题在于一方面由于某种原因我们必须直接从原始销售数据中进行统计;另一方面由于数据量巨大,原始的销售数据被存放于100台数据库服务器上。
现在需要查询所有门店当天的销售总额。算法十分简单,分为两步:第一步先在每个数据库服务器上查询当天的销售总额;第二步再将前一步计算的结果全部相加,最后得到查询结果。然而问题在于其中的第一步,是如果采用传统的串行算法逐一访问每个数据库服务器进行查询,其性能可想而知。由于对每一个数据库服务器的查询操作所占用的资源都是独立的,也就是说可以通过让多个数据库并行地执行查询运算,这样就可以极大的提高最终的查询性能。
为实现对第一步运算的并行调用,首先需要将这段处理包装为实现java.util.concurrent.Callable接口的类,如下。
public class QuerySaleAmountTask implements Callable<BigDecimal> {
private final Connection dbConn;
/** @param dbConn 数据库连接。 */
public QuerySaleAmountTask(Connection dbConn) {
this.dbConn = dbConn;
}
@Override
public BigDecimal call() throws Exception {
// 此处省略代码:构造查询语句,访问指定的数据库dbConn,并返回计算结果。
}
}
通过构造函数传入参数“dbConn”指定数据库连接。call()方法的返回值为指定数据库的销售总额。
接下来需要一个QuerySaleAmountTask任务的生成器,目的是将输入的数据库连接列表转换为一系列QuerySaleAmountTask的实例:
public class QuerySaleAmountTaskProducer implements Iterator<QuerySaleAmountTask> {
private final List<Connection> dbConns;
private int nextIndex = 0;
/** @param dbConns 包含数据库连接的列表。 */
public QuerySaleAmountTaskProducer(List<Connection> dbConns) {
this.dbConns = dbConns;
}
@Override
public boolean hasNext() { return nextIndex < dbConns.size(); }
@Override
public QuerySaleAmountTask next() {
return dbConns.get(nextIndex++);
}
@Override
public void remove() {
throw new UnsupportedOperationException("Never call me.");
}
}
至此我们已经完成了第一步计算的代码,下一步就是实现第二步计算,即将来自每个数据库的当天销售总额进行求和运算。也需要将其包装为实现了com.hd123.rumba.commons.concurrent.Collector接口的类:
public class SaleAmountCollector implements Collector<QuerySaleAmountTask, BigDecimal> {
private BigDecimal sumOfSaleAmount = BigDecimal.ZERO;
@Override
public void collect(QuerySaleAmountTask task, BigDecimal saleAmount) {
sumOfSaleAmount = sumOfSaleAmount.add(saleAmount);
}
public BigDecimal getSumOfSaleAmount() { return sumOfSaleAmount; }
}
做完这些准备工作后,接下来就来看看最终的调用代码:
public BigDecimal querySumOfSaleAmount(List<Connection> dbConns) {
ParallelExecutor executor = ParallelExecutorBuilder.newBuilder()
.setThreadCount(10) // 最多同时启动10个并发线程。
.build();
QuerySaleAmountTaskProducer taskProducer = new QuerySaleAmountTaskProducer(dbConns);
SaleAmountCollector collector = new SaleAmountCollector();
executor.execute(taskProducer, collector);
return collector.getSumOfSaleAmount();
}
出于尽量减少代码篇幅的考虑,以上所有代码都没有包含传入参数非法检查,以及过程中的异常处理。ParalledExecutor也提供了一系列与异常处理有关的功能。其中包括execute()方法可以加上第三个参数,用于收集处理过程中的异常。更多信息请参见API文档。
纵观以上过程,首先我们将整个计算过程分为前后两个步骤,其中第一步的计算是可以并行化的,第二步则是将第一步的计算结果进行归集,最后得到计算结果。其实大多数并发处理都可以被分解为这样两步,对于一些更加复杂的并发处理可以分解为多个这样的两步。到这里有些读者可能已经在脑海中出现“Map-Reduce”这个词了,没错,现在十分流行的“Map-Reduce”正是基于相同的原理。而ParallelExecutor这个工具的最大特点是其足够“轻量”(总共只有3个类,且不依赖其它中间件 ),以致于可以被放到Rumba Commons Mini这个包中。