使用 Go 和 Redis 构建快速 URL 缩短器
为什么现在要构建 URL 缩短器?
URL 缩短器已经存在了一段时间,人们可以从网络上数百个 URL 缩短器中选择一个并开始使用它。
然而,构建我们自己的 URL 缩短器有其自身的好处 -
- 大多数 URL 缩短器都要求您付费创建自定义 URL 短链接(或slug)
- 许多成熟的 URL 缩短器只允许您在短时间内保留重定向。
- URL 缩短服务通常通过向第三方出售 URL 重定向和客户 IP 地址来盈利。如果您担心数据隐私问题,可以自行构建 URL 缩短服务。
功能要求
- 我们应该能够为每个有效的 HTTPS URL 生成唯一的短 URL
- 我们应该能够快速解析短网址并将用户重定向到实际网址
- 短网址应该具有有效期/到期日。
- 我们应该能够指定一个可选的自定义短网址。如果提供了这样的自定义 slug,我们会尝试将原始网址映射到自定义短网址,否则我们将生成一个新的。
非功能性需求
- 系统读取量会很大,也就是说,重定向的比例会远高于生成短网址的比例。假设这个比例是 100:1
- 解析短网址并将用户重定向到原始网址应立即进行,并且延迟最少。
交通量估算
- 假设用户每月生成 10 万个短网址。这意味着我们预计每月会有 10 万 * 100 次重定向 -> 1000 万次重定向。
- 10M 次重定向将转换为每秒 10M / (30d * 24h * 3600s) = ~3 次重定向。
存储估算
为了计算存储,我们假设使用一个键值存储,其中键是短网址,值是原始网址,并且整个记录都有到期日。
custom-short: ~10 characters
url: ~1000 characters
ttl: int32
这样,我们可以假设一条记录大约为 1KB。假设每月有 10 万个唯一的短网址,我们可以假设内存容量为 100K * 1KB = 大约 100 MB。全年下来,我们可以假设内存容量大约为 1.2GB/年。
生成短 URL 或 Slug
如果用户已提供自定义短网址,我们会尝试将其设置为短网址。否则,我们将需要生成一个唯一的短网址。
请记住,我们每月生成 10 万个短网址,或每年生成 120 万个短网址,或 12 年内生成约 1500 万个短网址 :)
我们如何才能为 1500 万个 URL 生成唯一且冲突最少的 URL?
Base62编码
我们的自定义短网址必须合理地缩短至 1 到 10 个字符。要生成这样的网址,首先我们可以生成int64
一个特定范围内的随机数(0, 15M)
。具体方法是将这个数字从 转换Base10
为Base62
。
但在此之前,让我们先来了解一下——
假设int64
我们生成的随机数是234556
。
等效二进制或Base2
-> 001111000010100111
。
等效的 Hex 或Base16
->C3493
等效Base64
编码字符串将是 ->8q5
字母表Base64
由以下字符组成
const (
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/="
)
但是,我们不希望 URL 包含特殊字符。因此,我们删除了斜杠和等号,并创建了一个Base62
字母表,并将其编码int64
为Base62
字符串。
const (
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"
)
记住,我们假设最多存储 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()
}
系统设计
由于帖子标题有 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"`
}
连接到 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 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()
生成自定义短消息后,我们只需使用上面定义的结构返回响应。
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)
}
就这样!我们现在有一个快速运行的 URL 缩短器。点击此处查看完整源代码。请告诉我你的想法和反馈 :)
文章来源:https://dev.to/mahadevans87/building-a-fast-url-shortener-with-go-and-redis-31b9