1. 内容整体设计与思路拆解
短链这东西,听起来是个再小不过的工程。做之前我也觉得,不就是把一串长 URL 变成一个小短码嘛,能有多复杂。可真到动手写的时候才发现,一个能上线的短链系统,几乎把后端常见的套路全部串起来了:编码算法、缓存设计、数据库索引、跳转协议、安全校验、异步统计,甚至还有一点风控思维。Day04 复习到这儿,我把整套设计重新拉通了一遍,感觉不写下来是真的亏。
你在电商 App、内容平台、营销短信里点到的链接,十个里面有八个都是短链。有些短链是平台内部跳转用的,有些是分享给外部用户的。大家平时刷小红书、逛淘宝,看到的商品链接就是典型场景——正文里只能放有限内容,而真实落地页可能有几十个参数,不带短链的话链接又长又容易被截断。更别提用户对“跳转链接”天然有警惕心,什么“淘宝短链跳第三方是真的吗”这类疑问,在各大社交平台隔一阵就会冒出一次。这其实侧面说明了一件事:短链系统不仅要保证“跳得过去”,还要保证“跳得明白、跳得安全”。
所以,我把这次短链项目复习的核心目标拆成四块:一是把长链变短,这是表面功夫;二是把跳转做强,也就是高并发下还能稳定快速响应;三是把统计做全,记录谁在什么时间、用什么设备点了这个链接;四是把安全做好,避免链接被拿来钓鱼、跳转外站、或者被刷量。这四个目标环环相扣,缺一个都不能算完整的短链服务。
1.1 为什么选择“自研短链”而不是直接买服务
市面上的短链服务不少,有免费的也有收费的,功能看着也齐全。但个人项目想真正练手,自研和买服务完全是两个量级的事。买服务你只需要调用 API,拿回来一个短链直接用,省事是省事,但对内部机制一无所知。比如短码怎么生成、碰撞怎么处理、跳转状态码该用 301 还是 302、统计日志怎么设计,这些细节全部被黑盒封装了。写个人项目如果只停留在“会用”,那跟背单词只背了词形没背词义一样,等于没学。
自研短链还有一个实际收益:你可以完全掌控链路。很多短链服务会插入自己的广告页或者中间跳转页,这有时候会影响用户体验。自己做的话,从生成短码到落地页跳转,中间哪些环节需要展示提示页,哪些流量走静默 302,都由你来定。另外,自研后数据全部在自己手里,点击量、来源渠道、设备分布,想怎么分析就怎么分析,不会被第三方服务的数据口径限制住。
当然,自研也不是什么都自己做。像 Redis、MySQL、Nginx 这些基础设施,直接用开源方案就行,没必要重复造轮子。整个项目的自研核心其实只在业务层:短码生成算法、跳转控制器、统计落库逻辑、安全校验模块。把这些做透了,短链系统的骨架也就立起来了。
1.2 短码生成方案对比:自增 ID、哈希截取、预生成随机码
短码生成是整个系统的起点。表面上看就是“把一个数字/字符串变成短码”,但选什么方案,直接影响系统后续的扩展性、安全性和维护成本。我复习时对比了三种主流方案,也踩过其中的坑,在这里给各位复盘一下。
方案一:自增 ID 转 62 进制。数据库自增主键拿到一个十进制 ID,然后把这个 ID 转换成 62 进制字符串(0-9a-zA-Z 共 62 个字符),长度随 ID 增大而变长。这个方案实现最简单,ID 天然唯一,不需要额外处理碰撞,而且还能从短码反推生成顺序。缺点也很明显:短码可预测,别人可以通过遍历短码把所有公开链接都扫出来,这对某些私密链接来说是灾难。而且如果直接用数据库的自增主键,那主键本身就暴露了业务量,有点不体面。
方案二:对原始 URL 做哈希截取。用 MD5、SHA-256 之类的算法对长 URL 求哈希,然后截取前 6~8 位作为短码。这个方案的好处是,同一个长 URL 理论上会得到同一个短码,天然支持去重,省存储。坏处是哈希碰撞没法完全避免,需要查重和二次处理;截断后随机性也不够好,分布不均匀;而且面对恶意用户构造的批量长链接,哈希计算成本也不低。
方案三:预生成随机码并入库。维护一张短码池表,提前生成一批随机的短码,使用时取一个就标记为已占用。短码随机性高,不容易被遍历猜测,安全性最好。但实现复杂度最高,需要维护池子的补充逻辑,并发取码还要考虑锁竞争。
我自己最终采用的是方案一为主、方案二为辅的折中做法:数据库自增 ID 不直接对外,而是先做一次位混淆(比如乘以一个大质数再取模),再把混淆后的数字转 62 进制。这样既保持了唯一性,又让短码无法轻易被按顺序遍历——你拿到一个短码也没法推断出前一个短码指向的链接。这个做法在个人项目里性价比很高,既不用维护短码池,又能挡住 90% 的瞎扫行为。
1.3 跳转状态码选型:301 还是 302,二者差别不小
短链的跳转动作,核心是给浏览器返回一个 HTTP 重定向。但用 301 还是 302,不是随手一选的事,网上很多教程根本不提这个细节,导致不少人上线后才发现统计数据对不上、链接被“永久”缓存在了用户浏览器里。
301 是永久重定向。你访问短链,服务器返回 301,告诉浏览器“这个链接以后永远指向这个目标地址”,浏览器就会把这个映射关系缓存下来。下次再访问短链,浏览器直接跳过服务器,自己去目标地址,连请求都不发给短链服务了。这会导致一系列问题:用户第二次点击不再触发统计,因为请求到不了你的服务器;就算你后续修改了短链指向的目标地址,用户浏览器里缓存的还是旧地址,改了好几次都没生效。所以 301 基本只适合那种“一个短码绑定一个永久地址、完全不需要统计”的场景,实战里极少用。
302 是临时重定向。服务器告诉浏览器“这次先跳到这个地址,下次你还来问我”。这样每次点击都会先请求短链服务,才能跳转,统计数据自然就全了。用户第一次访问是 A 链接,你运营想改成 B 链接,也只需要改数据库里那一条记录,用户再点短链自然走到 B,没有缓存干扰。
这个选择题我在初版里就做错了。当时图省事,直接返回 301,结果统计面板里点击量比实际少了一大截,而且怎么查都查不出问题,最后用 curl 抓包才发现是浏览器把重定向缓存了。大家务必记住:短链系统默认用 302,只有在明确“链接地址永不变、不需要统计”的场景才考虑 301。
2. 核心细节解析与实操要点
把整体方案定下来之后,真正的工作才开始。短链系统最烦的不是“想不到”,而是“想到了但做不细”。这一步我按模块逐个拆解,把每个环节的关键参数和注意事项都盘了一遍。
2.1 短码生成算法的参数推导与转换过程
用自增 ID 转 62 进制这个方案,需要先把 62 进制转换彻底搞明白。别觉得这就是“进制换算”小学知识,真写起来,字符集的排序、处理 0 的方式、长度的边界条件都是有讲究的。
先定字符集。一般用这个顺序:0-9(对应十进制 0~9)、a-z(对应 10~35)、A-Z(对应 36~61)。注意,不同项目可能用不同的字符集顺序,比如有的把大小写顺序反过来,有的把部分形近字符去掉了。这个字符集本身没有绝对标准,但选定之后要全局统一,尤其是后续要做短码反解时,映射表一错就全乱了。
接下来是转换逻辑。假设数据库自增 ID 是45678901,先做一次位混淆。我这里用的混淆方式是:seed = (ID * 9301 + 49297) % 233280,这是线性同余法生成伪随机数时用的一组经典参数,放在这里能让连续 ID 的短码看起来完全不相关。当然如果你不想混淆,直接转也行,但推荐加一层,毕竟短码直接暴露自增序号的成本很低。
混淆后的数记为n,接下来循环执行:
chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" result = "" while n > 0: result = chars[n % 62] + result n = n // 62拿45678901 * 9301 + 49297举例,先算混淆值:45678901*9301 = 424860691801,再加49297得到424860741098,然后对233280取模,得到203434左右(具体值取决于参数,我这里直接给思路,你跑一下代码就拿到了)。把203434转 62 进制:
203434 / 62 = 3281,余数12,对应字符c3281 / 62 = 52,余数57,对应字符57 - 10 + 10,数一下字符集,a是第 10 个,往后数 57 位,落在F附近(具体看索引)52 / 62 = 0,余数52,对应字符Q
最终短码可能是cQF一类的三段字符串。这个过程你可以写成单元测试,用已知 ID 验证转换是否稳定。实测下来,7 位短码能覆盖 62^7 约 3.5 万亿个组合,个人项目用到天荒地老也用不完。
2.2 数据表设计:短码、原链接和统计字段怎么安排
存储层是短链的地基。表设计不合理,后面做统计、做筛选都会掣肘。我这里用 MySQL 做演示,设计了三张核心表:短链映射表、访问日志表、域名白名单表。
短链映射表的核心字段是这些:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint 自增主键 | 内部 ID,用于生成短码 |
| short_code | varchar(16) 唯一索引 | 短码,对外暴露 |
| original_url | varchar(2048) | 原始长链 |
| expire_time | datetime 可空 | 过期时间,空表示永久 |
| status | tinyint | 状态,1 正常 0 下线 2 审核中 |
| creator_id | varchar(64) 可空 | 创建者标识 |
| created_at | datetime | 创建时间 |
| updated_at | datetime | 更新时间 |
注意original_url一定要用varchar(2048)而不是varchar(255),很多长链接带了一堆查询参数,255 长度根本不够。如果空间紧张,还可以考虑用TEXT类型,但建索引要提前算好前缀长度。short_code上加唯一索引是必须的,一方面查询快,另一方面数据库层面兜底防止碰撞。
访问日志表用来记录每个短链的每一次点击:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint 自增 | 流水 ID |
| short_code | varchar(16) | 短码 |
| click_time | datetime | 点击时间 |
| ip | varchar(64) | 用户 IP,IPv6 也够存 |
| user_agent | varchar(512) | 浏览器 UA |
| referer | varchar(512) | 来源页 |
| device_type | tinyint | 设备类型,1 移动 2 PC 3 其他 |
这张表的数据量会暴涨,所以不用设太多索引,主键 + 短码索引就够了。查询统计时按short_code + click_time分组,提前建好复合索引能快不少。域名白名单表存的是允许跳转的目标域名,用于安全校验,后面会细说。
2.3 缓存设计:为什么用 Redis、怎么防止穿透和击穿
在高并发场景下,如果每个跳转请求都打到 MySQL,再好的数据库也会被拖垮。跳转这个动作的特点是读多写少,而且读的是同一个短码映射,天然适合加缓存。
我用 Redis 做缓存层,缓存 key 直接是短码,value 存序列化后的目标 URL 和过期时间。缓存读取逻辑很简单:用户访问短码,先查 Redis,查到了直接构造 302 跳转;查不到就去 MySQL 捞,捞到了回填 Redis,再跳转;MySQL 也没有就返回 404 页面。
这里有几个实操要点。一是缓存过期时间要加“随机抖动”,比如基础过期时间 24 小时,再加 0 到 6 小时内的随机值。不然所有短链在同一时间点集中过期,如果热点高,瞬间所有请求都打回源,数据库压力一下就上来了,这就是缓存雪崩的常见触发场景。
二是要防缓存穿透。恶意用户拿一堆不存在的短码来刷请求,每次都不命中缓存,直接打 MySQL,数据库早晚被问崩。我用的处理方案是:MySQL 查不到也缓存一个空值,过期时间设短一点,比如 5 分钟,这样同一批不存在短码的重复请求会被缓存挡住,不会再穿透下去。更正统的做法是引入布隆过滤器,把所有存在的短码加载进去,判断不存在就直接返回,不走缓存也不走 DB。个人项目初期用空值缓存就够,布隆过滤器等到了每日千万级访问再加不迟。
三是回填缓存的时候要注意并发问题。两个请求同时来查同一个短码,MySQL 都没命中缓存,两边都去 DB 查并回填,虽然不会出错,但白白浪费了一次 DB 查询。这里可以用 Redis 的SETNX做互斥锁,只有拿到锁的请求才允许查 DB,其余请求先短暂 sleep 再重新读缓存,能把重复回源降到最低。
2.4 跳转安全与“第三方跳转提示页”的设计思路
文章开头提到用户对“短链跳转”有顾虑,甚至有人专门发帖问“淘宝短链跳第三方是真的吗”。从技术角度看,平台在跳转前展示一个“安全提示页”或者说“中间页”,恰恰是负责任的做法。有了中间页,用户能看清楚自己即将去哪儿、由谁提供这个页面,决定权握在自己手里,而不是被一个短链蒙着眼睛拖到陌生地方。
自研短链系统想做这个能力,可以在跳转控制器里加一道校验。首先,拿到目标 URL 后解析出域名,拿这个域名到白名单表里比对。白名单命中就直接 302 跳转,不命中就返回一个风险提示 HTML 页面,上面写明目标地址,用户点击“继续访问”才真正跳转,同时记录一条“需确认跳转”的日志,便于事后审计。
这个设计还有一个好处:能防范 Open Redirect 漏洞。所谓 Open Redirect,就是短链被人利用,把用户引导到钓鱼页面。比如有人提交了一个看似正常的链接,但中间用 URL 编码、重定向嵌套等手段,最终把人带到诈骗网站。处理办法是:对目标 URL 做完整解析,提取 scheme、host、port,校验是否在允许名单里;对 URL 编码做一次解码后再解析,防止%2F%2Fevil.com这种绕过的花招;如果目标域名是 IP 地址,直接拦掉,不放过任何直连 IP 的情况。
我在项目里还加了一个“过期下线”的能力。如果某条短链被举报或自动检测判定为风险链接,管理员可以直接将status置为 0,顺便删掉 Redis 缓存。这一招在应对“短链被恶意使用”时非常管用,不需要动代码,运营同学在后台点一下按钮就能处理。
3. 实操过程与核心环节实现
理论学习再多,不动手写代码都是纸上谈兵。这一节我把整个链路从代码层面走一遍,给出可以直接参考的伪代码和核心逻辑。语言我选了 Python(个人项目开发快),但换成 Go 或 Java 思路完全一致。
3.1 生成短码的核心实现
首先生成短码的部分。我要做的是:接收原始 URL 以及可选的过期时间,生成短码并写入数据库。伪代码如下:
import time import hashlib CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" def id_to_short_code(seed: int) -> str: if seed == 0: return CHARS[0] code = [] while seed > 0: seed, rem = divmod(seed, 62) code.append(CHARS[rem]) return ''.join(reversed(code)) def obfuscate_id(raw_id: int) -> int: # 乘一个大质数再加偏移,把连续 ID 打散 return (raw_id * 9301 + 49297) % 233280 def create_short_url(original_url: str, expire_time=None): # 这里省略数据库连接细节 insert_id = insert_mapping_record(original_url, expire_time) short_code = id_to_short_code(obfuscate_id(insert_id)) update_short_code(insert_id, short_code) # 写入缓存 redis_client.setex(f"short:{short_code}", TTL, original_url) return short_code需要注意一个操作顺序:先插入记录拿到自增 ID,再算出短码,再回填短码字段。为什么不等短码算出来再插?因为短码依赖自增 ID,而自增 ID 只能由数据库生成。中途如果插入失败,短码字段留空没有关系,后续重算即可。为了不让短码字段出现 NULL,也可以在表设计时先给一个默认占位值,拿到 ID 后立刻更新。
3.2 跳转处理链路:从缓存到回源再到 302
跳转是短链系统最核心的接口,也是响应时间最敏感的环节。我的处理流程大致如下:
def redirect_handler(short_code: str): # 1. 先查 Redis cache_value = redis_client.get(f"short:{short_code}") if cache_value: if cache_value == "NULL": return "404 Not Found" # 2. 命中缓存,直接 302 return redirect_302(cache_value) # 3. 回源 MySQL record = query_mapping_record(short_code) if not record: # 4. 空值缓存,防止穿透 redis_client.setex(f"short:{short_code}", 300, "NULL") return "404 Not Found" # 5. 检查状态与过期时间 if record["status"] != 1 or is_expired(record["expire_time"]): return "410 Gone" # 6. 回填缓存,设置随机过期时间 ttl = 86400 + random.randint(0, 21600) redis_client.setex(f"short:{short_code}", ttl, record["original_url"]) # 7. 异步记录访问日志 async_log_click(short_code, request_ip, user_agent) # 8. 安全校验,判断是否直接跳转 if is_domain_in_whitelist(record["original_url"]): return redirect_302(record["original_url"]) else: return render_redirect_confirm_page(record["original_url"])这里有个容易被忽略的细节:空值缓存的 key 一定要和正常缓存的 key 区分开,或者在 value 里做标记,否则下次拿到“NULL”这个字符串会当成要跳转的地址,直接给用户返回一个试图跳转到/NULL的非法响应。
异步记录访问日志我用了简单的方式:把点击事件扔进 Redis 的 List 队列,后台再起一个 worker 批量消费并写入 MySQL。这样一个请求不需要等待数据库写入完成,响应时间能压得更低。如果怕日志丢失,可以对 List 做持久化,或者直接用消息队列代替。
3.3 统计闭环:日志聚合和展示维度
统计日志只有聚合起来看才有意义。我每天凌晨跑一个定时任务,把昨天的访问日志按短码、小时、设备类型做聚合,写入统计结果表。SQL 大致是:
INSERT INTO click_stat (short_code, stat_date, hour, device_type, cnt) SELECT short_code, DATE(click_time), HOUR(click_time), device_type, COUNT(*) FROM click_log WHERE click_time >= '2025-01-01 00:00:00' AND click_time < '2025-01-02 00:00:00' GROUP BY short_code, DATE(click_time), HOUR(click_time), device_type;统计结果表单独存在的意义是,前端展示的时候不需要再去扫全量日志表。一个短链如果被扫了几百万次,日志表重得不得了,每次打开详情页都实时聚合,数据库也扛不住。日级聚合的延迟虽然有点久,但对于大部分营销链路分析场景已经足够。
展示面板里我最关注的几个维度是:总点击量、独立访客数(按 IP 去重或按 Cookie 去重)、点击时间分布(24 小时热力图)、设备占比、来源 Referer Top10。这套看板做完之后,运营就能直观了解到某个活动链接在哪个时段、哪类设备上表现最好,对投放决策有实际参考价值。
3.4 部署环境与容量估算参考
短链服务部署上,我用的是一套非常经典的组合:Nginx 做流量入口和静态资源处理,后端服务跑在 Docker 容器里,MySQL 存映射和日志,Redis 做缓存和队列。Docker Compose 可以一键起整套服务,开发环境和生产环境行为保持一致,省去很多环境不一致导致的头疼问题。
容量估算方面,我给自己定的基准是:系统每天新增 10 万个短链,单条短链平均被访问 100 次。那么一年下来,短链映射表大约 3650 万条,日志表大约 36.5 亿条。映射表单表完全扛得住,但日志表必须分表或按天分区,否则查询回来越来越慢。缓存按 30 天有效计算,假设活跃短链 3000 万条,每条缓存占 200 字节左右,Redis 需要预留 6GB 左右的内存,实例规格不能低于这个数。如果你拿这些参数做压测,就能提前知道自己的机器水位,不至于线上撑爆了才反应过来。
4. 常见问题与排查技巧实录
短链系统写出来容易,真正稳定跑起来,问题一个接一个。我把实际开发中踩过并且修复过的典型问题整理成清单,供你对照排查。这些问题在官方文档里基本找不到现成答案,都是“掉过坑才知道”的类型。
4.1 短码碰撞问题:日志里出现奇奇怪怪的 404
短码碰撞的本质是“两条不同的原链接生成了同一个短码”。如果用了自增 ID 方案,理论是不会碰撞的,但一旦混淆参数设得不合理,或者回填短码的逻辑在并发下有覆盖风险,就可能出现。我在本地压测时遇到过:连续插入两条记录,都拿到了短码更新语句的相同值,后一条把前一条的 short_code 覆盖了。排查方式是先看数据库有没有重复索引报错,再用日志关联创建时间和短码,基本能定位到是回填逻辑缺了条件。
解决办法是在short_code上建唯一索引,同时更新语句加上WHERE short_code = ''或WHERE short_code IS NULL这样的条件,防止重复更新。万一出现碰撞,业务上可以临时生成一个随机后缀拼在短码后面。
提示:短码的生成逻辑写完后,一定要做一次并发单元测试,并发插入 1000 条记录,看有没有唯一索引冲突。这个测试能省掉你后面很多排查时间。
4.2 恶意刷量:如何区分“真流量”和“假点击”
上线没多久,我就发现某条短链的点击量一夜之间暴涨了十几万,一看 IP 全是同一个段,很明显是被脚本刷了。刷量不仅污染统计结果,还会白白消耗数据库和带宽资源。
我的应对手段有三层。第一层是 IP 频率限制:同一个 IP 在 1 分钟内最多访问 100 次短链,超出直接返回 429,并在 Redis 里记录违规 IP。第二层是 User-Agent 检测:识别常见的爬虫库特征,比如 Python-requests、curl 的 UA 指纹,对这些请求返回正常的 302,但打上“疑似机器”标签,不计入正常的点击统计。第三层是对外提供签名的短链,只有带合法签名的请求才被统计进入核心指标,不过这个做起来偏重,适合有更高安全诉求的场景。
4.3 302 vs 301 导致的统计差异
这个问题前面提过,但值得再强调一次,因为它太隐蔽了。如果你用了 301,浏览器会把跳转结果永久缓存,后续点击根本不会请求短链服务器。你的服务端日志上可能显示一分钟前刚有人访问过某个短链,可实际上用户一小时内点了十次,只有第一次到了你这里。
排查技巧是:用浏览器的无痕窗口访问短链,打开开发者工具看网络请求,如果看到状态码是301 Moved Permanently,那就说明你的跳转写成了永久重定向。把代码里的 301 改成 302,再清一次浏览器缓存,统计数据就恢复正常了。
4.4 链接被用于钓鱼:必须做域名白名单和内容检测
短链天生容易被滥用,因为它把真实地址藏起来了。别人拿你的短链去钓鱼,跳转到仿冒登录页,用户被骗后只会怪这个短链平台。所以在上线之前,我把目标域名的检测做好了:创建短链时,解析目标 URL 的域名,跟一份常见的恶意域名黑名单比对;黑名单命中的直接拒绝创建。同时,对目标域名不在白名单里的链接,跳转时都展示中间确认页,宁可牺牲一点转化率,也不能让用户无感知地被带走。
淘宝、小红书这类平台在跳转第三方链接前的那个“安全提示页”,本质上就是同一套思路。它不是为了恶心用户,而是在短链的“隐藏性”和用户“知情权”之间找一个平衡点。自研系统如果要做大,这个能力必须尽早纳入。
4.5 Redis 缓存持续穿透:布隆过滤器到底该不该上
前面说过,空值缓存能解决大部分穿透问题,但它的缺陷是:每次查一个不存在的短码,都会先占用一份缓存空间,而且存在短码字典量很大的时候,空值缓存的数量会非常可观。布隆过滤器的做法是启动时把所有合法短码加载到一个 bitmap 中,判断“一定不存在”直接返回,判断“可能存在”再去查缓存和 DB。它的问题是存在误判率,会把少部分非法请求放进真实查询链路,但整体性能远优于空值缓存。
我的建议是:初期用空值缓存,等合法短码数量超过百万级别,且恶意请求占比确实影响了 DB 健康度,再引入布隆过滤器。加布隆过滤器要注意一点:当新增短码时,除了写 DB,别忘了同步把短码加入 bitmap,不然新生成的短链会被自己的过滤器挡住,那乐子就大了。
4.6 短链过期时间到了,Redis 和 MySQL 数据不一致怎么办
我给部分短链设置了过期时间,比如 7 天后失效。第一个版本里只在跳转时判断expire_time,过了时间就返回 410,但 Redis 缓存还在,导致每次都要查 MySQL 才能知道到底过没过期。后来做了优化:写入缓存时,TTL 等于expire_time - now,让缓存自动过期,回源后继续判断过期状态。
但这里有个坑:如果用户修改了短链的过期时间,比如把 7 天改成了 30 天,Redis 里旧 TTL 不会自动更新,还是按 7 天过期。解决方式是修改短链时主动删除缓存,下次访问重新回源。这个动作很容易漏,我就在后台管理系统里漏过一次,结果改了过期时间后,用户访问短链直接 410,排查了半天才发现是缓存没删。
5. 复习 Day04 的一点个人体会
整个项目复习到第四天,短链这个项目算是走完了一个从 0 到 1 的闭环。坦白讲,短链系统的代码量不算大,但它的价值在于把太多后端基础知识浓缩在一个小场景里:进制转换让你重新理解了编码;缓存穿透和击穿让你第一次真正关心 Redis 底层;301 和 302 的区别让你意识到 HTTP 协议不是背概念,而是会影响线上行为的硬规则。就连简单的“短链跳转提示页”,背后都藏着对用户信任的理解——大家不是不信任短链接,而是不想被一个看不见的地址带到未知的地方。
如果你也想拿这个项目练手,我的建议是别急着抄代码,先自己画一画链路图,把请求进来之后每一步要做什么写清楚,再动手实现。哪怕画得丑,也比直接看别人的源码有收获。遇到问题就打开日志一层层追,短链系统因为链路短,排查起来相对容易,特别适合用来建立“先怀疑缓存、再怀疑 DB、最后怀疑代码”的排查直觉。
最后再分享一个小技巧:给短码生成逻辑加上单元测试,但有时间的话,尽量用真实浏览器的无痕窗口去验证一遍完整跳转流程。自动化测试能保证逻辑正确,但只有真实浏览器访问,你才能感受到 302 跳转的丝滑、中间确认页的体验、以及慢网络下缓存未命中的卡顿。这些“体感”层面的东西,才是把一个个人项目从“能跑”推向“能用”的关键。