URL短链接服务的核心目标是把一个较长的网址转换成较短的访问入口,并在用户访问短入口时准确还原到原始地址。这类服务在活动推广、消息分享、短信通知和后台跳转等场景中非常常见。由于短链接本身承担入口职责,因此在实现时需要同时关注唯一性、读取性能、存储可靠性以及后续扩展能力。使用 Golang 实现具备天然优势,协程模型可以支撑较高并发,标准库和生态组件也能方便地构建 HTTP 服务。

短链接服务的工作机制与设计重点
短链接服务表面上只是把一个长网址变短,实际上包含两个关键流程。第一个流程是生成流程,服务接收客户端提交的长网址,为其分配一个全局唯一的短标识,再把短标识和原始网址的映射关系保存下来。第二个流程是跳转流程,服务从请求路径中取出短标识,查找对应的原始网址,并通过重定向把访问者引导到目标页面。生成流程强调唯一性和写入稳定,跳转流程强调查询速度和命中率。
在短标识生成方式上,常见思路包括哈希摘要和自增编号两类。哈希摘要可以直接根据长网址计算出一个固定长度的字符串,实现简单,但不同输入可能产生相同结果,因此必须处理冲突。自增编号配合 Base62 编码则更加稳定,编号由统一存储或缓存统一分配,天然不会重复。编号经过 Base62 转换后,可以得到由数字、小写字母和大写字母组成的短字符串,长度可控,也便于在浏览器地址中使用。
在存储设计上,短链接映射关系既需要持久化,也需要高速读取。MySQL 适合保存最终数据,能够提供事务能力和关系查询能力。Redis 适合承担缓存职责,可以把高频访问的短标识直接放在内存中。写入时先落库再写缓存,可以保证数据有持久化兜底。读取时先查 Redis,未命中再查 MySQL 并回填缓存,可以显著降低数据库压力。这种组合在当下是比较稳妥的短链接服务实现方式。
使用 Golang 实现短链接服务的完整思路
在技术选型上,可以使用 gin 提供 HTTP 路由,使用 gorm 操作 MySQL,使用 go-redis 访问 Redis。三者组合可以快速搭建结构清晰的服务。项目初始化阶段需要创建 Go 模块,并安装依赖包。执行以下命令可以初始化模块并拉取常用组件。
# 初始化项目并安装依赖 go mod init short_url go get github.com/gin-gonic/gin go get gorm.io/gorm go get gorm.io/driver/mysql go get github.com/go-redis/redis/v8
核心代码可以组织为一个完整的服务入口。服务启动时初始化 MySQL 与 Redis,注册生成接口和跳转接口。生成接口从表单中读取长网址,通过 Redis 的 INCR 命令获得自增编号,再调用 encodeBase62 将编号转换为短标识。随后,映射关系写入 MySQL,并同步写入 Redis。跳转接口优先从 Redis 读取原始网址,如果缓存不存在,再回到 MySQL 查询,并将查询结果重新写入缓存。
package main
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var (
db *gorm.DB
redisClient *redis.Client
ctx = context.Background()
)
const base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// ShortURL 保存短标识与原始长网址的映射关系
type ShortURL struct {
gorm.Model
LongURL string `gorm:"type:text;not null" json:"long_url"`
ShortKey string `gorm:"type:varchar(20);uniqueIndex;not null" json:"short_key"`
}
// encodeBase62 将自增编号转换为 Base62 短字符串
func encodeBase62(num int64) string {
if num == 0 {
return "0"
}
result := ""
for num > 0 {
remainder := int(num % 62)
result = string(base62Chars[remainder]) + result
num = num / 62
}
return result
}
// initMySQL 初始化数据库连接并迁移表结构
func initMySQL() error {
dsn := "root:password@tcp(127.0.0.1:3306)/short_url?charset=utf8mb4&parseTime=True&loc=Local"
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return err
}
return db.AutoMigrate(&ShortURL{})
}
// initRedis 初始化 Redis 连接
func initRedis() error {
redisClient = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
return redisClient.Ping(ctx).Err()
}
// generateShortURL 接收长网址并生成短链接
func generateShortURL(c *gin.Context) {
longURL := c.PostForm("long_url")
if longURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "长网址不能为空"})
return
}
// 通过 Redis 自增生成全局唯一编号
id, err := redisClient.Incr(ctx, "short_url:id").Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成短标识失败"})
return
}
shortKey := encodeBase62(id)
record := ShortURL{
LongURL: longURL,
ShortKey: shortKey,
}
// 先持久化到 MySQL
if err := db.Create(&record).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存映射关系失败"})
return
}
// 再写入 Redis 提升跳转查询速度
if err := redisClient.Set(ctx, shortKey, longURL, 0).Err(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "写入缓存失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"short_url": "http://ipipp.com/" + shortKey,
"long_url": longURL,
})
}
// redirectShortURL 根据短标识跳转到原始长网址
func redirectShortURL(c *gin.Context) {
shortKey := c.Param("short_key")
longURL, err := redisClient.Get(ctx, shortKey).Result()
if err == redis.Nil {
var record ShortURL
if err := db.Where("short_key = ?", shortKey).First(&record).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "短链接不存在"})
return
}
longURL = record.LongURL
redisClient.Set(ctx, shortKey, longURL, 0)
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询缓存失败"})
return
}
c.Redirect(http.StatusFound, longURL)
}
func main() {
if err := initMySQL(); err != nil {
panic("MySQL 初始化失败: " + err.Error())
}
if err := initRedis(); err != nil {
panic("Redis 初始化失败: " + err.Error())
}
router := gin.Default()
router.POST("/generate", generateShortURL)
router.GET("/:short_key", redirectShortURL)
router.Run(":8080")
}
这段实现中有几个值得注意的细节。第一,短标识的唯一性依赖 Redis 的自增操作,因此多个服务实例可以同时生成短链接而不会出现编号冲突。第二,MySQL 中的 ShortKey 字段设置了唯一索引,即使缓存出现异常,数据库层也能阻止重复短标识写入。第三,跳转接口返回的是临时重定向,适合短链接这种中间入口。若业务明确希望浏览器缓存跳转结果,也可以根据场景调整为永久重定向,但需要谨慎评估后续变更成本。
测试验证与生产环境优化方向
服务启动后,可以通过命令行工具验证功能。首先生成短链接,然后访问返回结果中的短路径,观察是否能够跳转到原始地址。以下示例使用本地服务端口进行演示,请求中的长网址可以根据实际环境替换。
# 生成短链接 curl -X POST -d "long_url=https://ipipp.com/help/doc" http://127.0.0.1:8080/generate # 访问短链接,验证是否跳转 curl -L http://127.0.0.1:8080/abc123
在基础功能跑通之后,还可以围绕稳定性与运营能力继续完善。可以从以下几个方向入手。
- 过期控制,短链接不一定需要永久有效,可以在 Redis 中设置过期时间,并配合数据库状态字段判断是否允许跳转。
- 访问统计,记录每个短标识的访问次数、来源和响应状态,便于分析投放效果。
- 安全校验,服务应对提交的长网址做基本检查,过滤明显非法的地址,避免短链接被用于恶意跳转。
- 缓存优化,为热点短链接延长缓存时间,为冷门短链接设置较短缓存时间,从而平衡内存占用和命中率。
如果服务需要面对更高并发,还可以进一步优化缓存策略和 ID 分配方式。例如在 Redis 之外引入本地缓存,减少网络往返;在 ID 分配上采用分段自增或分布式 ID 生成策略,降低单点压力。总体而言,短链接服务的核心并不复杂,但要在唯一性、性能、可维护性和安全性之间做好平衡。通过 Golang 搭建基础版本后,再逐步加入统计、限流、过期和审计能力,就可以形成一个适合生产环境使用的短链接服务。