Реализовать класс-сервис для работы с токенами с ограничением на количество использований

2. Реализовать сервис токенов с ограничением количества использований

Условие задачи:
Необходимо реализовать сервис для работы с токенами.

Требования:

  • сервис должен генерировать токен;

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

  • при успешном использовании количество оставшихся попыток уменьшается;

  • после исчерпания лимита токен становится недействительным;

  • сервис должен корректно работать при одновременных обращениях нескольких потоков.


Спойлеры к решению

Подсказки
💡 Для хранения токенов можно использовать ConcurrentHashMap.
💡 Для генерации токена подойдёт UUID.
💡 Проверка лимита и его уменьшение должны выполняться атомарно.
💡 Для этого удобно использовать AtomicInteger.
💡 После исчерпания лимита токен можно удалить из хранилища.

Решение
@Service
public class TokenService {

    private static final int MAX_USES = 5;

    private final Map<String, AtomicInteger> tokens =
            new ConcurrentHashMap<>();

    public String generateToken() {
        String token = UUID.randomUUID().toString();

        tokens.put(
                token,
                new AtomicInteger(MAX_USES)
        );

        return token;
    }

    public boolean useToken(String token) {
        if (token == null) {
            return false;
        }

        AtomicBoolean used = new AtomicBoolean(false);

        tokens.computeIfPresent(token, (key, remainingUses) -> {
            int current = remainingUses.get();

            if (current <= 0) {
                return null;
            }

            int remaining = remainingUses.decrementAndGet();
            used.set(true);

            return remaining == 0
                    ? null
                    : remainingUses;
        });

        return used.get();
    }

    public int getRemainingUses(String token) {
        AtomicInteger remainingUses = tokens.get(token);

        return remainingUses == null
                ? 0
                : remainingUses.get();
    }
}

Для каждого токена хранится количество оставшихся использований:

token → remainingUses

Метод computeIfPresent() выполняет изменение значения для конкретного ключа атомарно относительно операций с этим ключом.

После последнего успешного использования запись удаляется из Map, поэтому последующие вызовы useToken() вернут false.

Пример:

String token = tokenService.generateToken();

System.out.println(tokenService.useToken(token)); // true
System.out.println(tokenService.getRemainingUses(token)); // 4

После пяти успешных использований:

System.out.println(tokenService.useToken(token)); // false
System.out.println(tokenService.getRemainingUses(token)); // 0

Для распределённого приложения, где запущено несколько экземпляров сервиса, состояние токенов нужно хранить во внешнем хранилище, например Redis, а уменьшение счётчика выполнять атомарной операцией.