Как работают сокращатели ссылок

4374

Как работают сокращатели ссылок

Сокращённые ссылки

«Сокращённый» URL-адрес может выглядеть примерно вот так: https://ts.dz/qp7h3zi. Здесь ts.dz – это домен приложения или компании, а последняя часть – уникальный идентификатор (токен).

Длина токена обычно варьируется от 6 до 9 символов. Для создания безопасного URL, берутся цифры 0-9 и буквы латинского алфавита в нижнем регистре a-z. То есть общий набор возможных вариантов каждого символа состоит из 36 элементов. Ниже показано количество уникальных комбинаций, которые могут быть сгенерированы в зависимости от длины токена.

Длина токена Уникальные комбинации Диапазон
6 2,176,782,336 > 2 млрд
7 78,364,164,096 > 78 млрд
8 2,821,109,907,456 > 2 трлн
9 101,559,956,668,416 > 100 трлн

Из приведённой таблицы следует, что длины токена из 7 символов достаточно, чтобы получить свыше 78 миллиардов уникальных комбинаций.

Как работают сокращённые ссылки

Работа со ссылками состоит из двух частей:

  1. Генерация коротких URL – обработка запроса на создание короткого URL.
  2. Перенаправление с короткого URL на оригинальный –необходимо описать, как обрабатывать короткий URL после клика юзера, чтобы перенаправить его на исходный длинный URL-адрес.

Генерация коротких URL-адресов

Служба сокращения ссылок действует следующим образом:

  1. Сервис принимает длинный URL, пришедший в POST-запросе.
  2. Сервис проверят, нет ли уже такого URL в базе данных. Если входящий длинный URL уже существует в системе, то в ней остался и сгенерированный короткий вариант.
  3. Если длинного URL нет в базе данных или срок действия сокращённой ссылки истёк, необходимо создать новый токен и отправить короткий URL в качестве ответа, сохранив результат в базе данных.
  4. Сервис отправляет короткий URL в качестве ответа. Статус HTTP 201, если создана новая запись или 200, если запись уже была в базе данных.
Как работают сокращатели ссылок
Как работают сокращатели ссылок

Перенаправление

Когда пользователь кликает по сокращённой ссылке, запрос отправляется сервису сокращения URL-адресов, а он перенаправит его на необходимый «реальный» адрес:

  1. Сервис принимает короткий URL в виде GET-запроса.
  2. Сервис проверяет, имеется ли данный токен в базе данных. Если токена не существует, генерируется ответ HTTP 404 NOT FOUND.
  3. Если токен присутствует в базе данных, возвращается соответствующих результат. Если время токена истекло, ответом будет HTTP 498.
  4. Если всё в порядке, возвращается LOCATION header и происходит перенаправление (HTTP 302) по длинной ссылке.

Как генерируются токены

Логика генерирования уникальных случайных токенов довольно хитрая. Служба сокращения URL должна гарантировать, что токен назначен только одному URL-адресу. Иначе может произойти некорректное перенаправление.

Надёжной библиотекой, генерирующей уникальные токены, является Hashids. Она позволяет создавать короткие, уникальные идентификаторы из цифр и букв для всех распространённых языков программирования. Объём кода очень мал, и нет никаких внешних библиотечных зависимостей.

Зашифруем для примера строку "this is my salt":

Hashid Токен Короткая ссылка
1 3joed16 https://test.com/3joed16
2 8lop31w https://test.com/8lop31w
3 l6o3k1n https://test.com/l6o3k1n
4 leo2418 https://test.com/leo2418

Как обрабатывается большое число обращений

Библиотеку Hashids можно использовать для распределённой генерации сокращённых ссылок. Входящее значение для Hashids – число типа long или его аналог в используемом языке программирования. На основании переданного числа генерируется уникальный токен. Чтобы управлять диапазоном передаваемых чисел, можно использовать, например, инструмент Zookeeper.

Каждый узел распределённой системы при запуске запрашивает у Zookeeper новый диапазон чисел. При создании новой короткой ссылки сокращательный софт увеличивает порядковый номер (исходя из диапазона Zookeeper) и передаёт его в Hashids, и библиотека генерирует уникальный токен. Этот токен, как и ранее, сохраняется в БД вместе с полным URL.

Как только узел исчерпает диапазон чисел, он запрашивает у Zookeeper новый интервал. Такая архитектура позволяет добавлять любое количество нод приложения, обслуживающих входящий трафик.

Роль Zookeeper заключается в управлении диапазонами, которые уже назначены или запрошены для назначения. Переменная со счётчиком является атомарной, таким образом, любая нода приложения, работающая на чтение и запись не конфликтует с другими.

Атомарные счётчики можно реализовать и с помощью DynamoDB, MySQL или любой другой СУБД, поддерживающей работу с атомарностью, например, Redis.

Полная схема работы

Примечание: на диаграмме добавлен кэш для повышения производительности приложения, поскольку недавно созданными укороченными URL можно управлять из кэша, а не ходить каждый раз в БД. Записи в кэше могут быть переконфигурированы по истечении срока действия.

Итак, полная схема работы простого сокращателя ссылок:

  1. Сокращательный софт работает в кластере. Всякий раз, когда ноды запускаются или добавляются в кластер, они обращаются к Zookeeper.
  2. Zookeeper по запросу выделяет уникальный диапазон номеров для каждой ноды.
  3. Запрос на сокращение URL поступает к балансировщику нагрузки, который делегирует работу нодам приложения.
  4. Приложение проверяет, существует ли запись соответствия длинного URL короткому в базе данных или кэше. Если существует и если короткий URL всё ещё действителен, то возвращается HTTP 200 OK.
  5. Если длинного URL нет в базе данных, то приложение использует следующий доступный номер из диапазона, выделенного Zookeeper на шаге 2, и генерирует новый токен в библиотеке Hashid. С помощью токена создаётся короткий URL, а вся сопутствующая информация сохраняется в базу данных. Короткий URL отправляется обратно клиенту со статусом HTTP 201 CREATED.
  6. При каждом щелчке по короткому URL-адресу запрос снова попадает к балансировщику и передаётся свободной ноде для сокращения адреса.
  7. Приложение проверяет, имеется ли длинный URL в кэше. Если да, то переходит к шагу 9, если нет – к десятому шагу.
  8. Если в кэше запись отсутствует, приложение проверяет базу данных. Если там запись есть, переходит к шагу 9, а если нет – к десятому шагу.
  9. После нахождения длинного URL, на который клиент должен быть перенаправлен, приложение отвечает HTTP 302 с длинным URL-адресом. Это действие перенаправляет клиента из короткого URL на фактическое местоположение искомой страницы.
  10. Если ни в кэше, ни в БД токен не найден, нода отвечает со статусом HTTP 404 NOT FOUND.

Дополнительный технический раздел: как Библиотека программиста обрабатывает сокращённые ссылки

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

Наш обработчик сокращённых ссылок написан на php. Для новой статьи создаётся сущность Transition, которая содержит переходы из каждой платформы. У неё есть длинная ссылка на статью, уникальный токен, количество переходов, название платформы и время последнего перехода, не считая остальных служебных полей вроде id.

<?php

declare(strict_types=1);

namespace App\Model\Post\Entity\Transition;

use Doctrine\ORM\Mapping as ORM;

/**
 * @Entity
 * @Table(name="article_transitions")
 */
class Transition
{
   ...
      
   /**
    * @ORM\Column(type="string", nullable=false)
    */
   private ?string $shortUrl = null;
   
   /**
    * @ORM\ManyToOne(targetEntity=Article::class)
    */
   private ?Article $article = null;
   
   /**
    * @ORM\Column(type="string", nullable=false)
    */
   private ?string $network = null;
   
   /**
    * @ORM\Column(type="integer", nullable=false)
    */
   private ?int $clicks = 0;
  
   /**
    * @ORM\Column(type="datetime_immutable", nullable=true)
    */
   private ?\DateTimeImmutable $lastClickDate = null;
  
   public function click(\DateTimeImmutable $date): void
   {
      $this->clicks += 1;
      $this->lastClickDate = $date;
   }
}

В приложении есть специальный эндпоинт, отвечающий за обработку переходов по сокращённым ссылкам. Поскольку наше приложение написано на Symfony, у нас есть встроенный ParamConverter, который достаёт из маршрута токен и с помощью метода findOneBy() и передаёт в качестве аргумента в наш action click сущность Transition.

Далее мы передаём сущность в команду Click\Command, отдаём обработчику команды и делаем редирект на статью.

<?php

declare(strict_types=1);

namespace App\Controller\Article;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;
use App\Model\Post\Entity\Transition;
use App\Model\Post\UseCase\Transition\Click;

final class TransitionController extends AbstractController
{
    /**
     * @Route("/sh/{transition}", name="article_transition_click", methods={"GET"})
     *
     * @param Transition $transition
     * @param Click\Handler $handler
     *
     * @return RedirectResponse
     */
    public function click(Transition $transition, Click\Handler $handler): RedirectResponse
    {
        $command = new Click\Command($transition);
        
        $handler->handle($command);
        
        return $this->redirectToRoute('article_show', ['slug' => $transition->getArticle()->getSlug()->getValue()]);
    }
}

Команда – это простая структура данных, которая представляет текущий запрос пользователя.

<?php

declare(strict_types=1);

namespace App\Model\Post\UseCase\Transition\Click;

class Command
{
   public Transition $transition;
  
   public function __construct(Transition $transition)
   {
      $this->transition = $transition;
   }
}

В обработчике команды мы вызываем метод click, передавая туда текущую дату в качестве даты последнего перехода, и вызываем flush, который выполнит запрос в базу данных на обновление кликов.

<?php

declare(strict_types=1);

namespace App\Model\Post\UseCase\Transition\Click;

use App\Model\Flusher;

final class Handler
{
   public Flusher $flusher;
  
   public function __construct(Flusher $flusher)
   {
      $this->flusher = $flusher;
   }
  
   public function handle(Command $command): void
   {
      $command->transition->click(new \DateTimeImmutable('NOW'));
      $this->flusher->flush();
   }
}

источник