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

Как реализовать логику подсчета при последовательной выдаче числа на каждый запрос?

1.0 Junior🔥 11 комментариев
#Кэширование и NoSQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Логика подсчета при последовательной выдаче числа на каждый запрос

Это классическая задача реализации счётчика, который инкрементируется при каждом запросе. Существует несколько подходов в зависимости от требований.

Простое решение с глобальной переменной

public class Counter {
    private static int count = 0;
    
    public static synchronized int getNextNumber() {
        return ++count;
    }
}

// Использование
int num1 = Counter.getNextNumber();  // 1
int num2 = Counter.getNextNumber();  // 2
int num3 = Counter.getNextNumber();  // 3

Проблемы:

  • Потокобезопасность достигается через synchronized, но это медленно
  • Глобальное состояние — плохая архитектура
  • Нельзя перезагрузить без перезапуска приложения

Лучший подход: AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private static final AtomicInteger count = new AtomicInteger(0);
    
    public static int getNextNumber() {
        return count.incrementAndGet();
    }
}

Преимущества:

  • Потокобезопасен без блокировок (lock-free)
  • Быстрее чем synchronized
  • Атомарные операции гарантируют консистентность

REST API реализация

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;

@RestController
public class CounterController {
    private final AtomicInteger counter = new AtomicInteger(0);
    
    @GetMapping("/api/next-number")
    public CountResponse getNextNumber() {
        int number = counter.incrementAndGet();
        return new CountResponse(number);
    }
}

class CountResponse {
    private int number;
    
    public CountResponse(int number) {
        this.number = number;
    }
    
    public int getNumber() {
        return number;
    }
}

С использованием базы данных

@Repository
public interface CounterRepository extends JpaRepository<Counter, Long> {
    Counter findByName(String name);
}

@Service
public class CounterService {
    @Autowired
    private CounterRepository repository;
    
    @Transactional
    public synchronized int getNextNumber() {
        Counter counter = repository.findByName("global");
        counter.setCount(counter.getCount() + 1);
        repository.save(counter);
        return counter.getCount();
    }
}

Проблема: синхронизированный метод блокирует поток. Лучше использовать SELECT FOR UPDATE:

@Repository
public interface CounterRepository extends JpaRepository<Counter, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT c FROM Counter c WHERE c.name = :name")
    Counter findByNameForUpdate(@Param("name") String name);
}

@Service
public class CounterService {
    @Transactional
    public int getNextNumber() {
        Counter counter = repository.findByNameForUpdate("global");
        counter.setCount(counter.getCount() + 1);
        return counter.getCount();
    }
}

С использованием Redis (для распределённых систем)

import org.springframework.data.redis.core.RedisTemplate;

@Service
public class CounterService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public long getNextNumber() {
        return redisTemplate.opsForValue().increment("counter");
    }
}

Преимущества:

  • Распределённый счётчик
  • Очень быстро (в памяти)
  • Масштабируется на несколько серверов

Последовательность с префиксом (например, ID документов)

public class DocumentIdGenerator {
    private final AtomicLong sequence = new AtomicLong(0);
    private final String prefix;
    
    public DocumentIdGenerator(String prefix) {
        this.prefix = prefix;
    }
    
    public String getNextId() {
        long num = sequence.incrementAndGet();
        return String.format("%s-%010d", prefix, num);
    }
}

// Использование
DocumentIdGenerator generator = new DocumentIdGenerator("DOC");
String id1 = generator.getNextId();  // DOC-0000000001
String id2 = generator.getNextId();  // DOC-0000000002

Сравнение подходов

ПодходПотокобезопасностьПроизводительностьМасштабируемостьСложность
synchronized❌ низкая❌ нетпростая
AtomicInteger✅ высокая❌ один серверпростая
БД + @Transactional⚠️ средняя✅ несколькосредняя
БД + SELECT FOR UPDATE✅ хорошая✅ несколькосредняя
Redis✅ очень высокая✅ полнаяпростая

Рекомендации

  1. Один сервер, однопоточноеAtomicInteger
  2. Один сервер, многопоточноеAtomicInteger или synchronized
  3. Несколько серверов → Redis или БД с SELECT FOR UPDATE
  4. Нужна персистентность → БД
  5. Максимальная производительность → Redis

Итоги

  • AtomicInteger — простое и эффективное решение
  • SELECT FOR UPDATE — для БД с гарантией консистентности
  • Redis — для распределённых систем
  • synchronized — только если нет альтернатив
  • Выбирайте подход в зависимости от требований масштабируемости
Как реализовать логику подсчета при последовательной выдаче числа на каждый запрос? | PrepBro