2024年毕设系列:从零实现一个高可用的短链服务——技术选型与工程实践
一、毕设常见痛点:为什么“短链”成了救命稻草
做毕设最怕两件事:老师一句“功能太单薄”,评委一句“代码像作业”。很多同学把商城、博客、聊天室翻来覆去写,最后落个“CRUD 堆砌”的评价。短链服务好在三点:
- 场景真实:面试常问,生产常用,简历能写。
- 技术纵深:ID 生成、缓存、防刷、302 性能、高可用,每点都能展开。
- 体量可控:两周可跑通主流程,四周能打磨到“能上线”的级别。
把短链做成毕设,既能让老师看见“分布式”影子,又不至于被微服务洪流拖垮,性价比极高。
二、需求拆解:高并发302、唯一ID、防滥用
先列三条最硬的核心指标,后面所有选型都围绕它们打分。
- 302 重定向 QPS ≥ 3w:校内毕设答辩现场可模拟,用
wrk一把梭就能出报告。 - ID 必须全局唯一且短:8~62 进制字符串,长度 ≤ 7 位,否则“短”链不短。
- 防刷:同一 IP 1min 内 > 60 次请求或 1 天 > 1000 次,直接 429,不废话。
把需求拆成表,后面技术选型直接打分,省得拍脑袋。
三、技术选型对比:Redis vs. DynamoDB,Snowflake vs. HashID
3.1 存储:Redis 还是 DynamoDB?
| 维度 | Redis(内存 + RDB) | DynamoDB(On-Demand) |
|---|---|---|
| 延迟 | 0.3 ms | 5~8 ms |
| 成本 | 内存贵,学校实验室白嫖 | 免费 tier 足够,毕设零元购 |
| 运维 | 需自己配主从、哨兵 | AWS 全托管,毕设省心 |
| 数据面代码量 | 少,Lua 脚本即可 | 多,要封装 SDK |
结论:
- 实验室有旧服务器可白嫖 → Redis
- 想直接上线放简历链接 → DynamoDB
下文代码同时给出两套实现,切换只需改一行配置。
3.2 ID 生成:Snowflake vs. HashID
| 维度 | Snowflake(64 bit) | HashID(salt + 自增) |
|---|---|---|
| 有序 | 时间有序,可排序 | 依赖 DB 自增,也有序 |
| 长度 | 19 位十进制 | 可配置 7 位 62 进制 |
| 时钟回拨 | 致命,需 NTP 校准 | 无,依赖 DB 即可 |
| 实现复杂度 | 中等,位运算 | 低,调库即可 |
结论:
- 想炫技、讲位运算 → Snowflake
- 求稳、两周交差 → HashID
下面代码把两种策略都包成IDGen接口,随时换刀。
四、核心代码:Go 版与 Python 版对照
4.1 Go 版(Redis + Snowflake)
package main import ( "context" "errors" "fmt" "net/http" "strconv" "time" "github.com/go-redis/redis/v8" "github.com/sony/sonyflake" ) var ( rdb *redis.Client sf *sonyflake.Sonyflake ctx = context.Background() ) func init() { rdb = redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"}) sf = sonyflake.NewSonyflake(sonyflake.Settings{}) // 默认本机时钟 } // 1. 生成短码 func createShortURL(longURL string) (string, error) { id, err := sf.NextID() if err != nil乃至于时钟回拨 { return "", fmt.Errorf("snowflake error: %w", err) } code := base62Encode(id) // 7 位 err = rdb.Set(ctx, code, longURL, 24*30*time.Hour).Err() return code, err } // 2. 302 重定向 func redirect(w http.ResponseWriter, r *http.Request) { code := r.URL.Path[1:] longURL, err := rdb.Get(ctx, code).Result() if err == redis.Nil { http.NotFound(w, r) return } if err != nil { http.Error(w, "internal error", 500) return } http.Redirect(w, r, longURL, http.StatusFound) } func main() { http.HandleFunc("/", redirect) http.HandleFunc("/create", func(w http.ResponseWriter, r *http.Request) { long := r.FormValue("url") code, err := createShortURL(long) if err != nil { http.Error(w, err.Error(), 500) return } fmt.Fprintf(w, "https://s.example.com/%s", code) }) http.ListenAndServe(":8080", nil) }4.2 Python 版(DynamoDB + HashID)
import os, boto3, hashids from flask import Flask, redirect, request, abort app = Flask(__name__) table = boto3.resource('dynamodb', region_name='ap-southeast-1').Table('short_url') hasher = hashids.Hashids(salt=os.getenv("HASH_SALT", "demo"), min_length=7) # 1. 生成短码 @app.route("/create", methods=["POST"]) def create(): long_url = request.form["url"] # 用 DynamoDB 原子计数器 resp = table.update_item( Key={"pk": "counter"}, UpdateExpression="ADD seq :1", ExpressionAttributeValues={":1": 1}, ReturnValues="UPDATED_NEW", ) seq = resp["Attributes"]["seq"] code = hasher.encode(seq) table.put_item(Item={"code": code, "long": long_url}) return f"https://s.example.com/{code}" # 2. 302 重定向 @app.route("/<code>") def short(code): resp = table.get_item(Key={"code": code}).get("Item") if not resp: abort(404) return redirect(resp["long"], code=302) if __name__ == "__main__": app.run(port=8080)两段代码都保留最简错误处理,方便读者先跑通再逐步加料。
五、压测结果与安全加固
5.1 压测环境
- 4C8G 笔记本,Docker 限 1G 内存
wrk -t4 -c100 -d30s https://s.example.com/xxxxxx
| 方案 | QPS | P99 延迟 | 备注 |
|---|---|---|---|
| Go+Redis | 3.2w | 9 ms | CPU 70%,网卡先顶满 |
| Py+DynamoDB | 1.1w | 28 ms | 受限于 AWS 延迟,可接受 |
5.2 安全三板斧
- 限流:
uber-go/ratelimit令牌桶,1min 60 个令牌,超了直接 429。 - Referer 校验:只允许白名单域名调用
/create,防止被当公共图床。 - 缓存穿透:Redis 层布隆过滤器,不存在短码直接返回 404,避免打 DB。
六、生产环境避坑指南
Snowflake 时钟回拨:
部署前跑ntpdate,仍回拨就抛异常降级到 HashID,保证链路可用。缓存穿透:
空对象也缓存 5min,值写"NIL",客户端见"NIL"直接 404。DynamoDB 热分区:
短码做code主键,随机性足够,一般无热点;若仍出现,把表开 Adaptive Capacity。Go map 并发写:
全局计数器别用map,用sync.Map或干脆扔 Redis。
七、把短链做成可插拔组件
整个服务被拆成三块:
idgen:接口化,Snowflake/HashID 随换。storage:抽象Get/Set,Redis、DynamoDB、MySQL 都能插。limiter:令牌桶、漏桶、滑动窗口,各实现一个Allow() bool即可。
毕业设计答辩完,把仓库公开,README 里留一张“架构插拔图”,面试官一看就懂:这同学不是堆功能,而是面向接口设计。后续想加“二维码生成”、“自定义域名”、“访问统计”,直接写新插件,不碰老代码。
八、小结与下一步
两周时间,先把 302 跑通;再花一周加布隆过滤器、限流、单元测试;最后一周写压测报告和 PPT,毕业设计稳稳过关。代码已开源在 GitHub,欢迎提 PR 一起把idgen、storage、limiter继续拆成独立子模块,让短链服务成为真正的“毕设脚手架”。