Создание быстрого средства сокращения URL-адресов с помощью Go и Redis
Зачем сейчас создавать средство сокращения URL-адресов?
Сокращатели URL-адресов существуют уже некоторое время, и можно просто выбрать любой из сотен сокращателей URL-адресов, доступных в интернете, и начать им пользоваться.
Однако, создание собственного средства сокращения URL=адресов имеет свои преимущества:
- Большинство программ для сокращения URL-адресов требуют, чтобы вы платили за создание пользовательских сокращений URL-адресов (или slugs).
- Многие современные средства сокращения URL-адресов позволят вам удерживать перенаправления только в течение короткого периода времени.
- Сокращатели URL-адресов обычно зарабатывают деньги, продавая перенаправление URL-адресов и IP-адреса третьим лицам. Если вы беспокоитесь о конфиденциальности данных, вам следует создать свой собственный сокращатель URL-адресов.
Функциональные требования
- Мы должны иметь возможность генерировать уникально короткие URL-адреса для каждого действительного HTTPS URL.
- Мы должны иметь возможность быстро разрешать короткие URL-адреса и перенаправлять пользователей на реальные URL-адреса.
- Короткие URL-адреса должны иметь срок службы/истечения срока действия.
- Мы должны иметь возможность указать необязательный пользовательский короткий URL-адрес. Если такой пользовательский slug предоставлен, мы попытаемся сопоставить исходный URL-адрес с пользовательским коротким, в противном случае мы создадим новый.
Нефункциональные требования
- Система будет загружена для чтения, т.е. количество переадресаций будет намного выше, чем при генерации коротких URL-адресов. Давайте предположим, что соотношение 100:1
- Разрешение коротких URL-адресов и переадресаций пользователей на исходный URL-адрес должно происходить мгновенно с минимальной задержкой
Оценки трафика
- Давайте предположим, что пользователи генерируют 100-тысяч коротких URL-адресов каждый месяц. Это означает, что мы можем ожидать 100 тыс*100 переадресаций -> 10 млн переадресаций каждый месяц
- 10 млн переадресаций будут переведены в 10M/(30d*24h*3600s)=~3 переадресации в секунду
Оценки хранилища
Чтобы вычислить хранилище, давайте предположим, что мы используем хранилище ключ-значение, где ключ является коротким URL-адресом, а значение - исходным URLP-адресом, а срок действия записи истекает.
custom-short: ~10 characters
url: ~1000 characters
ttl: int32
При этом мы можем предположить, что размер записи составит ~1 КБ. Имея 100 тыс. уникальных коротких URL-адресов каждый месяц, мы можем предположить, что объем нашей памяти составляет 100K*1KB =~100 MB. В течение всего года можем предположить ~1.2GB в год.
Генерация URL-адреса или slug
Если пользователь уже предоставил пользовательский короткий адрес, мы попытаемся установить его в качестве короткого URL-адреса. В противном случает нам придется сгенерировать уникальный короткий URL-адрес.
Помните, что мы генерируем 100 тыс коротких ссылок в месяц или 1,2 млн коротких ссылок в год или ~ 15 млн коротких ссылок за 12 лет.
Как нам генерировать уникальные URL-адреса с минимальной коллизией для 15 млн URL-адресов с течением времени?
Кодировка Base62
Наши пользовательские короткие URL-адреса должны быть достаточно короткими от 1 до 10 символов. Чтобы сгенерировать такие URL-адреса, изначально мы можем сгенерировать случайное число int64
в определенном диапазоне (0, 15M)
. Идея состоит в том, чтобы преобразовать это число из Base10
в Base62
.
Но перед этим давайте попробуем прочувствовать интуицию -
- предположим, что случайный
int64
, который мы генерируем, равен234556
; - Эквивалентный двоичный или
Base2
->001111000010100111
; - Эквивалентный шестнадцатеричный или
Base16
->C3493
; - Эквивалентно закодированная строка
Base64
будет ->8q5
.
Алфавит Base64
состоит из следующих символов
const (
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/="
)
Однако, мы не хотим, чтобы URL-адреса содержали специальные символы. Итак, мы убираем косую черту и знак равенства, создаем алфавит Base62
и кодируем наш int64
в строку Base62
.
const (
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"
)
Помните, что мы предполагаем, что мы будем хранить не более 15 млн коротких позиций. Итак, если мы сгенерируем 15 млн как случайное целое число для нашей короткой позиции, эквивалентная строка в кодировке Base62
будет -> El6ab
.
Всего 5 символов! Как насчет 100,000,000
(100M)? Эквивалент Base62
будет -> oJKVg
.
Видите ли, наши короткие URL-адреса/slug состоят из <5 символов, а это именно то, что нам нужно.
Вот версия алгоритма int64 -> Base62, написанного на go, и алгоритм довольно прост.
const (
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
func Base62Encode(number uint64) string {
length := len(alphabet)
var encodedBuilder strings.Builder
encodedBuilder.Grow(10)
for ; number > 0; number = number / uint64(length) {
encodedBuilder.WriteByte(alphabet[(number % uint64(length))])
}
return encodedBuilder.String()
}
Проектирование системы
Поскольку в названии поста есть Golang и Redis, мы, очевидно, собираемся использовать их оба для создания нашего сервиса сокращения URL-адресов.
Redis - это быстрое хранилище значений ключей, и для наших целей оно кажется идеальным решением. Для веб-сервера в go я использовал go-fiber, если вы из Node.JS/Express background, вы должны чувствовать себя как дома с go-fiber.
Наш основной запрос и ответ могут быть определены следующим образом -
Модели данных
type request struct {
URL string `json:"url"`
CustomShort string `json:"short"`
Expiry time.Duration `json:"expiry"`
}
type response struct {
URL string `json:"url"`
CustomShort string `json:"short"`
Expiry time.Duration `json:"expiry"`
XRateRemaining int `json:"rate_limit"`
XRateLimitReset time.Duration `json:"rate_limit_reset"`
}
Подключение к Redis
Мы подключимся к Redis с помощью помощника, подобного этому -
package database
import (
"context"
"os"
"github.com/go-redis/redis/v8"
)
var Ctx = context.Background()
func CreateClient(dbNo int) *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("DB_ADDR"),
Password: os.Getenv("DB_PASS"),
DB: dbNo,
})
return rdb
}
Конечная точка сокращения
Конечная точка Shorten принимает следующую полезную нагрузку -
{
"url": "https://your-really-long-url/"
"custom-short": "optional",
"expiry": 24
}
Сокращенный URL-адрес сопоставляется с файлом shorten.go
. Сначала мы проверяем запрос, выполняя следующие проверки
Ограничение скорости
r2 := database.CreateClient(1)
defer r2.Close()
val, err := r2.Get(database.Ctx, ctx.IP()).Result()
limit, _ := r2.TTL(database.Ctx, ctx.IP()).Result()
if err == redis.Nil {
// Set quota for the current IP Address
_ = r2.Set(database.Ctx, ctx.IP(), os.Getenv("API_QUOTA"), 30*60*time.Second).Err()
} else if err == nil {
valInt, _ := strconv.Atoi(val)
// If Quota has been exhausted
if valInt <= 0 {
return ctx.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"error": "Rate limit exceeded",
"rate_limit_reset": limit / time.Nanosecond / time.Minute,
})
}
}
Мы используем Redis-0 для хранения коротких URL-адресов и Red is-1 для управления IP-адресами и ограничения скорости. Мы предполагаем, что одному и тому же IP-адресу не будет разрешено попадать в конечную точку /shorten более 10 раз (значение по умолчанию в .env
).
Создание короткого Base-62 или хранение пользовательских сокращений
Мы просто проверяем базу данных на наличие коллизии перед сохранением сгенерированного/пользовательских сокращений
if body.CustomShort == "" {
id = helpers.Base62Encode(rand.Uint64())
} else {
id = body.CustomShort
}
r := database.CreateClient(0)
defer r.Close()
val, _ = r.Get(database.Ctx, id).Result()
if val != "" {
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "URL Custom short is already in use",
})
}
if body.Expiry == 0 {
body.Expiry = 24
}
err = r.Set(database.Ctx, id, body.URL, body.Expiry*3600*time.Second).Err()
После создания пользовательских сокращений мы просто возвращаем ответ, используя структуру, определенную выше.
Конечная точка разрешения
Разрешение URL-адресов довольно просто. Мы просто проверяем параметр path на соответствие нашим ключам в redis и перенаправляем на исходное значение (URL), используя HTTP 301 Redirect
.
func Resolve(ctx *fiber.Ctx) error {
url := ctx.Params("url")
r := database.CreateClient(0)
defer r.Close()
value, err := r.Get(database.Ctx, url).Result()
if err == redis.Nil {
return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "short-url not found in db"})
} else if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal error"})
}
return ctx.Redirect(value, 301)
}
Теперь у нас есть быстро работающая программ для сокращения URL-адресов.