Skip to content

第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 用途数据类型命令
短链接缓存StringSET/GET
访问计数StringINCR
频率限制ZSetZADD/ZCOUNT(滑动窗口)
热门链接ZSetZINCRBY(按点击量排名)

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

练习题 ​

  1. 完成短链接系统的所有接口。
  2. 添加"热门短链接排行"(按点击量排序)。
  3. 实现"短链接过期自动清理"。

把零散知识,整理成可以反复走的路。