51-How to use CompletableFuture to realize the problem of "travel platform"?

Tourism platform issues

What is the travel platform problem? If you want to build a travel platform, there is often such a demand, that is, users want to obtain flight information of multiple airlines at the same time. For example, how much is the air ticket from Beijing to Shanghai? There are many airlines that have such flight information, so all airlines' flight, fare and other information should be obtained and then aggregated. Since each airline has its own server, it is enough to request their servers separately, such as requesting Air China, Hainan Airlines, Eastern Airlines, etc., as shown in the following figure:
Insert picture description here
Serial
A more primitive way is to use serial To solve this problem.
Insert picture description here
For example, if we want to get the price, we must first visit Air China, which is called website 1 here, and then visit HNA website 2, and so on. After each request is sent, we can request the next website after it responds. This is the serial method.

This is very inefficient. For example, there are more airlines. Assuming that each airline takes 1 second, then users must not be able to wait, so this method is not advisable.

parallel

Next, we will improve the idea just now. The main idea is to change the serial to parallel, as shown in the following figure:
Insert picture description here
we can obtain the ticket information in parallel, and then aggregate the ticket information. In this case, Efficiency will be doubled.

Although this kind of parallelism improves efficiency, it also has a disadvantage, that is, it "waits until all requests are returned." If there is a website that is particularly slow, then you should not be dragged down by that website. For example, it takes twenty seconds to open a certain website. It must not wait that long. So we need a feature, that is, timeout access.

Parallel acquisition with timeout
Let's take a look at the following parallel acquisition with timeout.
Insert picture description here
In this case, it belongs to the parallel acquisition with timeout, and it is also requesting the information of each website in parallel. But we have set a timeout, for example, 3 seconds, then if all have returned by 3 seconds, of course it is best to collect them; but if there are still some websites that fail to return in time, we will Ignore these requests so that the user experience is better. It only needs to wait a fixed 3 seconds to get the information at most. Although the information may not be the most complete, it is better than waiting. .

There are several ways to achieve this goal, let's take a look at them one by one.

Implementation of thread pool

The first solution is to use thread pool, let's take a look at the code.

public class ThreadPoolDemo {
    
    
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    public static void main(String[] args) throws InterruptedException {
    
    
        ThreadPoolDemo threadPoolDemo = new ThreadPoolDemo();
        System.out.println(threadPoolDemo.getPrices());
    }
    private Set<Integer> getPrices() throws InterruptedException {
    
    
        Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
        threadPool.submit(new Task(123, prices));
        threadPool.submit(new Task(456, prices));
        threadPool.submit(new Task(789, prices));
        Thread.sleep(3000);
        return prices;
    }
    private class Task implements Runnable {
    
    
        Integer productId;
        Set<Integer> prices;
        public Task(Integer productId, Set<Integer> prices) {
    
    
            this.productId = productId;
            this.prices = prices;
        }
        @Override
        public void run() {
    
    
            int price=0;
            try {
    
    
                Thread.sleep((long) (Math.random() * 4000));
                price= (int) (Math.random() * 4000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            prices.add(price);
        }
    }
}

In the code, a thread-safe Set is created, which is used to store various price information, named it Prices, and then put tasks in the thread pool. The thread pool is created at the very beginning of the class and is a thread pool with fixed 3 threads. And this task is described in the Task class below. In this Task, we see the run method. In this method, we use a random time to simulate the response time of various aviation websites, and then return a random To indicate the fare, and finally put this fare in Set. This is what our run method does.

Going back to the getPrices function, we created three new tasks, the productId is 123, 456, 789, the productId here is not important, because the price we return is random, in order to achieve the function of overtime waiting, we call it here The sleep method of Thread sleeps for 3 seconds. If it does so, it will wait here for 3 seconds, and then return directly to prices.

At this point, if the previous response speed is fast, there will be up to three values ​​in prices, but if each response time is very slow, then there may be no value in prices. No matter how many you have, it will return prices directly after the sleep ends, that is, after executing the sleep of Thread, and finally print the result in the main function.

Let’s take a look at the possible execution results. One possibility is that there are 3 values, namely [3815, 3609, 3819] (the numbers are random); there may be 1 [3496] or 2 [1701, 2730 ], if each response speed is particularly slow, there may not be a value.

This is the most basic solution implemented with thread pools.

CountDownLatch

There will be room for optimization. For example, when the network is particularly good, each airline responds very quickly. You don’t need to wait three seconds at all. Some airlines may return within a few hundred milliseconds, so we don’t The user should be asked to wait 3 seconds. So you need to make such an improvement, look at the following code:

public class CountDownLatchDemo {
    
    
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
        System.out.println(countDownLatchDemo.getPrices());
    }
    private Set<Integer> getPrices() throws InterruptedException {
    
    
        Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
        CountDownLatch countDownLatch = new CountDownLatch(3);
        threadPool.submit(new Task(123, prices, countDownLatch));
        threadPool.submit(new Task(456, prices, countDownLatch));
        threadPool.submit(new Task(789, prices, countDownLatch));
        countDownLatch.await(3, TimeUnit.SECONDS);
        return prices;
    }
    private class Task implements Runnable {
    
    
        Integer productId;
        Set<Integer> prices;
        CountDownLatch countDownLatch;
        public Task(Integer productId, Set<Integer> prices,
                CountDownLatch countDownLatch) {
    
    
            this.productId = productId;
            this.prices = prices;
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
    
    
            int price = 0;
            try {
    
    
                Thread.sleep((long) (Math.random() * 4000));
                price = (int) (Math.random() * 4000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            prices.add(price);
            countDownLatch.countDown();
        }
    }
}

This code uses CountDownLatch to achieve this function. The overall idea is the same as before. The difference is that we have added a CountDownLatch and passed it to the Task. In Task, after obtaining the ticket information and adding it to the Set, the countDown method is called, which is equivalent to decrementing the count by 1.

In this way, when the countDownLatch.await(3,
TimeUnit.SECONDS) function is executed to wait, if the three tasks are executed very quickly, then the three threads have executed the countDown method, then the await method is Will return immediately, no need to wait for 3 seconds.

If a request is particularly slow, it is equivalent to a thread that does not execute the countDown method, and it is too late to execute within 3 seconds, then the await method with a timeout parameter will also give up this wait in time after 3 seconds. , So the prices are returned. So in this way, we use CountDownLatch to achieve this requirement, that is to say we wait at most 3 seconds, but if all return within 3 seconds, we can also return quickly, not silly waiting, improved effectiveness.

CompletableFuture

Let's take a look at the usage of using CompletableFuture to achieve this function. The code is as follows:

public class CompletableFutureDemo {
    
    
    public static void main(String[] args)
            throws Exception {
    
    
        CompletableFutureDemo completableFutureDemo = new CompletableFutureDemo();
        System.out.println(completableFutureDemo.getPrices());
    }
    private Set<Integer> getPrices() {
    
    
        Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
        CompletableFuture<Void> task1 = CompletableFuture.runAsync(new Task(123, prices));
        CompletableFuture<Void> task2 = CompletableFuture.runAsync(new Task(456, prices));
        CompletableFuture<Void> task3 = CompletableFuture.runAsync(new Task(789, prices));
        CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2, task3);
        try {
    
    
            allTasks.get(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
    
    
        } catch (ExecutionException e) {
    
    
        } catch (TimeoutException e) {
    
    
        }
        return prices;
    }
    private class Task implements Runnable {
    
    
        Integer productId;
        Set<Integer> prices;
        public Task(Integer productId, Set<Integer> prices) {
    
    
            this.productId = productId;
            this.prices = prices;
        }
        @Override
        public void run() {
    
    
            int price = 0;
            try {
    
    
                Thread.sleep((long) (Math.random() * 4000));
                price = (int) (Math.random() * 4000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            prices.add(price);
        }
    }
}

Here we no longer use the thread pool, we see the getPrices method, in this method, we use the runAsync method of CompletableFuture, this method will perform tasks asynchronously.

We have three tasks, and after executing this code, they will return a CompletableFuture object respectively. We name them task 1, task 2, task 3, then execute the allOf method of CompletableFuture, and pass task 1, task 2, task 3 Into. The function of this method is to aggregate multiple tasks, and then you can get the return results of these tasks passed in parameters as needed, or wait for them to be executed. We call this return value allTasks, and call its get method with timeout below, and pass in a timeout parameter of 3 seconds at the same time.

In this way, the effect is that if these 3 tasks can be returned smoothly within 3 seconds, that is, if each of the three tasks included in this task is executed, the get method can be normal in time Return and execute it down, which is equivalent to executing to return prices. In the run method of the Task below, if the method is executed, it means the end of the task for CompletableFuture, and it uses this as a mark to judge whether the task has been executed. But if there is a task that fails to return within 3 seconds, then this get method with timeout parameters will throw a TimeoutException, and we will also catch it. In this way, it achieves the effect: it will try to wait for all tasks to be completed, but it will only wait at most 3 seconds, and in the meantime, if it is completed in time, it will return in time. So we use CompletableFuture, which can also solve the problem of the travel platform. Its running result is the same as before, there are many possibilities.

Finally, make a summary. In this time, we first gave a travel platform problem, it needs to obtain the ticket information of each airline, and then the code evolution, from serial to parallel, then to parallel with timeout, and finally to not only timeout Parallel, and if everyone is fast, then there is no need to wait until the timeout expires. We have carried out such a step-by-step iteration.

Of course, in addition to these several implementation schemes, there will be other implementation schemes. Which ones can you think of? Feel free to leave a comment below and let me know, thank you.

Guess you like

Origin blog.csdn.net/Rinvay_Cui/article/details/111056598