Вступление
В последние несколько недель и особенно в последние несколько дней с рекомендацией оставаться дома из-за пандемического кризиса, с которым мы боремся (оставайтесь дома, ребята! эта рекомендация никогда не будет преувеличением), я читал много статей и экспериментировал с новыми технологиями. В один из таких случаев я наткнулся на эту отличную статью от Sunil P V о том, как работает приложение для сокращения URL-адресов, и, прочитав ее и увидев архитектуру, я подумал, что ее разработка была бы отличным способом скоротать время. Я уже писал ранее о том, как мне трудно найти идеи для практических проектов здесь и здесь , так что это звучало разумно.
Объяснение архитектуры
Заимствуя диаграмму, которую Сунил П.В. привел в своей статье, я объясню, какие технологии я использовал для разработки проекта и почему. Во-первых, давайте посмотрим на диаграмму:
Каждое приложение для сокращения URL-адресов, которое я видел до сих пор, работает аналогично. Когда пользователь отправляет длинный URL-адрес, API генерирует короткий хэш, который идентифицирует длинный URL-адрес и сохраняет обе информации в базе данных, затем, когда пользователь запрашивает sho.rt/hash
API получает хэш и представленный им длинный URL-адрес и перенаправляет на этот URL-адрес. Достаточно просто.
Наиболее важной частью архитектуры является сам API, представленный зеленым прямоугольником на диаграмме, предполагается, что этот API имеет много экземпляров за балансировщиком нагрузки, что означает, что он должен быть распределен. У нас есть база данных, в которой сохраняется информация об URL-адресе, Redis для целей кэширования и Zookeeper для координации. Давайте углубимся в подробности:
API для сокращения URL-адресов : API должен быть распределенным и устойчивым. Поскольку это был проект, созданный для того, чтобы практиковать новые вещи, я смешал вещи, которые я знаю (Spring), и тему, с которой я хотел поэкспериментировать в течение некоторого времени (WebFlux), и поэтому приложение создано с использованием Spring WebFlux в качестве основы. Небольшой нагрузочный тест, проведенный с помощью Apache JMeter, показывает, что благодаря своему неблокирующему подходу он гораздо более устойчив, чем обычный старый Spring MVC при высокой нагрузке. В дополнение к этому, архитектура предполагает использование библиотеки Hashids чтобы сгенерировать хэши для URL-адреса, я последовал этой рекомендации, подробнее об этом позже.
База данных : База данных, описанная Сунилом П.В. в этой статье, – это DynamoDB, но в ней также упоминаются реляционные базы данных. Я решил использовать реляционную базу данных для простой настройки (нет необходимости в облачной настройке и т.д.), А также потому, что это также дало мне еще одну возможность поэкспериментировать. Поскольку само приложение было реактивным и неблокирующим, остальные компоненты должны быть такими же, чтобы в полной мере воспользоваться его преимуществами. Таким образом, я решил использовать PostgreSQL с его реактивным драйвером и на базе Spring R2DBC. Spring R2DBC упрощает использование своих репозиториев для всех, кто раньше использовал Spring Data JPA, так что это не было чем-то новым, но здорово, что уровни контроллера и данных говорят на одном языке с
Mono
иПоток
повсюду.Redis : Это самая простая часть для объяснения, не только потому, что это рекомендуется в архитектуре, но и потому, что в настоящее время это почти стандарт для кэширования. Он используется для кэширования отношения short url x long url, чтобы не нагружать базу данных многочисленными получениями для одного и того же URL-адреса, базовое кэширование. Spring также предлагает реактивную реализацию для Redis, поэтому использовать ее в архитектуре – отличная идея.
Zookeeper : Возможно, это накладные расходы, хотя в статье конкретно упоминается Zookeeper, в нем также говорится, что его можно заменить Redis. Zookeeper просто используется для хранения счетчика, который является общим для всех экземпляров API, который используется для генерации диапазона идентификаторов, за которые будет отвечать данный экземпляр. Дайте мне знать, что вы думаете об этом, так как это что-то новое для меня. Впервые в жизни я воспользовался Zookeeper.
Код можно найти здесь и у него также есть некоторые дополнительные функции, с которыми я экспериментировал, такие как трассировка с помощью Zipkin и экспорт метрик в Elasticsearch. Поскольку это не является частью ядра, я не буду говорить об этом здесь, но не стесняйтесь включать их, если хотите.
Детали Кода
Давайте начнем обсуждение кода со специфики конфигурации. Есть два очень важных компонента и один для удобства, которые настроены. Для удобства я объявляю Hashids
bean уже с солью, так что мне не нужно воссоздавать каждый раз, когда я использую:
@Bean public Hashids hashids() { return new Hashids(hashIdsSalt); }
Затем есть довольно простая конфигурация Redis, но обратите внимание, что она является реактивной. У Spring Data Redis уже есть готовая к использованию фабрика подключений, мне просто нужно иметь Reactive RedisTemplate
для использования:
@Bean public ReactiveRedisOperationsredisOperations(ReactiveRedisConnectionFactory connectionFactory) { GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); RedisSerializationContext context = RedisSerializationContext. newSerializationContext(new StringRedisSerializer()) .value(serializer).build(); return new ReactiveRedisTemplate<>(connectionFactory, context); }
Затем есть компонент с именем UrlIdRange
это используется для управления диапазоном идентификаторов, за которые отвечает экземпляр приложения. Как я уже говорил, диапазон настраивается в Zookeeper, но у каждого экземпляра есть свой собственный диапазон идентификаторов для назначения, и только когда он исчерпан, он снова отправляется на сервер Zookeeper, чтобы получить новый диапазон. Он уже связывается с Zookeeper, чтобы создать диапазон при создании компонента.
@Bean public UrlIdRange urlIdRange(SharedConfigurationService sharedConfigurationService) { Integer counter = sharedConfigurationService.getSharedCounter(urlRangeKey); return new UrlIdRange(counter); }
С учетом этого сейчас самое подходящее время объяснить Службу общей конфигурации |/и классы
UrlIdRange .
Реализация Службы общей конфигурации
использует Apache Curator Framework для подключения к серверу Zookeeper и выполнения действий с общим счетчиком. Я потрачу на это немного больше времени, потому что это то, что я сделал впервые, и предложения о том, как улучшить или даже сказать, что что-то не так, приветствуются. У класса есть только один метод получите общий счетчик
и свойство, которое получает базовый URL-адрес сервера Zookeeper из yml-файла свойств приложения. Он получает ключ
типа/| Строка а затем начинает его обработку:
1 – Запускает клиент:
final CuratorFramework client = newClient(baseUrl,new RetryNTimes(3, 100)); client.start();
2 – Создайте Общий счет
:
SharedCount sharedCounter = new SharedCount(client, key, 0);
3 – На блоке try-catch все сделано:
try { sharedCounter.start(); VersionedValuecounter = sharedCounter.getVersionedValue(); while(!sharedCounter.trySetCount(counter, counter.getValue() + 1)) { counter = sharedCounter.getVersionedValue(); } sharedCounter.close(); client.close(); return counter.getValue(); } catch (Exception e) { log.error("Error while starting shared counter, impossible to update counter.", e); throw new NotAbleToUpdateCounterException(); }
Запускается Shared Count
, а затем приложение получает свое значение versionedvalue и пытается установить новое значение для счетчика, оно будет установлено только в том случае, если значения не изменились, в противном случае оно продолжит попытки. Я поступил так, потому что думал о распределенной характеристике приложения, в среде с высокой нагрузкой могут возникнуть коллизии, и это будет проблемой для управления идентификаторами. Я не знаю, является ли это хорошей практикой или даже действительно ли это необходимо, так что предупредите меня о том, что может пойти не так или нет. Затем я закрываю счетчик и клиент и возвращаю значение.
Диапазон идентификаторов URL
конструктор вызывается при создании компонента с текущим счетчиком, а затем выполняется следующее:
public UrlIdRange(Integer counter) { this.calculateRange(counter); this.hasNext = true; } public void calculateRange(Integer counter) { this.initialValue = counter * 100_000; this.currentValue = new AtomicInteger(initialValue); this.finalValue = initialValue + 99_999; this.hasNext = true; }
Конструктор вызывает calculate Range
, где выполняется вычисление, диапазон равен 100 000 идентификаторов, если счетчик равен 0, то он будет от 0 до 99,999 и так далее. Возможно, я немного преувеличил количество, и его можно было бы настроить с помощью .файл yml.
С первоначальной настройкой покончено, пришло время погрузиться в Web Flux. Как я уже говорил, у API есть две конечные точки: одна используется для поиска URL-адреса и перенаправления, а другая – для создания новых коротких URL-адресов. Маршрутизатор очень прост:
@Bean public RouterFunctionroute(UrlInfoHandler handler, ErrorHandler errorHandler) { return RouterFunctions.route() .onError(Exception.class, errorHandler::handleError) .GET("/{url}", handler::findLongUrlAndRedirect) .POST("/", accept(MediaType.APPLICATION_JSON), handler::generateAndSaveShortUrl) .build(); }
Люди могут счесть аннотацию @RequestMapping
из Spring MVC удобной, но я думаю, что подобная функция маршрутизатора, где все маршруты определены в одном месте, также имеет свои преимущества, гораздо проще увидеть, какие маршруты доступны, и Spring предоставляет некоторые функции для организации его в больших приложениях. Поскольку Spring Boot предназначен для микросервисов, у которых в любом случае не должно быть много маршрутов для каждого приложения, это не проблема.
Затем у нас есть UrlInfoHandler
, где происходит самое интересное. Очень интересно и, возможно, немного ошеломляюще впервые столкнуться с декларативным программированием, но поскольку у меня уже был некоторый опыт работы с Javascript и его декларативным, неблокирующим стилем, это было не так уж плохо, но, конечно, мне нужно позже показать некоторым коллегам, чтобы посмотреть, понимают ли они, что такое происходит. Давайте начнем с метода find Long Url И перенаправления
:
public MonofindLongUrlAndRedirect(ServerRequest serverRequest) { String shortUrl = serverRequest.pathVariable("url"); return cacheService.getFromCacheOrSupplier(shortUrl, UrlInfo.class, () -> urlInfoService.findByShortUrl(shortUrl)) .doOnNext(urlInfo -> log.info("UrlInfo found: {}", urlInfo)) .flatMap(urlInfo -> permanentRedirect(URI.create(urlInfo.getLongUrl())).build()) .switchIfEmpty(status(HttpStatus.NOT_FOUND) .bodyValue(ErrorResponse.notFound(shortUrl, ErrorCode.URL_INFO_NOT_FOUND))); }
Я получаю короткий URL-адрес
из запроса, и начинается волшебство. Сначала я иду в служба кэширования
чтобы увидеть, является ли Url Info
уже существует, если нет, вызывается Поставщик
. Поскольку реактивный подход Redis cache в Spring не поддерживается @Cacheable и его простотой, насколько я знаю, я решил использовать этот подход. Возможно, я мог бы использовать AOP, но я подумал, что этот способ хорошо сочетается с остальным кодом, полным лямбда-функций, или Функциональные интерфейсы , как это описано в Java. Затем я вызываю doOnNext()
только для целей ведения журнала, а затем Плоская карта
перенаправление. Если пусто, приложение отвечает статусом 404. Метод, который генерирует короткий URL-адрес, работает аналогично в реактивном смысле, хотя и отличается по логике, вы можете взглянуть на код в репозитории .
Осуществление Служба кэширования
это интересно, потому что оно использует функции Reactor исключительно для кэширования:
publicMono getFromCacheOrSupplier(String key, Class clazz, Supplier > orElseGet) { return CacheMono.lookup(k -> redisOps.opsForValue().get(buildKey(key, clazz)) .map(w -> Signal.next(clazz.cast(w))), key) .onCacheMissResume(orElseGet) .andWriteWith((k, v) -> Mono.fromRunnable(() -> redisOps.opsForValue().setIfAbsent(buildKey(k, clazz), v.get()).subscribe())); }
Использование Cache Mono
Я могу получить реактивный поток управления исключительно для кэширования, это также сделало возможным использование моего поставщика таким плавным способом, используя onCacheMissResume
. Но сначала он использует метод lookup
для вызова Redis с заданным ключом, что очень просто, поскольку интерфейсы Red is reactive используются так же, как и блокирующие, но доставляют нам Mono
. Наконец, в случае промаха значение кэшируется с помощью и записывается с помощью
. В любом случае объект возвращается к нам, продолжая поток. Очень интересная логика, и, пожалуйста, скажите мне, есть ли что-нибудь, что можно улучшить!
Несмотря на то, что мы не вдаваемся в подробности о маршруте POST, интересно увидеть реализацию safe If Not Exists
из UrlInfoService
, который вызывается обработчиком для генерации короткого URL-адреса и сохранения, но только если он не существует, цепочки из трех методы вызываются:
// Tries to find the long url, if empty, save the request public MonosaveIfNotExist(UrlGenerateRequest request) { return findByLongUrl(request.getLongUrl()).switchIfEmpty(save(request)); } // This is where hashids comes into play, encoding the id given by url range private Mono save(UrlGenerateRequest request) { Integer id = getUrlId(); String hash = hashids.encode(id); return urlInfoRepository.save( UrlInfo.builder() .longUrl(request.getLongUrl()) .shortUrl(hash) .expiryAt(request.getExpiryAt()) .build() ); } // Get the current value of the range and deals with range shortage as well private Integer getUrlId() { if (!urlIdRange.hasNext()) { Integer counter = sharedConfigurationService.getSharedCounter(urlRangeKey); urlIdRange.calculateRange(counter); } return urlIdRange.getCurrentValue(); }
Последняя важная часть, которую осталось упомянуть, – это уровень данных, использующий R2DBC. Это также очень просто, поскольку репозитории Spring Data R2DBC не сильно отличаются от стандартных, отличаясь только возвратом Mono
или Поток
. Однако я заметил одну вещь: я не нашел способа сопоставить отношения таблиц в R2DBC, для этого, конечно, нет аннотаций javax или аннотаций Hibernate и я не нашел никаких ссылок на это в документации. Однако я могу ошибаться. Дайте мне знать, если вам что-нибудь известно об этом.
Вывод
Это был действительно приятный опыт – получить схему архитектуры приложения, добавить свои собственные идеи и воплотить их в коде. Я надеюсь, что смогу найти больше статей такого типа в будущем. Были и другие эксперименты, о которых я здесь не упоминал, такие как документация и тестирование, загляните в репозиторий , чтобы узнать, что я с этим сделал, я, вероятно, напишу об этом еще один пост. Спасибо всем, кто дошел до конца поста (он был длинным), и позвольте мне услышать, что вы думаете!
Оригинал: “https://dev.to/leoat12/building-an-url-shortening-api-with-spring-webflux-and-a-lot-of-supporting-cast-3ni8”