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
queryUserByIdBatch(List
userReqs);
}
@Service
public class UserServiceImpl implements UserService {
@Resource
private UsersMapper usersMapper;
@Override
public Map
queryUserByIdBatch(List
userReqs) {
List
userIds = userReqs.stream().map(UserWrapBatchService.Request::getUserId).collect(Collectors.toList());
QueryWrapper
queryWrapper = new QueryWrapper<>();
queryWrapper.in("id", userIds);
List
users = usersMapper.selectList(queryWrapper);
Map
> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId));
HashMap
result = new HashMap<>();
userReqs.forEach(val -> {
List
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
completableFuture;
// getters and setters omitted for brevity
}
private final Queue
queue = new LinkedBlockingQueue();
@PostConstruct
public void init() {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
int size = queue.size();
if (size == 0) return;
List
list = new ArrayList<>();
for (int i = 0; i < size && i < MAX_TASK_NUM; i++) {
list.add(queue.poll());
}
List
userReqs = new ArrayList<>(list);
Map
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
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
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.
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.