使用 Go 和 Redis 构建快速 URL 缩短器

2025-06-07

使用 Go 和 Redis 构建快速 URL 缩短器

为什么现在要构建 URL 缩短器?

URL 缩短器已经存在了一段时间,人们可以从网络上数百个 URL 缩短器中选择一个并开始使用它。

然而,构建我们自己的 URL 缩短器有其自身的好处 -

  1. 大多数 URL 缩短器都要求您付费创建自定义 URL 短链接(或slug
  2. 许多成熟的 URL 缩短器只允许您在短时间内保留重定向。
  3. URL 缩短服务通常通过向第三方出售 URL 重定向和客户 IP 地址来盈利。如果您担心数据隐私问题,可以自行构建 URL 缩短服务。

功能要求

  1. 我们应该能够为每个有效的 HTTPS URL 生成唯一的短 URL
  2. 我们应该能够快速解析短网址并将用户重定向到实际网址
  3. 短网址应该具有有效期/到期日。
  4. 我们应该能够指定一个可选的自定义短网址。如果提供了这样的自定义 slug,我们会尝试将原始网址映射到自定义短网址,否则我们将生成一个新的。

非功能性需求

  1. 系统读取量会很大,也就是说,重定向的比例会远高于生成短网址的比例。假设这个比例是 100:1
  2. 解析短网址并将用户重定向到原始网址应立即进行,并且延迟最少。

交通量估算

  1. 假设用户每月生成 10 万个短网址。这意味着我们预计每月会有 10 万 * 100 次重定向 -> 1000 万次重定向。
  2. 10M 次重定向将转换为每秒 10M / (30d * 24h * 3600s) = ~3 次重定向。

存储估算

为了计算存储,我们假设使用一个键值存储,其中键是短网址,值是原始网址,并且整个记录都有到期日。

custom-short: ~10 characters
url: ~1000 characters
ttl: int32
Enter fullscreen mode Exit fullscreen mode

这样,我们可以假设一条记录大约为 1KB。假设每月有 10 万个唯一的短网址,我们可以假设内存容量为 100K * 1KB = 大约 100 MB。全年下来,我们可以假设内存容量大约为 1.2GB/年。

生成短 URL 或 Slug

如果用户已提供自定义短网址,我们会尝试将其设置为短网址。否则,我们将需要生成一个唯一的短网址。

请记住,我们每月生成 10 万个短网址,或每年生成 120 万个短网址,或 12 年内生成约 1500 万个短网址 :)

我们如何才能为 1500 万个 URL 生成唯一且冲突最少的 URL?

Base62编码

我们的自定义短网址必须合理地缩短至 1 到 10 个字符。要生成这样的网址,首先我们可以生成int64一个特定范围内的随机数(0, 15M)。具体方法是将这个数字从 转换Base10Base62

但在此之前,让我们先来了解一下——

假设int64我们生成的随机数是234556

等效二进制或Base2-> 001111000010100111

等效的 Hex 或Base16->C3493

等效Base64编码字符串将是 ->8q5

字母表Base64由以下字符组成

const (
    alphabet      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/="
)
Enter fullscreen mode Exit fullscreen mode

但是,我们不希望 URL 包含特殊字符。因此,我们删除了斜杠和等号,并创建了一个Base62字母表,并将其编码int64Base62字符串。

const (
    alphabet      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"
)
Enter fullscreen mode Exit fullscreen mode

记住,我们假设最多存储 15M 个短字符串。因此,如果我们15,000,000为短字符串生成一个随机整数,则等效的Base62编码字符串将是 ->El6ab

那只有 5 个字符!那100,000,000(100M) 呢?等价的Base62应该是 ->oJKVg

您会看到,我们的短网址/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()
}

Enter fullscreen mode Exit fullscreen mode

系统设计

由于帖子标题有 Golang 和 Redis,我们显然将使用它们两者来构建我们的 URL 缩短服务。

设计)

Redis 是一个快速的键值存储系统,就我们的目的而言,它似乎是理想的解决方案。对于 Go 语言的 Web 服务器,我使用了受Akhil Sharma 的 YouTube 频道启发的 go-fiber 。如果您有 Node.JS / Express 背景,那么 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"`
}
Enter fullscreen mode Exit fullscreen mode

连接到 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
}

Enter fullscreen mode Exit fullscreen mode

缩短端点

Shorten 端点接收以下有效载荷 -

{
  "url": "https://your-really-long-url/"
  "custom-short": "optional",
  "expiry": 24
}
Enter fullscreen mode Exit fullscreen mode

缩短的 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,
            })
        }
    }
Enter fullscreen mode Exit fullscreen mode

我们使用 Redis-0 存储 URL Shorts,并使用 Redis-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()
Enter fullscreen mode Exit fullscreen mode

生成自定义短消息后,我们只需使用上面定义的结构返回响应。

Resolve 端点

URL 解析非常简单。我们只需根据 Redis 中的键检查路径参数,然后使用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)
}
Enter fullscreen mode Exit fullscreen mode

就这样!我们现在有一个快速运行的 URL 缩短器。点击此处查看完整源代码。请告诉我你的想法和反馈 :)

文章来源:https://dev.to/mahadevans87/building-a-fast-url-shortener-with-go-and-redis-31b9
PREV
别再在 GitHub 上推送你的 React API Key 了😪
NEXT
掌握干净代码:开发人员的基本实践