Как работают сокращатели ссылок
Сокращённые ссылки
«Сокращённый» 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 миллиардов уникальных комбинаций.
Как работают сокращённые ссылки
Работа со ссылками состоит из двух частей:
- Генерация коротких URL – обработка запроса на создание короткого URL.
- Перенаправление с короткого URL на оригинальный –необходимо описать, как обрабатывать короткий URL после клика юзера, чтобы перенаправить его на исходный длинный URL-адрес.
Генерация коротких URL-адресов
Служба сокращения ссылок действует следующим образом:
- Сервис принимает длинный URL, пришедший в
POST
-запросе. - Сервис проверят, нет ли уже такого URL в базе данных. Если входящий длинный URL уже существует в системе, то в ней остался и сгенерированный короткий вариант.
- Если длинного URL нет в базе данных или срок действия сокращённой ссылки истёк, необходимо создать новый токен и отправить короткий URL в качестве ответа, сохранив результат в базе данных.
- Сервис отправляет короткий URL в качестве ответа. Статус
HTTP 201
, если создана новая запись или200
, если запись уже была в базе данных.
Перенаправление
Когда пользователь кликает по сокращённой ссылке, запрос отправляется сервису сокращения URL-адресов, а он перенаправит его на необходимый «реальный» адрес:
- Сервис принимает короткий URL в виде
GET
-запроса. - Сервис проверяет, имеется ли данный токен в базе данных. Если токена не существует, генерируется ответ
HTTP 404 NOT FOUND
. - Если токен присутствует в базе данных, возвращается соответствующих результат. Если время токена истекло, ответом будет
HTTP 498
. - Если всё в порядке, возвращается
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 можно управлять из кэша, а не ходить каждый раз в БД. Записи в кэше могут быть переконфигурированы по истечении срока действия.
Итак, полная схема работы простого сокращателя ссылок:
- Сокращательный софт работает в кластере. Всякий раз, когда ноды запускаются или добавляются в кластер, они обращаются к Zookeeper.
- Zookeeper по запросу выделяет уникальный диапазон номеров для каждой ноды.
- Запрос на сокращение URL поступает к балансировщику нагрузки, который делегирует работу нодам приложения.
- Приложение проверяет, существует ли запись соответствия длинного URL короткому в базе данных или кэше. Если существует и если короткий URL всё ещё действителен, то возвращается
HTTP 200 OK
. - Если длинного URL нет в базе данных, то приложение использует следующий доступный номер из диапазона, выделенного Zookeeper на шаге 2, и генерирует новый токен в библиотеке Hashid. С помощью токена создаётся короткий URL, а вся сопутствующая информация сохраняется в базу данных. Короткий URL отправляется обратно клиенту со статусом
HTTP 201 CREATED
. - При каждом щелчке по короткому URL-адресу запрос снова попадает к балансировщику и передаётся свободной ноде для сокращения адреса.
- Приложение проверяет, имеется ли длинный URL в кэше. Если да, то переходит к шагу 9, если нет – к десятому шагу.
- Если в кэше запись отсутствует, приложение проверяет базу данных. Если там запись есть, переходит к шагу 9, а если нет – к десятому шагу.
- После нахождения длинного URL, на который клиент должен быть перенаправлен, приложение отвечает
HTTP 302
с длинным URL-адресом. Это действие перенаправляет клиента из короткого URL на фактическое местоположение искомой страницы. - Если ни в кэше, ни в БД токен не найден, нода отвечает со статусом
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(); } }