1. 短链接系统的价值远不止“把长链接变短”
先聊一个很多人忽略的事实:短链接从表面看就是一段几十个字符压缩成几个字符,但真正做过这套系统的人都知道,它的核心价值集中在三个方向——营销追踪、渠道归因和运营风控。我自己最早做短链接,就是因为投放广告时需要一个能区分渠道来源、统计点击量的入口。当时的落地页链接又长又带一堆 UTM 参数,用户在微信里点开都经常被截断,更别提后期做数据分析了。
你做短链接系统,本质上是在做一个带有跳转能力的数据采集器。用户通过短码访问时,系统要做的不仅仅是 302 跳转到目标地址,还要在跳转前后完成埋点、拦截、统计、缓存等一系列动作。这也是短链接系统和普通重定向服务最大的区别。
这篇文章我会结合自己做短链接系统的完整过程,讲清楚核心链路、表结构设计、跳转流程、防并发踩坑、数据统计方案以及上线后的安全防护。内容偏实战,适合已经有一定后端基础、想独立搭建一套短链接服务的开发者参考。
有一点先说明:短链接看起来简单,但对性能、稳定性、安全性都有隐性要求。如果你只是做个练习项目,那随便怎么玩都行;如果要上线给业务用,那 Redis 缓存、布隆过滤器、黑名单校验、统计异步化这些东西一个都不能省。
2. 需求拆解:短链接系统的核心功能边界
在动手写代码之前,先把功能边界理清楚。我见过不少半途而废的短链接项目,基本都是因为一开始没想清楚到底要做到什么程度,结果写着写着发现工作量远超预期。
2.1 必须具备的核心功能
一个能用的短链接系统,至少需要以下四个能力:
- 生成短链接:接收原始长链接,返回短码。短码可以是纯数字、Base62 字符串或随机哈希。
- 跳转还原:用户访问短链接时,解析短码并 302 到原始地址。
- 数据统计:记录每次访问的 IP、User-Agent、Referer、访问时间等信息,支撑后续的 PV/UV 分析。
- 过期管理:短链接需要有有效期概念,比如 7 天有效、30 天有效或永久有效,过期后自动失效。
这四点是底线。如果你问我能不能砍掉统计功能先上线,我的建议是:不要砍。统计是整个系统的灵魂,没有统计能力的短链接就是一个普通跳转工具,连“系统”都算不上。
2.2 可选但强烈建议做的进阶功能
这是在核心功能之上根据业务需要扩展出来的,按优先级排列:
- 自定义短链:让用户指定短码,比如
/activity、/sale2024。这个对运营非常有用,因为自定义短码可读性强、方便记忆。 - 域名分组:同一套系统服务多个业务线时,需要区分不同品牌域名的短链,便于独立统计和独立管理。
- 访问限制:按 IP、地域、设备或时间段做访问控制。比如只允许中国大陆访问,或者只允许微信内打开。
- 安全检测:对原始链接做黑名单校验,防止短链被滥用为钓鱼、诈骗或违规内容的跳板。
- 二维码生成:很多线下场景需要把短链接转成二维码,方便物料印刷和扫码跳转。
我用下来最大的体感是:自定义短链和访问限制这两个功能,在真实业务中的使用频率远超预期。尤其是访问限制,在风控场景下几乎是刚需。当然这些功能不一定要在第一版做,但要提前想好表结构是否能兼容,避免后期大改。
2.3 系统边界要明确
务必要和业务方确认两个边界问题:
- 短链接服务是否需要支持账号体系和权限管理?如果需要多人协同运营,就必须引入组织、成员、角色等概念,复杂度会明显上升。
- 短链接的管理端是给内部运营用,还是面向 C 端用户开放?如果开放给 C 端用户,就需要做注册、登录、配额限制、内容审核等,这就是一个完整 SaaS 产品的量级了。
我第一版做的时候就明确只做内部使用,不做注册体系,这样能把精力聚焦在短链接的核心链路上。这个取舍很重要——做系统不是功能越多越好,而是能在可控成本内解决核心问题,并且给后续扩展留好余地。
3. 核心技术选型与数据表设计
短链接系统的技术选型,关键点不在用了什么框架,而在于跳转路径要短、存储查询要快、数据量扩展要容易。我这边选的是 Go + Redis + MySQL 的组合,如果你更熟悉 Java 或 Node.js,完全不影响,核心思路是一模一样的。
3.1 为什么用 Redis 做缓存层
短链接的访问特征是读多写少,而且热点极其集中。比如一个活动短链上线当天,短时间内可能涌入几十万次点击。这种场景下,如果每次都查 MySQL,数据库很快就会被冲垮。
我的方案是Redis 作为一级缓存,MySQL 作为持久化存储。短链接生成时直接写入 Redis 并设置过期时间,跳转时先查缓存,命中就直接返回目标地址;缓存未命中才回源查库,查到后再回填缓存。
这里有一个非常重要的细节:短码和原始链接的映射缓存,过期时间必须比短链接本身的有效期短,并且要留出余量。比如短链接有效期 30 天,那缓存过期时间建议设置为 30 天减 5 分钟,避免缓存恰好和有效期同时失效时出现一瞬间的回源风暴。
3.2 短码生成方案的取舍
短码生成的常见方案有三种,我列个表直接对比:
| 方案 | 生成方式 | 优点 | 缺点 |
|---|---|---|---|
| 随机哈希截取 | 对原始链接做 MD5/SHA 后截取片段 | 实现简单 | 存在碰撞概率,需要碰撞检测与重试 |
| Base62 自增 ID 编码 | 数据库自增主键转 Base62 编码 | 无碰撞,短码长度稳定 | 依赖发号器,需要额外维护 ID 生成策略 |
| 预生成短码池 | 提前生成一批随机短码,使用时取用 | 速度快,可控性强 | 需要维护池子,短码可能被占用 |
我最早用的是随机哈希截取 + 碰撞重试,后来发现并发量上来后碰撞概率不低,加上无法保证短码长度一致,就换成了Base62 自增编码。
Base62 编码就是把一个十进制数字转换成 0-9、a-z、A-Z 组成的字符串。比如数字123456转成 Base62 后可能变成w7e这类的短串。这种方案的好处是短码绝对不重复,因为数据库主键本身就是递增且唯一的。
需要额外注意的是发号器的选择。如果用 MySQL 自增主键,生成速度受限于数据库写入;如果并发量很高,建议提前做一个内存发号器或使用 Redis INCR 命令生成 ID 区间,减少数据库负担。
下面我给出一个基于 Redis INCR 的 ID 生成示例,先分段拿号,再在内存中转换成短码:
func generateShortCode() (string, error) { // 从 Redis 批量获取一段 ID 区间,每次 1000 个 current, err := redisClient.IncrBy(ctx, "short_link_id_segment", 1).Result() if err != nil { return "", err } // current 的取值是 1,2,3... // 先把自增值转成 int64,再编码为 Base62 字符串 id := current return base62Encode(id), nil }这段代码逻辑上有简化,实际生产环境一般会先向 Redis 申请一个较大的 ID 段(比如 10000 个),缓存在本地内存中逐步分配,用完了再去申请下一段。这样做的好处是减少对 Redis 的频繁请求,也能扛住更高并发。
3.3 数据库表结构:一张核心表 + 一张统计表
我一开始只设计了一张short_link表,后来发现统计数据查询越来越慢,才意识到统计数据和短链接主数据混在一张表里是大忌。最后拆成了两张表。
主表short_link核心字段如下:
CREATE TABLE `short_link` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `short_code` varchar(16) NOT NULL COMMENT '短码,例如 abc123', `original_url` varchar(2048) NOT NULL COMMENT '原始长链接', `domain` varchar(64) NOT NULL DEFAULT '' COMMENT '所属域名分组', `expire_at` datetime NOT NULL COMMENT '过期时间', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态 1生效 0禁用', `description` varchar(255) DEFAULT '' COMMENT '备注名称', `creator` varchar(64) DEFAULT '' COMMENT '创建人', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_short_code` (`short_code`), KEY `idx_original_url_hash` (`original_url_hash`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里我额外加了一个original_url_hash字段,用于快速判断两个长链接是否已存在。因为长链接本身可能长达几千字符,无法建索引,用哈希字段做索引就非常方便。哈希算法可以用 MD5 或 SHA256,注意这里只是用于查询定位,不做安全用途,所以不需要加盐。
统计表link_visit_log核心字段如下:
CREATE TABLE `link_visit_log` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `short_code` varchar(16) NOT NULL, `visit_time` datetime NOT NULL, `ip` varchar(64) DEFAULT '', `user_agent` varchar(512) DEFAULT '', `referer` varchar(512) DEFAULT '', `device_type` tinyint(4) DEFAULT '0' COMMENT '0未知 1PC 2移动端', `country` varchar(64) DEFAULT '', `province` varchar(64) DEFAULT '', `city` varchar(64) DEFAULT '', PRIMARY KEY (`id`), KEY `idx_short_code_time` (`short_code`, `visit_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;统计表的核心设计原则是:每次访问都要记录一行。虽然数据量增长很快,但这是后续做任何分析的数据基础。如果担心存储成本,可以定期归档到数仓或冷存储,但千万别直接不记录。
3.4 要不要用布隆过滤器
当短链接数量达到百万甚至千万级别时,用户访问一个不存在的短码,系统需要判断“这个短码是否存在”。如果直接查 Redis,每个不存在的短码都会引发一次缓存穿透,全部打到 MySQL,数据库压力很大。
解决方案是用布隆过滤器预判短码是否存在。布隆过滤器的特点是:判断“不存在”一定准确,判断“存在”可能有误判。把全量短码预先加载进布隆过滤器,当请求到达时先查询布隆过滤器,如果返回不存在,就直接返回 404,不再访问 Redis 和 MySQL;如果返回存在,再走正常的查询链路。
布隆过滤器在 Go 中的实现可以选择 bloom 库,也可以直接用 Redis 的BF.EXISTS命令。需要注意一点:布隆过滤器需要在系统启动时构建,并且要定期更新,保证新增的短码能被识别。
4. 跳转链路的完整实现与性能优化
短链接系统的核心链路就是:用户访问短链 → 短码解析 → 查询目标链接 → 302 跳转。这条链路每增加一个毫秒,对用户体验的影响都是实打实的。我自己优化这块时踩了不少坑,写出来给大家避雷。
4.1 302 还是 301,这是个关键决定
短链接跳转状态码有两种选择:301 永久重定向和 302 临时重定向。
- 使用 301,浏览器和 CDN 会缓存重定向结果,后续访问不会请求短链接服务端,服务端压力小。
- 使用 302,每次访问都会请求服务端,可以实时统计每次点击。
我强烈建议使用302。原因很简单——如果使用 301,用户第一次点击后被浏览器缓存,后续就不再请求你的服务,统计数据会严重失真。做短链接系统如果统计失真,那这系统就废了一大半。服务端压力可以通过缓存和扩容解决,数据缺失却是无法弥补的。
4.2 跳转接口核心代码示例
下面给出一个带缓存和回源逻辑的跳转接口实现,这是整个系统最关键的一环:
func redirectHandler(w http.ResponseWriter, r *http.Request) { // 从路径中提取短码,例如 /abc123 中的 abc123 shortCode := strings.TrimPrefix(r.URL.Path, "/") if shortCode == "" { http.NotFound(w, r) return } // 第一步:查询缓存 originalURL, err := cache.Get(ctx, "short_link:"+shortCode).Result() if err == redis.Nil { // 缓存未命中,查 MySQL originalURL, err = queryOriginalURL(shortCode) if err != nil { if errors.Is(err, ErrLinkNotFound) { http.NotFound(w, r) return } // 数据库异常,返回 500 但不把错误信息暴露给客户端 http.Error(w, "internal error", http.StatusInternalServerError) return } // 回填缓存,注意设置合适的过期时间 cache.Set(ctx, "short_link:"+shortCode, originalURL, 24*time.Hour) } else if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } // 第二步:异步记录访问日志 go recordVisitLog(shortCode, r) // 第三步:302 跳转,不能使用 301,原因上文已说明 http.Redirect(w, r, originalURL, http.StatusFound) }这段代码做完之后,单机就能扛住很高的 QPS,瓶颈基本都在 Redis 和网络带宽上。实际生产环境还会加一层 Nginx 负载均衡,部署多个实例横向扩容。
4.3 缓存穿透、击穿、雪崩三个坑的应对
这三个问题是高并发系统绕不开的坎,短链接系统尤其明显。
缓存穿透:指请求一个不存在的短码,缓存查不到,数据库也查不到。大量这样的请求会直接打满数据库。解决思路就是我上面提的布隆过滤器,配合返回空值缓存(对不存在的短码也缓存一个标记,比如short_link:missing:abc,过期时间设短一些)。
缓存击穿:指某个热点短码的缓存恰好过期,此时大量请求同时涌入,全部回源查库。解决思路是加锁或使用singleflight机制,保证同一个短码只有一个请求去查库,其余请求等待结果。
下面是 singleflight 的简单示例:
var sf singleflight.Group func queryOriginalURL(shortCode string) (string, error) { result, err, _ := sf.Do(shortCode, func() (interface{}, error) { // 此处真正查询数据库 return queryFromDB(shortCode) }) if err != nil { return "", err } return result.(string), nil }这样处理后,即使缓存过期瞬间并发 10000 个请求,也只有一个请求真正去查库,其余全部复用查询结果。
缓存雪崩:指大量短码的缓存同时过期,导致数据库瞬间压力暴增。应对策略是为不同短码设置不同的过期时间,在基础过期时间上增加一个随机偏移量,比如24h + rand.Int(30)分钟。这一条我在代码里一直被要求遵守,是真的有用。
4.4 压缩跳转路径的细节
除了代码逻辑,网络路径也是延迟的重要来源。我在这里说几个实操细节:
- 域名解析:为短链接服务配置独立的短域名,DNS 解析记录尽量使用 TTL 较小的值,方便后续切换 IP。
- CDN 加速:如果用户群体是全国范围,可以在短链接服务前套一层 CDN,CDN 节点缓存 302 响应。不过要注意,CDN 对 302 的缓存时间要控制在 30 秒左右,否则统计时效性会受影响。
- HTTPS:强制启用 HTTPS,防止跳转过程中链接被中间人篡改。短链变长链的过程中如果被插入广告或跳转到钓鱼页面,后果非常严重。
- HTTP/2 与连接复用:确保网关和反向代理启用 HTTP/2,多个请求复用同一个 TCP 连接,减少握手开销。
5. 数据统计体系:从点击记录到可视化报表
短链接系统的数据统计,最基础的有两个指标:PV和UV。PV 是访问次数,UV 是独立访客数。UV 的识别通常靠 IP + User-Agent 组合或独立的 Cookie ID 来近似判断。
5.1 统计数据的采集与落库
采集环节我采用的是异步写入。在跳转接口里,通过go recordVisitLog(...)把日志写入消息队列(比如 Kafka 或 Redis 的列表),再由消费端批量写入 MySQL。这样跳转接口不用等数据库写完就能返回,用户体验更好,也不容易出现日志写入拖垮主链路的问题。
如果项目规模不大,直接用 Redis List 做一个简单的队列也能应付:
func recordVisitLog(shortCode string, r *http.Request) { // 解析 IP、UA、Referer 等信息 logData := map[string]interface{}{ "short_code": shortCode, "ip": getClientIP(r), "user_agent": r.UserAgent(), "referer": r.Referer(), "visit_time": time.Now(), } // 写入 Redis 队列,由后台任务消费写入 MySQL data, _ := json.Marshal(logData) redisClient.LPush(ctx, "visit_log_queue", data) }后台消费者定期从队列取数据批量写入,比如每 5 秒刷一次,或者攒够 100 条刷一次,具体看数据量。
5.2 IP 归属地解析的取舍
做地域统计时需要对 IP 进行归属地解析。这里有两种方案:
- 使用本地 IP 库(如 ip2region),部署简单、速度快、无外部依赖。
- 调用在线 IP 解析 API,数据新但依赖第三方服务,且可能产生额外费用。
我建议第一版先用本地 IP 库,把 IP 解析成国家、省份、城市,落库到link_visit_log表中。解析动作要放在异步消费者里,不能在跳转主链路中同步执行,因为 IP 库查询即使是毫秒级,在超高并发下也会放大成明显的延迟。
5.3 统计报表的查询设计
统计报表的查询基本都围绕short_code + 时间范围进行聚合。如果直接对link_visit_log表做COUNT和GROUP BY,数据量超过几百万时明显变慢。
我的做法是增加一张小时级聚合表。后台任务每小时对原始访问日志做一次聚合,按照短码、小时、省份等维度汇总 PV/UV,然后报表查询只查聚合表:
CREATE TABLE `link_visit_hourly` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `short_code` varchar(16) NOT NULL, `hour_start` datetime NOT NULL COMMENT '统计小时的起点', `province` varchar(64) DEFAULT '', `pv` int(11) NOT NULL DEFAULT '0', `uv` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uk_code_hour_province` (`short_code`, `hour_start`, `province`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;查询某个短链最近 7 天的日报,只需按天分组,扫描的数据量小好几个数量级,报表打开速度能控制在 1 秒内。
5.4 趋势数据的留存周期
访问日志的原始明细表建议只保留 30 天,聚合表可以保留 1 年以上。30 天前的原始数据如果业务上确实需要,可以导出到对象存储或数据仓库,不需要一直留在 MySQL 里。我做第一版时没考虑这个问题,结果一年后数据库表膨胀到几千万行,查询性能直线下降,后来花了很大精力才完成归档迁移。
6. 秒级定位问题:日志、监控与告警
一个上线后的短链接系统,除了功能正常,还要能快速发现问题。我总结一下自己常用的排障思路。
6.1 三处日志缺一不可
第一处是访问日志,就是用户每一次点击短链产生的原始记录。排查“某个用户反映链接打不开”这类问题时,先查访问日志看这条短链有没有被访问过。
第二处是服务日志,记录请求处理过程中的错误、慢查询、异常堆栈。排查系统报错时最依赖这部分。
第三处是跳转日志,专门记录 302 跳转的目标地址、来源地址、响应耗时。用于分析“跳转后用户流失”这类问题非常有效。
我把日志统一输出到本地的 JSON 格式文件,再通过 Filebeat 采集到 Elasticsearch,使用 Kibana 做可视化检索。不追求复杂的日志平台也没关系,即使是简单的grep + awk,能快速定位问题就够用。
6.2 核心指标告警
系统上线后至少要监控四类指标:
- 缓存命中率:正常情况下应该保持在 95% 以上。如果明显下降,说明缓存策略可能出了问题。
- 接口响应耗时 P99:短链接跳转是链路极短的操作,P99 超过 200ms 就要排查瓶颈。
- MySQL 慢查询数量:短码查询走了索引之后一般不会慢,如果出现慢查询,大概率是索引失效或被迫全表扫描。
- Redis 内存使用率:缓存越来越多,内存不设监控很容易 OOM。要关注内存淘汰策略是否合理。
告警渠道用企业微信或钉钉机器人即可,核心是告警要能触达到负责人,并且信息要足够定位问题,比如“短域名接口 P99 已超过 200ms,最近 5 分钟,峰值 QPS 8000”,这种告警一次就能让人明确下一步动作。
6.3 一次线上事故的复盘
我之前遇到过一次明显的问题:某活动短链上线后,短码查询全部成功,但用户反馈点击后页面白屏。查日志后发现跳转地址变成了一个奇怪的空参数链接。
最终定位原因是:运营在创建短链接时,原始长链接末尾带了换行符和空格,我这边存储时没有做 trim 清洗,导致 302 跳转到畸形地址。这个例子说明,用户输入的原始链接在入库前必须做完整的标准化处理,包括去空格、补协议头、校验非法字符。后来我在入库逻辑里加了一个normalizeURL()方法,专门处理这类脏数据。
这类问题不复杂,但不踩一次真的很难想到。
7. 安全防护:短链接被滥用怎么办
短链接本质上是一个跳板,天然容易被滥用为钓鱼、诈骗、违规内容的分发工具。虽然做内部系统时不一定会遇到恶意攻击,但基础防护必须有。
7.1 原始链接的安全校验
创建短链接时,对原始链接进行安全检测的流程大概分这几步:
- 协议校验:只允许 http/https,禁止其他危险协议。
- 域名黑白名单:维护一份可信域名白名单和风险域名黑名单。白名单命中直接通过,黑名单命中直接拒绝。
- 威胁情报接口:调用外部安全服务的 URL 检测接口,对未知域名做实时检测。
- 人工审核兜底:对系统无法判断的链接,标记为待审核状态,由运营人员人工确认。
这一步是真正能降低被举报和封禁概率的关键。不能省。
7.2 访问侧的频率限制与封禁
如果某个短链接被恶意刷量,会影响数据真实性,还会拖垮服务。所以要为每个 IP 设置访问频率上限,比如每秒 10 次。超过阈值后返回 429 状态码,或者直接封禁该 IP 一段时间。
实现上可以直接用 Redis 计数:
func checkRateLimit(ip string) bool { key := "rate_limit:" + ip current, _ := redisClient.Incr(ctx, key).Result() if current == 1 { redisClient.Expire(ctx, key, time.Minute) } if current > 10 { // 超过每分钟 10 次,触发限制 return false } return true }这种简单计数方案已经能挡住绝大多数异常流量。如果面对更专业的防护需求,可以考虑引入专门的限流组件,但第一版用不上。
7.3 防刷短码与 ID 混淆
短码如果使用自增 ID 编码生成,有一个潜在问题:短码是可预测的。攻击者可以从aaa、aab逐步遍历所有短码,批量抓取短链信息。
解决思路有两种:
- 自增 ID 生成后用可逆加密算法做一次编码转换,让短码和 ID 之间看起来没有关联。
- 直接使用随机短码,但需要处理碰撞重试问题。
从我的实战经验来看,自增 + 可逆混淆的方案综合成本最低,既不用担心碰撞,又难以被批量遍历。混淆算法可以选择 Base62 编码后加一个自定义字符映射表,把字符对应关系打乱。比如原始 Base62 是0-9a-zA-Z的顺序,你可以把表重排,让 ID 在视觉上无规律可循。
8. 上线前的性能压测与容量预估
短链接系统写完之后,我强烈建议做一轮压测,不要直接上线。压测的目的不是看代码能不能跑,而是搞清楚系统的天花板在哪,好确定部署规模和容量规划。
8.1 压测方案怎么设计
压测工具我习惯用 wrk 或 hey,简单直接,能快速给出并发区间内的表现数据。压测时要覆盖两个场景:
- 纯缓存命中场景:短码已经在 Redis 中,模拟正常业务下的连续访问,验证跳转接口的 QPS 上限。
- 缓存穿透场景:清空缓存后短码仍然存在,模拟热点失效瞬间的回源情况,验证数据库连接池和 singleflight 是否能兜住。
我压测过的单机配置(4 核 8G,Go 服务 + Redis + MySQL 同机部署)大概能跑到 12000 QPS 左右的短链接跳转,瓶颈在 Nginx 与网络层。如果你要更高并发,直接考虑水平扩展实例并做负载均衡。
8.2 容量规划的几个估算公式
这部分是硬核内容,直接给可用的估算方法:
假设你的业务预估每日短链点击数D,平均每个用户点击C次,则:
- 高峰期 QPS ≈
D * C / 86400 * 10(10 为高峰系数,按经验取) - 每日访问日志新增行数 ≈
D * C - Redis 内存预估 ≈
短码数量 × 200字节(短码+原始链接+缓存开销)
举个例子:预估每日点击量 100 万次,则高峰 QPS 大约在:1000000 * 1.5 / 86400 * 10 ≈ 173 QPS
这个量级单机服务完全扛得住。如果每日点击量到 1 亿次,高峰 QPS 就在 17000 左右,那就需要上多实例 + CDN + 更高规格的 Redis。
8.3 压测中发现的典型瓶颈
我压测时发现过一个很有意思的问题:MySQL 连接数成了瓶颈。原因是 Go 服务默认的连接池参数太乐观,高并发时创建了过多连接,直接把 MySQL 的连接数打满了。
解决办法是设置合理的连接池上限:
sqlDB.SetMaxOpenConns(50) sqlDB.SetMaxIdleConns(10) sqlDB.SetConnMaxLifetime(time.Hour)类似这种问题不压测根本发现不了。所以压测真的是上线前最重要的一道防线。
9. 踩过的坑与最后的实操建议
这部分算是我个人经验的附加福利。写出来给大家省点走弯路的时间。
9.1 长链接重复创建的合并问题
同一个原始链接可能被不同运营人员多次创建。如果不做任何控制,数据库里会有大量重复记录,而且短码各不相同,统计时会出现同一活动多处数据。
我的方案是:在创建接口里根据original_url_hash先查一遍,如果已存在且未过期,直接返回已有的短码;如果已存在但已过期,就需要重新生成短码并更新过期时间。这块逻辑虽然增加了一点查询成本,但对数据质量的提升非常明显。
9.2 短链接被微信拦的风险
微信对短链接的拦截规则比较严格,如果短域名没有做备案或缺少安全认证,很容易被提示“非官方网页”。这虽然不完全可控,但有两个点可以缓解:
- 使用已经通过微信校验的品牌域名,不要用纯 IP 或未备案域名。
- 目标链接也必须走 HTTPS,否则从短链跳转过去会触发浏览器的“不安全网页”警告。
9.3 兜底页面的重要性
当短码不存在、已过期或触发访问限制时,用户需要一个友好的兜底页面,而不是直接看到 404 空白页。我在生产环境配置了一个动态页面,根据不同的失败原因展示不同文案,并且放置了返回主页的入口。这个细节看起来不起眼,却直接影响了用户体验和整体完成度。
我之前在 404 页面上偷懒过,结果被产品经理找了很多次,后来老老实实做了一个兜底页,大家的口碑立刻就不一样了。
9.4 上线第一周要盯什么
新系统上线第一周,别急着加功能,先盯这四件事:
- 短码命中率与缓存命中率是否符合预期,如果不能到 90% 以上,说明缓存设计有问题。
- 是否存在明显的时间段热点,提前做好资源调配,比如晚间 8 点到 10 点通常是流量高峰。
- 数据统计报表是否正确,抽样对比手工统计的 PV/UV 和系统的统计值。
- 是否有异常的原始链接被创建,及时清理违规内容,避免被安全机构点名。
我自己上线后的第一个周末,就是从这几个维度全部过了一遍才放心。
短链接系统不是那种眼花缭乱的高大上项目,但它把网络跳转、缓存设计、数据埋点、安全防护这些基础能力全部串联起来了,非常适合作为巩固后端功力的实战项目。如果你正在做类似的系统,希望这篇内容能帮你少踩几个坑,尤其是缓存穿透、数据统计和入场校验这些环节,真的值得多花时间。