前言
在当前的系统中,我们可能会在一个事务中,执行多项操作,调用多个外部服务,查询数据或者更新数据,进行一系列逻辑处理之后返回给客户一个结果。
例如,以下是一个顾客下单的流程模拟:
1、获取基本产品信息(此处查询数据库)
2、获取每一个产品的价格(假设此处需要通过第三方服务平台进行实时定价,产品不同调用的平台亦不同,所有此处是挨个获取)
3、计算产品总价
4、获取用户余额(此处也是调取第三方服务,获取用户账户余额)
5、比对余额是否充足
6、如果余额充足则提示购买成功,并扣去花费,更新用户余额(此处也是调取第三方服务,更新用户账户余额)
7、返回是否购买成功
这七个步骤,如果顺序执行,那么耗时甚多。
仔细观察,我们可以发现,步骤1和步骤4是上下文无关的可以并发执行;步骤2获取各个产品的价格也是互不相关的,可以并发执行;之后步骤3、5、6、7则只能顺序执行。
Java提供了这样的并发查询,异步获取结果的工具,那就是Future和Callable。
Callable 是一个函数式接口:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future则是一个未来才会有结果的结果。采用future.get()方法会阻塞线程直到结果返回。
为了方便使用,我封装了一个简单的工具类:
package cn.hengyumo.dawn.utils;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
/**
* 并发工具类
*
* @author hengyumo
* @since 2021-07-13
*
*/
public class ConcurrentUtil {
/**
* 执行任务
*
* @param executorService ExecutorService
* @param callable 回调
* @param <T> 返回的结果集Future泛型
* @return Future泛型
*/
public static <T> Future<T> doJob(ExecutorService executorService, Callable<T> callable) {
return executorService.submit(callable);
}
/**
* 获取结果集,执行时会阻塞直到有结果,中间的异常不会被静默
*
* @param future Future
* @param <T> 返回的结果集泛型
* @return T
*/
public static <T> T futureGet(Future<T> future) {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
下面来对比一下顺序执行、并发执行的速度差异吧。
基础代码
/**
* 获取产品价格,模拟调用外部服务
*
* @param product 产品
* @return 产品价格
* @throws InterruptedException Exception
*/
public BigDecimal getProductPrice(Product product) throws InterruptedException {
// 模拟耗时查询——调用外部服务接口
Thread.sleep(500);
return new BigDecimal(product.getId() * 10 + 233);
}
/**
* 获取产品列表,模拟从数据库查询数据
*
* @return 产品列表
* @throws InterruptedException Exception
*/
public List<Product> getProducts() throws InterruptedException {
List<Product> productList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Product product = new Product();
product.setId((long) i);
product.setName("product" + i);
product.setCreateTime(new Date());
productList.add(product);
}
// 模拟耗时查询
Thread.sleep(1000);
return productList;
}
/**
* 获取用户的账户余额
*
* @return 账户余额
* @throws InterruptedException Exception
*/
public BigDecimal getUserAccount() throws InterruptedException {
// 模拟耗时查询 —— 调用外部服务获取当前用户余额
Thread.sleep(500);
return new BigDecimal(3000);
}
/**
* 更新用户的账户余额
*
* @param left 账户余额
* @throws InterruptedException Exception
*/
public boolean updateUserAccount(BigDecimal left) throws InterruptedException {
// 模拟耗时操作 —— 调用外部服务更新当前用户余额
Thread.sleep(500);
return true;
}
顺序执行
@Test
public void searchProduct() throws InterruptedException {
long timeStart = System.currentTimeMillis();
// 1、获取基本产品信息
List<Product> productList = getProducts();
// 2、获取每一个产品的价格
for (Product product : productList) {
product.setPrice(getProductPrice(product));
}
// 3、计算产品总价
BigDecimal priceSum = productList.stream()
.map(Product::getPrice)
.reduce(new BigDecimal(0), BigDecimal::add);
System.out.println("产品总价:" + priceSum);
Assert.assertEquals("2780", priceSum.toString());
// 4、获取用户余额
BigDecimal userAccount = getUserAccount();
// 5、比对余额是否充足
String result;
if (userAccount.compareTo(priceSum) < 0) {
result = "余额不足,购买失败";
} else {
result = "余额充足,购买成功";
// 6、更新用户的余额
BigDecimal left = userAccount.subtract(priceSum);
System.out.println("用户购买后余额:" + left);
updateUserAccount(left);
}
System.out.println(result);
long timeUseMs = System.currentTimeMillis() - timeStart;
System.out.println("非并发查询耗时:" + timeUseMs);
}
顺序执行的结果:
产品总价:2780
用户购买后余额:220
余额充足,购买成功
非并发查询耗时:7175
并发执行
@Test
public void searchProductConcurrent() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(20);
long timeStart = System.currentTimeMillis();
// 1、获取基本产品信息
Future<List<Product>> productListFuture = ConcurrentUtil.doJob(executorService, this::getProducts);
// 2、获取用户余额
Future<BigDecimal> userAccountFuture = ConcurrentUtil.doJob(executorService, this::getUserAccount);
// 1、2并发的查询结果
List<Product> productList = ConcurrentUtil.futureGet(productListFuture);
BigDecimal userAccount = ConcurrentUtil.futureGet(userAccountFuture);
// 3、并发获取每一个产品的价格
List<Future<BigDecimal>> priceFutureList = new ArrayList<>();
for (Product product : productList) {
priceFutureList.add(ConcurrentUtil.doJob(executorService, () -> getProductPrice(product)));
}
for (int i = 0; i < productList.size(); i++) {
productList.get(i).setPrice(ConcurrentUtil.futureGet(priceFutureList.get(i)));
}
// 3、计算产品总价
BigDecimal priceSum = productList.stream()
.map(Product::getPrice)
.reduce(new BigDecimal(0), BigDecimal::add);
System.out.println("产品总价:" + priceSum);
Assert.assertEquals("2780", priceSum.toString());
// 5、比对余额是否充足
String result;
if (userAccount.compareTo(priceSum) < 0) {
result = "余额不足,购买失败";
} else {
result = "余额充足,购买成功";
// 6、更新用户的余额
BigDecimal left = userAccount.subtract(priceSum);
System.out.println("用户购买后余额:" + left);
updateUserAccount(left);
}
System.out.println(result);
long timeUseMs = System.currentTimeMillis() - timeStart;
System.out.println("并发查询耗时:" + timeUseMs);
}
产品总价:2780
用户购买后余额:220
余额充足,购买成功
并发查询耗时:2049
总结
顺序执行耗时7175ms ,并发执行耗时2049ms,这个时间在不同机器上运行应该会有些许差距。但总的来说,这个模拟流程的并发执行的速度应该至少是顺序执行的3倍左右(10个产品的并发价格查询节省了4500ms)。
并发查询的效率会受到机器配置下影响,核心线程多的机器可以理论上执行更多的并发任务。
并发查询可以更加合理的将本服务的压力分担给别的服务。而不是本服务积压大量服务,而其它服务又大量空闲,使得前端的请求得不到及时的响应。
并发查询除了跨服务的rpc调用之外也可以进行各种IO操作,如查询数据库、读取文件等。合理的使用并发进行查询可以使得系统的速度更上一层楼。
END