← Назад к вопросам
Возникнут ли проблемы при базовой реализации @RestController с Endpoint и счетчиком, при множестве одновременных запросов
2.0 Middle🔥 161 комментариев
#REST API и микросервисы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблемы базовой реализации @RestController со счётчиком при множественных запросах
Да, при базовой реализации счётчика в @RestController возникнут серьёзные проблемы потокобезопасности (Thread Safety). Это классический пример race condition.
Проблемная реализация
@RestController
@RequestMapping("/api")
public class CounterController {
// ПРОБЛЕМА: обычная переменная, доступная всем потокам
private int requestCount = 0;
@GetMapping("/endpoint")
public ResponseEntity<?> endpoint() {
// Race condition здесь!
requestCount++; // Это НЕ атомарная операция
return ResponseEntity.ok(new CounterResponse(requestCount));
}
@GetMapping("/count")
public ResponseEntity<?> getCount() {
return ResponseEntity.ok(new CounterResponse(requestCount));
}
}
class CounterResponse {
public int count;
public CounterResponse(int count) {
this.count = count;
}
}
Почему это проблема?
Операция requestCount++ состоит из трёх машинных операций:
// requestCount++ раскрывается в:
1. int temp = requestCount; // LOAD (прочитать значение)
2. temp = temp + 1; // ADD (увеличить)
3. requestCount = temp; // STORE (записать обратно)
Сценарий race condition:
Поток 1 Поток 2 Значение
====================================================================
LOAD (читает 0)
LOAD (читает 0)
ADD (0 + 1 = 1)
ADD (0 + 1 = 1)
STORE (пишет 1)
STORE (пишет 1)
Ожидаемое значение: 2
Актуальное значение: 1 ❌
Доказательство проблемы (тест)
@RestController
public class BadCounterController {
private int counter = 0;
@GetMapping("/increment")
public ResponseEntity<Integer> increment() {
counter++;
return ResponseEntity.ok(counter);
}
}
@SpringBootTest
public class CounterConcurrencyTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private BadCounterController controller;
@Test
public void testConcurrentRequests() throws InterruptedException {
final int THREAD_COUNT = 100;
final int REQUESTS_PER_THREAD = 100;
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
for (int j = 0; j < REQUESTS_PER_THREAD; j++) {
// Делаем 100 параллельных потоков × 100 запросов
// Ожидаем: counter = 10000
controller.increment();
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// Результат может быть: 9845, 9920, 9999 — НИКОГДА не 10000!
System.out.println("Actual count: " + controller.counter);
// Все попытки найти равно 10000 будут падать
}
}
Решение 1: AtomicInteger (простое)
@RestController
@RequestMapping("/api")
public class SafeCounterController1 {
// Атомарная переменная - потокобезопасна
private final AtomicInteger requestCount = new AtomicInteger(0);
@GetMapping("/endpoint")
public ResponseEntity<?> endpoint() {
// Атомарно увеличиваем на 1
int count = requestCount.incrementAndGet();
return ResponseEntity.ok(new CounterResponse(count));
}
@GetMapping("/count")
public ResponseEntity<?> getCount() {
return ResponseEntity.ok(new CounterResponse(requestCount.get()));
}
}
Почему это работает:
- AtomicInteger использует CAS (Compare-And-Swap) операции
- Гарантирует атомарность без блокировок
- Производительнее, чем synchronized
// AtomicInteger работает так (упрощённо):
public final int incrementAndGet() {
int current;
int next;
do {
current = this.value; // Читаем текущее
next = current + 1; // Вычисляем новое
} while (!compareAndSwap(current, next)); // Пытаемся записать
return next;
}
Решение 2: synchronized (классический)
@RestController
@RequestMapping("/api")
public class SafeCounterController2 {
private int requestCount = 0;
private final Object lock = new Object();
@GetMapping("/endpoint")
public ResponseEntity<?> endpoint() {
// Мьютекс блокирует доступ для других потоков
synchronized (lock) {
requestCount++;
}
return ResponseEntity.ok(new CounterResponse(requestCount));
}
@GetMapping("/count")
public ResponseEntity<?> getCount() {
synchronized (lock) {
return ResponseEntity.ok(new CounterResponse(requestCount));
}
}
}
Проблема: Блокировка может привести к bottleneck при высокой нагрузке.
Решение 3: ReentrantLock (гибкое)
@RestController
@RequestMapping("/api")
public class SafeCounterController3 {
private int requestCount = 0;
private final ReentrantLock lock = new ReentrantLock();
@GetMapping("/endpoint")
public ResponseEntity<?> endpoint() {
lock.lock();
try {
requestCount++;
return ResponseEntity.ok(new CounterResponse(requestCount));
} finally {
lock.unlock();
}
}
}
Решение 4: Database (рекомендуется для production)
@Entity
public class Counter {
@Id
private Long id = 1L;
private long count = 0;
}
@Repository
public interface CounterRepository extends JpaRepository<Counter, Long> {
}
@RestController
@RequestMapping("/api")
public class SafeCounterController4 {
@Autowired
private CounterRepository counterRepository;
@GetMapping("/endpoint")
@Transactional
public ResponseEntity<?> endpoint() {
Counter counter = counterRepository.findById(1L).orElse(new Counter());
// БД гарантирует consistency на уровне SQL
counter.setCount(counter.getCount() + 1);
counterRepository.save(counter);
return ResponseEntity.ok(counter);
}
}
// SQL уровне:
// UPDATE counter SET count = count + 1 WHERE id = 1; -- АТОМАРНО
Решение 5: Redis (для распределённых систем)
@RestController
@RequestMapping("/api")
public class SafeCounterController5 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/endpoint")
public ResponseEntity<?> endpoint() {
// Redis INCR операция атомарна на уровне клиента
Long count = redisTemplate.opsForValue().increment("request-count");
return ResponseEntity.ok(new CounterResponse(count.intValue()));
}
}
Сравнение решений
| Решение | Производительность | Простота | Для 1 инстанса | Для распределённой системы |
|---|---|---|---|---|
| AtomicInteger | Отлично | Простое | ✅ | ❌ |
| synchronized | Плохо | Простое | ✅ | ❌ |
| ReentrantLock | Хорошо | Средняя | ✅ | ❌ |
| Database | Хорошо | Средняя | ✅ | ✅ |
| Redis | Отлично | Средняя | ✅ | ✅ |
Рекомендуемое решение
@RestController
@RequestMapping("/api")
public class OptimalCounterController {
private final AtomicInteger requestCount = new AtomicInteger(0);
@GetMapping("/endpoint")
public ResponseEntity<?> endpoint() {
// Хороший баланс между простотой и производительностью
int count = requestCount.incrementAndGet();
return ResponseEntity.ok(Map.of("count", count));
}
}
Проверка работоспособности
@SpringBootTest
public class SafeCounterTest {
@Autowired
private OptimalCounterController controller;
@Test
public void testThreadSafety() throws InterruptedException {
final int THREADS = 100;
final int ITERATIONS = 100;
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
for (int i = 0; i < THREADS; i++) {
executor.submit(() -> {
for (int j = 0; j < ITERATIONS; j++) {
controller.endpoint();
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// Теперь будет ровно 10000!
assertEquals(10000, controller.requestCount.get());
}
}
Ключевые моменты
- Обычные переменные НЕ потокобезопасны — избегай в многопоточных контекстах
- Spring создаёт один инстанс Controller — все потоки его используют
- AtomicInteger — идеален для простых счётчиков
- БД/Redis — для распределённых систем
- Всегда тестируй многопоточность — используй ExecutorService и стресс-тесты