短链接系统需采用“先生成、再查重、不存则写”策略,用随机base62短码+数据库唯一索引防冲突;redis缓存热key,mysql存全量数据;重定向默认用302并禁用缓存;访问统计须原子更新。

短链接生成必须避开重复和冲突
用 hash 或 base62 直接编码原始 URL 得到的短码,大概率会碰撞——尤其当业务量上来后,md5(url)[0:6] 这类做法在千万级数据下几乎必然重复。真正可用的方案是「先生成、再查重、不存则写」,而不是「直接算完就用」。
- 推荐用随机生成 + 唯一索引兜底:每次生成 6 位
base62字符(如"aB3xK9"),插入前查数据库是否已存在;失败就重试,最多 5 次 - 数据库表必须给
short_code字段加唯一索引,否则并发写入时仍可能写入重复值 - 避免用自增 ID 转
base62:虽然绝对不重复,但暴露业务总量、易被爬取、无法支持多实例分库
Redis 和 MySQL 怎么分工存短链
高频跳转场景下,纯 DB 查询扛不住,但全放 Redis 又丢数据风险大。合理分工是:Redis 存热 key(short_code → long_url),MySQL 存全量+元数据(创建时间、访问统计、所属用户)。
- 首次请求短链时,先查 Redis;未命中则查 MySQL,查到后立刻
SETNX写入 Redis,过期时间设为 1 小时(防止雪崩) - 写入 MySQL 后,如果失败,要主动删掉刚塞进 Redis 的脏数据,否则下次读会返回错误长链
- 不要用 Redis 的
EXPIRE自动过期来替代冷热分离逻辑——过期后大量请求会穿透到 DB,需配合布隆过滤器或本地缓存降级
重定向响应必须控制 http 状态码和头字段
浏览器对 301 和 302 处理差异极大:301 会被强缓存,改链后旧短码仍跳转到旧地址;302 不缓存,但部分客户端(如微信内嵌浏览器)会限制跳转次数。生产环境建议默认用 302,仅对明确需要 seo 收录的链接开放 301 开关。
- golang 中用
http.Redirect(w, r, url, http.StatusFound)发送302;别手误写成StatusMovedPermanently - 务必设置
Cache-Control: no-store, no-cache, must-revalidate,否则 CDN 或中间代理可能缓存重定向响应 - 如果支持自定义短码(如
/go/github),需额外校验该码未被系统保留(如/admin、/api),否则可能引发路由冲突
Go 并发写入短链时容易漏掉计数更新
访问统计不能只靠 MySQL 的 UPDATE ... SET clicks = clicks + 1,因为高并发下会丢失更新(两个请求同时读出 100,各自加 1 再写回,结果仍是 101)。必须用原子操作或队列异步落库。
立即学习“go语言免费学习笔记(深入)”;
- 简单方案:用 Redis 的
INCR实时累加,定时任务每 5 分钟把各短码的计数同步到 MySQL - 更稳方案:把访问事件发到内存队列(如
chan Struct{ code String }),由单个 goroutine 消费并批量更新 DB - 千万别在 HTTP handler 里直接
db.Exec("UPDATE ...")—— 单次请求延迟可能从毫秒级飙到数百毫秒,拖垮整个服务
实际最难的部分不是生成短码,而是让重定向快、准、不翻车。比如微信里点短链,它会预请求一次 HEAD,如果服务没正确响应 302+location,或者返回了 200 带 HTML,就会卡住或跳转失败。这些边界情况比算法本身更耗调试时间。