第37章:实战项目 —— 短链接系统
本章目标
用 Gin + GORM + Redis 构建完整的短链接服务
37.1 需求分析
用户输入长URL → 生成短码 → /s/{code} → 302跳转到长URL
功能:
1. POST /api/links 创建短链接(长URL → 短码)
2. GET /s/:code 访问短链接(302跳转)
3. GET /api/links 我的短链接列表
4. GET /api/links/:code/stats 访问统计37.2 核心代码
go
// 模型
type ShortLink struct {
gorm.Model
LongURL string `gorm:"type:text;not null"`
Code string `gorm:"uniqueIndex;size:10"`
UserID uint
ExpireAt *time.Time
}
// 生成短码(Base62)
func generateCode(id uint) string {
const base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
var code string
for id > 0 {
code = string(base62[id%62]) + code
id /= 62
}
return code
}
// 创建短链接
func CreateLink(c *gin.Context) {
var req struct {
URL string `json:"url" binding:"required,url"`
ExpireIn int `json:"expire_in"` // 过期小时
}
c.ShouldBindJSON(&req)
link := ShortLink{LongURL: req.URL, UserID: c.GetUint("user_id")}
if req.ExpireIn > 0 {
t := time.Now().Add(time.Duration(req.ExpireIn) * time.Hour)
link.ExpireAt = &t
}
db.Create(&link)
// 生成短码(用自增ID)
link.Code = generateCode(link.ID)
db.Save(&link)
// 缓存热点链接到 Redis
data, _ := json.Marshal(link)
rdb.Set(ctx, "link:"+link.Code, data, 24*time.Hour)
c.JSON(201, gin.H{"short_url": "http://localhost:8080/s/" + link.Code})
}
// 访问跳转(Redis 缓存 + 计次)
func RedirectLink(c *gin.Context) {
code := c.Param("code")
// 1. 查 Redis 缓存
if cached, err := rdb.Get(ctx, "link:"+code).Result(); err == nil {
var link ShortLink
json.Unmarshal([]byte(cached), &link)
// 异步更新访问计数
go func() {
rdb.Incr(ctx, "stats:"+code+":clicks")
db.Model(&ShortLink{}).Where("code = ?", code).
UpdateColumn("click_count", gorm.Expr("click_count + 1"))
}()
c.Redirect(302, link.LongURL)
return
}
// 2. 查 MySQL
var link ShortLink
if db.Where("code = ?", code).First(&link).Error != nil {
c.JSON(404, gin.H{"error": "链接不存在"})
return
}
// 3. 写缓存
data, _ := json.Marshal(link)
rdb.Set(ctx, "link:"+code, data, 24*time.Hour)
// 4. 访问计数 + 跳转
rdb.Incr(ctx, "stats:"+code+":clicks")
c.Redirect(302, link.LongURL)
}37.3 Redis 在本项目中的角色
| Redis 用途 | 数据类型 | 命令 |
|---|---|---|
| 短链接缓存 | String | SET/GET |
| 访问计数 | String | INCR |
| 频率限制 | ZSet | ZADD/ZCOUNT(滑动窗口) |
| 热门链接 | ZSet | ZINCRBY(按点击量排名) |
37.4 频率限制
go
func RateLimitMiddleware(rdb *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
key := "ratelimit:" + ip
now := time.Now().Unix()
// 滑动窗口:删除60秒前的记录
rdb.ZRemRangeByScore(ctx, key, "0", fmt.Sprint(now-60))
// 计数当前窗口
count, _ := rdb.ZCount(ctx, key, fmt.Sprint(now-60), "+inf").Result()
if count >= 30 { // 每分钟最多30次
c.AbortWithStatusJSON(429, gin.H{"error": "请求太频繁"})
return
}
rdb.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: fmt.Sprint(now)})
rdb.Expire(ctx, key, 60*time.Second)
c.Next()
}
}本章小结
本项目综合了 Gin + GORM + Redis 全栈技术:
- String 缓存热点数据
- INCR 原子计数
- ZSet 滑动窗口限流
- 缓存策略:先查 Redis → 没有才查 MySQL → 写入 Redis
练习题
- 完成短链接系统的所有接口。
- 添加"热门短链接排行"(按点击量排序)。
- 实现"短链接过期自动清理"。