How to Merge Requests in Spring Boot to Reduce DB Load and Boost Performance
This article explains how to combine multiple user queries into a single database request using a queue, ScheduledThreadPoolExecutor, and CompletableFuture in Spring Boot, demonstrating code implementations, handling Java 8 CompletableFuture timeout limitations, and showing performance gains through request merging under high concurrency.
Preface
What is the benefit of merging requests? The diagram below shows three users (IDs 1, 2, 3) each requesting their basic information, which would normally trigger three separate database queries. Since database connections are a precious resource, we aim to minimize the number of connections.
By merging the three requests on the server side, we issue a single SQL query, then distribute the returned data to each user based on a unique request ID.
The same principle applies when the database is replaced by a remote service.
We change the approach as shown in the next diagram.
Technical Means
LinkedBlockingQueue– a blocking queue ScheduledThreadPoolExecutor – scheduled task thread pool CompletableFuture – asynchronous result handling (Java 8 lacks a timeout mechanism, so a queue is used as a workaround)
Code Implementation
Query User Service
public interface UserService {<br/> Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs);<br/>}<br/><br/>@Service<br/>public class UserServiceImpl implements UserService {<br/> @Resource<br/> private UsersMapper usersMapper;<br/><br/> @Override<br/> public Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) {<br/> List<Long> userIds = userReqs.stream().map(UserWrapBatchService.Request::getUserId).collect(Collectors.toList());<br/> QueryWrapper<Users> queryWrapper = new QueryWrapper<>();<br/> queryWrapper.in("id", userIds); // combine into one SQL<br/> List<Users> users = usersMapper.selectList(queryWrapper);<br/> Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId));<br/> HashMap<String, Users> result = new HashMap<>();<br/> userReqs.forEach(val -> {<br/> List<Users> usersList = userGroup.get(val.getUserId());<br/> if (!CollectionUtils.isEmpty(usersList)) {<br/> result.put(val.getRequestId(), usersList.get(0));<br/> } else {<br/> result.put(val.getRequestId(), null); // no data<br/> }<br/> });<br/> return result;<br/> }<br/>}Batch Request Merging Service
package com.springboot.sample.service.impl;<br/><br/>@Service<br/>public class UserWrapBatchService {<br/> @Resource<br/> private UserService userService;<br/><br/> public static int MAX_TASK_NUM = 100;<br/><br/> public static class Request {<br/> String requestId;<br/> Long userId;<br/> CompletableFuture<Users> completableFuture;<br/> // getters and setters omitted for brevity<br/> }<br/><br/> private final Queue<Request> queue = new LinkedBlockingQueue<>();<br/><br/> @PostConstruct<br/> public void init() {<br/> ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);<br/> scheduledExecutorService.scheduleAtFixedRate(() -> {<br/> int size = queue.size();<br/> if (size == 0) return;<br/> List<Request> list = new ArrayList<>();<br/> System.out.println("Merged [" + size + "] requests");<br/> for (int i = 0; i < size; i++) {<br/> if (i < MAX_TASK_NUM) {<br/> list.add(queue.poll());<br/> }<br/> }<br/> List<Request> userReqs = new ArrayList<>(list);<br/> Map<String, Users> response = userService.queryUserByIdBatch(userReqs);<br/> for (Request request : list) {<br/> Users result = response.get(request.requestId);<br/> request.completableFuture.complete(result);<br/> }<br/> }, 100, 10, TimeUnit.MILLISECONDS);<br/> }<br/><br/> public Users queryUser(Long userId) {<br/> Request request = new Request();<br/> request.requestId = UUID.randomUUID().toString().replace("-", "");<br/> request.userId = userId;<br/> CompletableFuture<Users> future = new CompletableFuture<>();<br/> request.completableFuture = future;<br/> queue.offer(request);<br/> try {<br/> return future.get();<br/> } catch (InterruptedException | ExecutionException e) {<br/> e.printStackTrace();<br/> }<br/> return null;<br/> }<br/>}Controller Call
@RequestMapping("/merge")<br/>public Callable<Users> merge(Long userId) {<br/> return () -> userBatchService.queryUser(userId);<br/>}High‑Concurrency Test
public class TestBatch {<br/> private static int threadCount = 30;<br/> private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(threadCount);<br/> private static final RestTemplate restTemplate = new RestTemplate();<br/><br/> public static void main(String[] args) {<br/> for (int i = 0; i < threadCount; i++) {<br/> new Thread(() -> {<br/> COUNT_DOWN_LATCH.countDown();<br/> try { COUNT_DOWN_LATCH.await(); } catch (InterruptedException e) { e.printStackTrace(); }<br/> for (int j = 1; j <= 3; j++) {<br/> int param = new Random().nextInt(4);<br/> if (param <= 0) param++;<br/> String response = restTemplate.getForObject("http://localhost:8080/asyncAndMerge/merge?userId=" + param, String.class);<br/> System.out.println(Thread.currentThread().getName() + " param " + param + " response " + response);<br/> }<br/> }).start();<br/> }<br/> }<br/>}Test Results
Things to Note
Java 8 CompletableFuture does not provide a timeout mechanism.
The underlying SQL statement has length limits, so batch size must be capped (MAX_TASK_NUM) to avoid exceeding the limit.
Problem Solving
Using a Queue to Add Timeout for CompletableFuture
public class UserWrapBatchQueueService {<br/> // similar structure as UserWrapBatchService but each Request holds a LinkedBlockingQueue<Users> for timeout<br/> // queue.poll(3000, TimeUnit.MILLISECONDS) provides a 3‑second wait before giving up<br/> // rest of the implementation follows the same merging logic<br/>}Conclusion
Request merging and batch processing can dramatically reduce connection resource consumption for the called system (e.g., a database). The trade‑off is added waiting time before actual logic execution, making it unsuitable for low‑concurrency scenarios.
Source Code
https://gitee.com/apple_1030907690/spring-boot-kubernetes/tree/v1.0.5
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
