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, а уменьшение счётчика выполнять атомарной операцией.