DevGang
Авторизоваться

Создание быстрого средства сокращения URL-адресов с помощью Go и Redis

Зачем сейчас создавать средство сокращения URL-адресов?

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

Однако, создание собственного средства  сокращения URL=адресов имеет свои преимущества:

  1. Большинство программ для сокращения URL-адресов требуют, чтобы вы платили за создание пользовательских сокращений URL-адресов (или slugs).
  2. Многие современные средства сокращения URL-адресов позволят вам удерживать перенаправления только в течение короткого периода времени.
  3. Сокращатели URL-адресов обычно зарабатывают деньги, продавая перенаправление URL-адресов и IP-адреса третьим лицам. Если вы беспокоитесь о конфиденциальности данных, вам следует создать свой собственный сокращатель URL-адресов.

Функциональные требования

  • Мы должны иметь возможность генерировать уникально короткие URL-адреса для каждого действительного HTTPS URL.
  • Мы должны иметь возможность быстро разрешать короткие URL-адреса и перенаправлять пользователей на реальные URL-адреса.
  • Короткие URL-адреса должны иметь срок службы/истечения срока действия.
  • Мы должны иметь возможность указать необязательный пользовательский короткий URL-адрес. Если такой пользовательский slug предоставлен, мы попытаемся сопоставить исходный URL-адрес с пользовательским коротким, в противном случае мы создадим новый.

Нефункциональные требования

  1. Система будет загружена для чтения, т.е. количество переадресаций будет намного выше, чем при генерации коротких URL-адресов. Давайте предположим, что соотношение 100:1
  2. Разрешение коротких 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-адресов. 

#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу