Merging Backend Requests in SpringBoot to Reduce Database Connections
This article explains how to merge multiple backend requests in a SpringBoot application using a blocking queue, ScheduledThreadPoolExecutor, and CompletableFuture to batch database queries, reduce connection overhead, handle high concurrency, and includes full Java code examples and performance testing.
The author, a senior architect, demonstrates the significance of request merging to save valuable database connection resources by consolidating multiple user queries into a single SQL statement.
Technical Means
LinkedBlockingQueue– a blocking queue for request aggregation. ScheduledThreadPoolExecutor – a scheduled thread pool that periodically processes the queued requests. CompletableFuture – used to return results asynchronously (note: Java 8 CompletableFuture lacks a timeout mechanism, which is later addressed).
Code Implementation
Query Service Interface
public interface UserService {
Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs);
}
@Service
public class UserServiceImpl implements UserService {
@Resource
private UsersMapper usersMapper;
@Override
public Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) {
List<Long> userIds = userReqs.stream().map(UserWrapBatchService.Request::getUserId).collect(Collectors.toList());
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id", userIds);
List<Users> users = usersMapper.selectList(queryWrapper);
Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId));
HashMap<String, Users> result = new HashMap<>();
userReqs.forEach(val -> {
List<Users> usersList = userGroup.get(val.getUserId());
if (!CollectionUtils.isEmpty(usersList)) {
result.put(val.getRequestId(), usersList.get(0));
} else {
result.put(val.getRequestId(), null);
}
});
return result;
}
}Batch Request Service
package com.springboot.sample.service.impl;
import com.springboot.sample.bean.Users;
import com.springboot.sample.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.*;
@Service
public class UserWrapBatchService {
@Resource
private UserService userService;
public static int MAX_TASK_NUM = 100;
public class Request {
String requestId;
Long userId;
CompletableFuture<Users> completableFuture;
// getters and setters omitted for brevity
}
private final Queue<Request> queue = new LinkedBlockingQueue();
@PostConstruct
public void init() {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
int size = queue.size();
if (size == 0) return;
List<Request> list = new ArrayList<>();
for (int i = 0; i < size && i < MAX_TASK_NUM; i++) {
list.add(queue.poll());
}
List<Request> userReqs = new ArrayList<>(list);
Map<String, Users> response = userService.queryUserByIdBatch(userReqs);
for (Request request : list) {
Users result = response.get(request.requestId);
request.completableFuture.complete(result);
}
}, 100, 10, TimeUnit.MILLISECONDS);
}
public Users queryUser(Long userId) {
Request request = new Request();
request.requestId = UUID.randomUUID().toString().replace("-", "");
request.userId = userId;
CompletableFuture<Users> future = new CompletableFuture<>();
request.completableFuture = future;
queue.offer(request);
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
}Controller Invocation
/*** Request merging */
@RequestMapping("/merge")
public Callable<Users> merge(Long userId) {
return () -> userBatchService.queryUser(userId);
}High‑Concurrency Test
package com.springboot.sample;
import org.springframework.web.client.RestTemplate;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class TestBatch {
private static int threadCount = 30;
private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(threadCount);
private static final RestTemplate restTemplate = new RestTemplate();
public static void main(String[] args) {
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
COUNT_DOWN_LATCH.countDown();
try { COUNT_DOWN_LATCH.await(); } catch (InterruptedException e) { e.printStackTrace(); }
for (int j = 1; j <= 3; j++) {
int param = new Random().nextInt(4);
if (param <= 0) param++;
String response = restTemplate.getForObject("http://localhost:8080/asyncAndMerge/merge?userId=" + param, String.class);
System.out.println(Thread.currentThread().getName() + " param " + param + " response " + response);
}
}).start();
}
}
}Conclusion
Request merging and batch processing can dramatically reduce the number of database connections or remote‑service calls, improving resource utilization in high‑concurrency scenarios, though it adds a small waiting latency and may not suit low‑traffic workloads.
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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
