How to Merge Concurrent Requests in Spring Boot and Save Database Connections
This article explains how to batch multiple user‑info requests on the server side, merge them into a single SQL query using a blocking queue and ScheduledThreadPoolExecutor, and return the results individually, thereby reducing database connection usage and improving performance under high concurrency.
Introduction
What is the benefit of merging requests? When three users (IDs 1, 2, 3) each query their basic information, the server would normally issue three separate database calls, wasting valuable connection resources.
By merging the requests, only one SQL query is sent to the database, and the results are grouped and returned to each user based on a unique request ID.
Replacing the database with a remote service follows the same principle.
Technical Approach
LinkedBlockingQueue– a blocking queue ScheduledThreadPoolExecutor – scheduled thread pool CompletableFuture – future without built‑in timeout (later replaced by a queue)
Code Implementation
Query Service Interface
public interface UserService {
Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs);
}Service Implementation
@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
@Service
public class UserWrapBatchService {
@Resource
private UserService userService;
public static int MAX_TASK_NUM = 100;
public static 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 executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
int size = queue.size();
if (size == 0) return;
List<Request> list = new ArrayList<>();
System.out.println("Merged [" + size + "] requests");
for (int i = 0; i < size; i++) {
if (i < MAX_TASK_NUM) list.add(queue.poll());
}
List<Request> userReqs = new ArrayList<>();
for (Request r : list) userReqs.add(r);
Map<String, Users> response = userService.queryUserByIdBatch(userReqs);
for (Request r : list) {
Users result = response.get(r.requestId);
r.completableFuture.complete(result);
}
}, 100, 10, TimeUnit.MILLISECONDS);
}
public Callable<Users> merge(Long userId) {
return () -> {
Request req = new Request();
req.requestId = UUID.randomUUID().toString().replace("-", "");
req.userId = userId;
CompletableFuture<Users> future = new CompletableFuture<>();
req.completableFuture = future;
queue.offer(req);
return future.get();
};
}
}High‑Concurrency Test
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();
}
}
}Key Points to Note
Java 8 CompletableFuture lacks a timeout mechanism.
SQL length limits require batch size control (MAX_TASK_NUM).
Conclusion
Merging requests and processing them in batches can dramatically reduce connection usage for the target system (database or RPC service). The trade‑off is added latency before the actual logic runs, making this pattern unsuitable for low‑concurrency scenarios.
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.
