查询速度起飞之Java利用多线程进行并发数据查询

收录于墨的2020~2021开发经验总结

前言

在当前的系统中,我们可能会在一个事务中,执行多项操作,调用多个外部服务,查询数据或者更新数据,进行一系列逻辑处理之后返回给客户一个结果。

例如,以下是一个顾客下单的流程模拟:

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

  • 18
    点赞
  • 90
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Java线程结合数据实现可以通过以下步骤实现: 1. 定义一个共享数据类,该类包含需要被多个线程访问和修改的数据。 2. 创建一个或多个线程,每个线程都可以访问和修改共享数据类的数据。 3. 在访问和修改共享数据类的数据时,需要使用同步机制来保证线程安全,例如使用synchronized关键字或Lock接口。 4. 在多个线程之间进行通信时,可以使用wait()、notify()、notifyAll()等方法来实现线程之间的协作。 举个例子,假设我们有一个共享数据类Counter,其中包含一个计数器count,我们需要多个线程对该计数器进行加1操作,可以按照以下步骤实现: ```java public class Counter { private int count; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } public class CounterThread implements Runnable { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } public void run() { for (int i = 0; i < 10000; i++) { counter.increment(); } } } public class Main { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread thread1 = new Thread(new CounterThread(counter)); Thread thread2 = new Thread(new CounterThread(counter)); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(counter.getCount()); } } ``` 在上面的例子中,我们定义了一个Counter类来表示共享数据,其中increment()方法和getCount()方法都使用了synchronized关键字来保证线程安全。我们创建了两个CounterThread线程,并将它们传递给两个Thread对象,然后启动这两个线程并等待它们完成。最后,我们输出计数器的值,可以看到输出结果为20000,说明两个线程对计数器进行了正确的加1操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值