Today, while maintaining my blog system, I encountered a concurrency issue that caused multiple duplicate requests. Since this problem is very common, I'll document the various solutions here.
The blog's AI summary is lazy-loaded. When a request comes in for an article but finds that the summary and keywords don't exist, it sends an asynchronous request to the Spark large model to populate those two fields in the database. Large model requests typically take about 10 seconds. If the article page is refreshed continuously during those 10 seconds, it triggers repeated asynchronous requests. This is a classic duplicate request problem, which manifests differently across scenarios—like duplicate orders in e-commerce.
The essence of debouncing/duplicate request prevention is to use a flag to record that a request has already been sent, and skip subsequent requests when the flag is encountered. Across different scenarios, this flag is simply placed in memory, a database, Redis, or a message queue.
1. In-Memory Lock Approach
const processingMap = new Map<string, boolean>();
async function process(id: string) {
if (processingMap.get(id)) {
return;
}
processingMap.set(id, true);
try {
} finally {
processingMap.delete(id);
}
}
| Suitable Scenarios |
Advantages |
Disadvantages |
| Monolithic services |
Simple implementation |
State lost on service restart |
| Short-duration concurrent processing |
Best performance |
No distributed support |
| Memory-sensitive, high-performance requirements |
Minimal memory usage |
Cannot retrieve processing results |
2. Promise Cache Approach
const processingCache = new Map<string, Promise<any>>();
async function process(id: string) {
if (processingCache.has(id)) {
return processingCache.get(id);
}
const promise = (async () => {
try {
return result;
} finally {
processingCache.delete(id);
}
})();
processingCache.set(id, promise);
return promise;
}
| Suitable Scenarios |
Advantages |
Disadvantages |
| Monolithic services |
Can return processing results |
Relatively complex implementation |
| Need to return processing results |
Robust error handling |
Higher memory usage |
| Need error handling |
Good code maintainability |
No distributed support |
| Concurrent requests need to await results |
|
|
3. Database Lock Approach
async function process(id: string) {
const record = await prisma.task.findUnique({
where: { id }
});
if (record?.isProcessing) {
return;
}
await prisma.task.update({
where: { id },
data: { isProcessing: true }
});
try {
} finally {
await prisma.task.update({
where: { id },
data: { isProcessing: false }
});
}
}
| Suitable Scenarios |
Advantages |
Disadvantages |
| Distributed services |
Supports distributed systems |
Lower performance |
| Need to persist processing state |
State persistence |
Complex implementation |
| High data consistency requirements |
Good data consistency |
Requires additional database operations |
| Can tolerate performance overhead |
|
|
4. Redis Distributed Lock
async function process(id: string) {
const lockKey = `lock:${id}`;
const acquired = await redis.set(lockKey, '1', 'EX', 60, 'NX');
if (!acquired) {
return;
}
try {
} finally {
await redis.del(lockKey);
}
}
| Suitable Scenarios |
Advantages |
Disadvantages |
| Distributed services |
Good performance |
Requires Redis maintenance |
| High-performance requirements |
Supports distributed systems |
Complex implementation |
| Need timeout mechanism |
Built-in timeout mechanism |
Higher cost |
| High reliability requirements |
High reliability |
|
5. Message Queue Approach
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 1 });
async function process(id: string) {
queue.add(async () => {
});
}
| Suitable Scenarios |
Advantages |
Disadvantages |
| Need to control concurrency |
Controllable concurrency |
Queue lost on service restart |
| Tasks can be queued for processing |
Supports priority |
High memory usage |
| Need task priority |
Simple implementation |
No distributed support |
| Memory usage not a concern |
|
|
Selection Recommendations
| Scenario |
Recommended Approach |
Reason |
| Monolithic service, simple scenarios |
In-memory lock approach |
Simple and efficient, meets basic requirements |
| Monolithic service, need result return |
Promise cache approach |
Handles async results, robust error handling |
| Distributed service, high-performance requirements |
Redis distributed lock |
Good performance, distributed support, high reliability |
| Distributed service, strong consistency requirements |
Database lock approach |
Good data consistency, persistent state |
| Need to control concurrency count |
Message queue approach |
Precise concurrency control, supports task queuing |
Finally, in real-world applications, these approaches can also be combined, for example:
- Redis lock + Message queue
- Database lock + Promise cache
- In-memory lock + Debounce handling