← Назад к вопросам

Возникнут ли проблемы при базовой реализации @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());
    }
}

Ключевые моменты

  1. Обычные переменные НЕ потокобезопасны — избегай в многопоточных контекстах
  2. Spring создаёт один инстанс Controller — все потоки его используют
  3. AtomicInteger — идеален для простых счётчиков
  4. БД/Redis — для распределённых систем
  5. Всегда тестируй многопоточность — используй ExecutorService и стресс-тесты