做短链接系统之前,我原以为这不过是个“长网址转短码”的小工具,顶多写个发号器加一张映射表就完事了。等真正从零开始搭完一套能扛住生产流量的短链接服务,我才发现里面藏着不少值得掰开揉碎讲的东西。短链接的核心价值从来不只是“把URL变短”,而是一个集编码算法、发号策略、存储选型、缓存设计、跳转追踪、安全防护于一体的链路系统。如果你正准备自己动手实现一套,或者正在做技术选型想摸清底细,这篇文章会把我在落地过程中验证过的方案、踩过的坑、以及不值得再走一遍的弯路都摆出来。
1. 系统结构拆解:一个短链接服务到底由哪些模块组成
先别急着写代码。短链接服务从用户点击“生成”到访客最终落地到目标页面,中间要经过至少四个核心环节。把这些环节一层层拆开,整个系统的骨架就清晰了。
1.1 核心链路:从长链接进入到最后跳转
用户把长链接粘贴进输入框,点击生成,前端请求到达后端接口。后端首先要做的不是直接存库,而是校验这个长链接的合法性——是不是一个可访问的HTTP/HTTPS地址、有没有恶意特征、是不是已经存在于系统里(存在就直接复用老短码,避免数据膨胀)。校验通过后,系统分配一个短码,把“短码与长链接的映射关系”写入存储,返回完整的短链接给用户。
当访客在浏览器里打开这个短链接时,DNS解析落到你的服务上,短码从URL路径中被提取出来(例如https://your.domain/xk9f2p中的xk9f2p),系统拿着这个短码去缓存里查映射。命中就直接返回302 Location跳转;没命中就穿透到数据库查询,查到之后再回填缓存。整个过程从请求进来,到浏览器收到跳转指令,理想状态下应该控制在10毫秒以内。
1.2 三大核心子系统:生成、跳转、统计
把链路摊开看,系统本质上由三个子系统组成。
生成子系统负责短码的分配与持久化。这个模块的核心难点不在“怎么写接口”,而在“如何保证短码唯一且高效”。后面我会专门讲发号器设计。
跳转子系统负责短码到长链接的解析与重定向。这个模块最容易被低估,因为看起来就是“查表->重定向”两件事。但真实场景里要考虑缓存击穿、缓存雪崩、单短码热点、恶意遍历短码、失效短码的快速判断,每一样都需要单独设计。
统计子系统负责记录每一次点击的来源、IP、设备、时间。短链接系统在企业场景里往往不是单纯“缩短URL”,而是渠道追踪工具。投放人员要看某个渠道带来多少点击,所以统计模块的可扩展性从一开始就要留好。
我在实际项目中见过不少失败的方案,最典型的是把统计埋点直接写在跳转接口里同步落库。一旦访问量上来,数据库先被写满,跳转链路跟着一起雪崩。正确的做法是把统计事件异步化,也就是跳转接口只干一件事——查映射、回302,然后把点击事件丢进消息队列,由消费端异步落库。
1.3 技术栈选型逻辑:为什么要这样搭
短链接系统不挑技术栈,我用的是Go + Redis + MySQL的组合,这个选型理由很直白:Go的并发模型处理高并发HTTP跳转请求有天然优势,Redis承担短码映射的读缓存和发号器的原子自增,MySQL做最终的数据持久化。如果你更熟悉Java、Node.js或者Python,同样能搭出来,核心逻辑是通用的。
关键不是语言,而是每一层承担什么职责。Redis负责热数据路径,MySQL负责冷数据存储和查询,消息队列(我用的是Kafka,体量小的也可以用RabbitMQ或者Redis Stream)负责削峰填谷。这套三层结构几乎成了短链接系统的标准解。
2. 核心算法解析:短码生成方案怎么选
短码是短链接系统的门面,用户看到的就是那一串字符。短码的生成方案直接决定了系统的并发上限、数据表结构、以及后续的维护复杂度。
2.1 自增ID + Base62编码:最稳的方案
我用的是“全局自增ID + Base62编码”方案。系统每接收到一个生成短链接的请求,就从Redis里执行INCR shortlink_id拿到一个全局自增的整数ID,然后把这个十进制整数转成62进制字符串——用0-9、a-z、A-Z共62个字符表示。结果就是短码。
举个例子:假设当前自增ID是100000,转成Base62之后是q0M(具体值取决于你的字符表顺序)。这个方案的好处极其明显——短码天然唯一,因为源头就是唯一自增ID;同时短码长度稳定可控,在数据量没到千万级之前,短码长度基本保持在6到7位,完全够用。
我在编码时用的是自定义字符表,而不是直接用现成库。原因是自定义字符表可以去掉容易混淆的字符(比如数字0和大写字母O、数字1和小写字母l),避免用户手动输入短链接时出错。这个细节在生产环境里很实用。
2.2 哈希截取方案:简单但有冲突风险
另一种常见方案是把长链接做哈希(MD5或MurmurHash),取结果的前几位作为短码。这个方案的好处是同一个长链接生成的短码是固定的,不用做“查重后复用”的逻辑;坏处是哈希截断必然存在碰撞风险。一旦两个不同的长链接哈希出同一个短码,就要做冲突检测和二次哈希,反而增加了系统的复杂度和响应延迟。
哈希方案在并发量不高的内部工具场景下能用,但作为对外开放的短链接服务,我强烈建议用发号器方案。发号器方案把“冲突”问题从源头消解了,而不是等撞上了再解决。
2.3 短码查重:绝对不能省略的一步
不管是哪种方案,短码落库之前必须做唯一性校验。我的实践是在数据库层给短码字段加唯一索引,同时在写入前先查一次Redis缓存。有人觉得“有了唯一索引,查重就交给数据库报错好了”,理论上确实如此,但生产环境里数据库唯一索引冲突抛异常会打断批量写入的节奏,而且异常日志会刷得很猛。更好的做法是“先查Redis,再写数据库,数据库唯一索引兜底”——三层防线同时存在,才能保证短码绝对唯一。
这个环节还有一个容易踩坑的细节:一旦短码查重发现已存在,前端拿到的应该还是原来的短链接,而不是报错。很多用户会重复提交同一个长链接,这种场景下直接返回已有短码,既省了存储空间,也让用户觉得系统很“智能”。
3. 存储与缓存设计:数据层要解决的关键问题
短链接系统的数据模型极度简单——一张表,两个字段(短码、长链接,再加创建时间和过期时间)。但就是这张简单的表,在高并发场景下藏着不少讲究。
3.1 数据库表结构设计与索引策略
表结构我保留最精简的版本,生产环境里加了一些企业级字段:
CREATE TABLE `short_link` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `short_code` varchar(16) NOT NULL COMMENT '短码', `long_url` varchar(2048) NOT NULL COMMENT '原始长链接', `expire_at` datetime DEFAULT NULL COMMENT '过期时间', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_short_code` (`short_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表时最需要留意的两个点:第一,short_code必须加唯一索引,这是短码唯一性的数据库兜底;第二,long_url字段长度要留够。我见过有人用varchar(255)存长链接,结果遇到带了一长串追踪参数的URL直接报错。2048是常见选择,但如果你预判会有极端场景,用TEXT类型也行,只是会牺牲一点查询性能。
过期短链接的清理我用的是定时任务 + 批量删除。每过一个小时扫描一次expire_at不为空且已经过期的记录,按主键ID分批删除,避免一次DELETE太多锁住大范围行。
3.2 Redis缓存策略:缓存什么、缓存多久、怎么更新
Redis在短链接系统里承担两个职责:短码到长链接的映射缓存,以及发号器的原子自增。
映射缓存我用的是String结构,key为shortlink:{shortCode},value为长链接。缓存过期时间设置成24小时,配合LRU淘汰策略。为什么要设过期时间而不是永久有效?因为短链接系统天然存在“热点短码”和“冷门短码”之分,老的短码热度下降后,让缓存自然过期可以减少内存占用。即便缓存过期了,请求会穿透到MySQL,数据库查到后重新回填缓存,对用户体验几乎没有影响。
回填缓存时一定要设置过期时间,而且要设置一个随机值(比如24小时加上0到5分钟的随机偏移)。如果不加随机偏移,大量短码同时过期,请求在同一个时间点全部穿透到数据库,数据库瞬间被打满,这就是经典的缓存雪崩。
3.3 多实例部署下的发号器设计
单机部署的时候,Redis的INCR命令做发号器毫无压力。但如果你做了多实例部署,就会遇到一个问题:每次生成短链接都调一次Redis,Redis挂了或者网络抖动了,生成接口会跟着不可用。
更稳的方案是“号段模式”。从Redis里一次取一批ID(比如一次取10000个),每个实例在内存里维护自己的号段,用完了再向Redis取下一批。这样Redis的访问频率直接降低三个数量级,即使Redis抖动一下,实例内存里还有几千个号可用。我实践的参数是每次取10000个号段,按当前业务量计算,一次Redis交互可以支撑很久,几乎可以忽略发号器的性能瓶颈。
号段模式还有个附加好处:可以支持批量生成短链接的场景。运营人员一次性导入一万条长链接,如果用逐条INCR+ 逐条写库,耗时非常难堪。号段模式下,一次取号、批量写库,效率完全不同。
4. 统计模块与技术实现:点击数据从哪里来
统计模块是短链接系统里最容易被“先不做”的部分。但如果你做过渠道投放场景,就会知道没有统计的短链接系统等于废了一半。投放人员不仅要知道短链接被点了几次,还要知道是谁在什么时间点、从哪个平台点进来的。
4.1 埋点设计:不影响主链路的最小侵入方案
我的做法是把统计逻辑从跳转链路中摘出去。跳转接口收到请求后只做三件事:查缓存拿到长链接、记录一个访问日志(Nginx访问日志或者独立的事件日志)、返回302。访问日志里的关键字段包括:短码、访问时间、访客IP、User-Agent、Referer、可选的自定义渠道参数。
日志产生之后,通过Filebeat之类的采集组件持续收集,发送到Kafka。下游的消费服务从Kafka拉取数据,解析User-Agent得到设备类型和浏览器类型,解析IP得到地理位置信息,最后批量写入ClickHouse或者MySQL里的统计宽表。
这么做的核心逻辑是“先保证跳转成功率,再谈统计”。把统计从同步链路里摘出来之后,即使统计系统整体挂了,短链接跳转功能依然不受影响。这是生产系统设计里非常重要的隔离思想。
4.2 PV / UV / 独立访客的计算口径
统计口径上踩过坑的人都知道,“PV和UV看着简单,定义起来全是细节”。
PV(Page View)就是短链接被访问的总次数,每一条点击日志都算一次PV。UV(Unique Visitor)需要按访客去重,我用的口径是“IP + User-Agent”组合去重,24小时内视为同一个访客。用Cookie去重更准,但短链接场景里很多点击来自微信、抖音这类App的内置浏览器,Cookie不一定能正常写入,所以IP+UA组合是更现实的方案。
还有一个容易被忽略的点:302跳转本身由搜索引擎的爬虫、微信的链接防屏蔽检测服务触发时,这些流量不是真实用户的点击,却会记录成PV。为了过滤这部分噪音,我会在统计逻辑里维护一个“已知爬虫UA清单”,匹配到的记录打上爬虫标签,不进入有效PV统计。
4.3 异步落库:消息队列扛住刷量场景
接入消息队列还有一个被很多人忽视的好处——它有天然的抗流量峰值能力。比如某个短链接被放到了App首页,瞬间涌入几千个点击请求。如果这些请求直接写数据库,数据库写入线程池会瞬间被打满。但经过Kafka缓冲之后,落库速度就完全由消费端决定,数据库始终处于“可控的写入压力”下,不会被打爆。
如果你不想引入Kafka这种重量级组件,体量在百万级以下时,用Redis的List结构做简易队列完全够用。生产端LPUSH,消费端RPOP,起一个定时任务批量处理。我在早期版本里就是这么实现的,跑了近一年没出过问题。技术选型永远不要一步到位,够用就行。
5. 安全防护与踩坑实录:那些上线之后才暴露的问题
短链接系统上线之前,我心里最没底的不是功能跑不跑得通,而是这个系统一旦对外开放,会成为恶意流量的靶子。实际上线后遇到的问题也确实集中在这个方向。
5.1 并发冲突:短码被同时请求两次
第一种高频问题不是安全攻击,而是并发写入导致的数据重复。两个请求同时携带着同一个短码进来,都去数据库插入,其中一条会触发唯一索引冲突报错。排查方式很简单——看后端日志里有没有Duplicate entry关键字的报错。
应对方案分两层:数据库层已经有唯一索引兜底,遇到冲突直接捕获异常,回查已有记录返回即可;应用层在插入前先查一次缓存,能拦下大部分重复请求。实测下来,加了应用层缓存查询后,数据库层的唯一索引冲突日志直接少了九成以上。
5.2 恶意遍历:短码被脚本批量探测
短码的字符集是有限集合(62个字符的N次方),这决定了它天然可以被枚举。黑客或者竞争对手可以写一个脚本,遍历所有可能的短码组合,请求你的跳转接口,找出所有有效短链接,再对这些链接做批量刷量或者收集信息。
应对方案是我在实际运维中逐渐补全的,目前有效手段有三个:
第一,接口限流。针对同一个IP的短链接访问频率做限制,比如一分钟内超过200次请求就弹出验证码或者直接拒绝。限流用的是Redis计数,简单有效。
第二,短码长度提到8位以上。短码每多一位,枚举空间就扩大一个量级。8位短码的枚举空间是62的8次方,约两百万亿,脚本遍历的代价大到不值得。
第三,监控异常模式。同一个IP在短时间内请求了大量不同短码,触发告警,人工确认后加入黑名单。这个手段最原始,但也是最后一道防线。
5.3 失效短码的高效判定
短链接系统都会遇到“短码不存在”的请求。最糟糕的做法是每次都查数据库,然后发现查不到。因为这种“无效请求”如果来自恶意脚本,会直接击穿缓存、压垮数据库。
我的做法是在Redis里维护一个 “短码黑名单缓存”:查缓存没命中时,再查数据库,如果数据库也没查到,就往Redis里写入一个空值或者特殊标记,过期时间设置得短一些(比如5分钟)。这样同一个不存在的短码在短时间内再次被请求时,直接命中“空缓存”,无需穿透数据库。这个做法非常基础,但能挡住大部分恶意探测的压力。
5.4 跳转安全:不允许开放重定向
安全评审里被点名的一个问题就是“开放重定向漏洞”。具体场景是攻击者构造一个短链接(系统生成的短码指向恶意外部网站),诱导用户点击,用来钓鱼或者传播恶意链接。这就是“短链接成为恶意帮凶”的情况。
我的处理方式分两步:生成短链接时,用服务端请求库(Golang的http.Client)发起一个HEAD请求,验证目标地址是否可访问,并检查域名是否命中已知恶意域名库。跳转时,在302 Location里强制要求目标地址必须是http/https协议,禁止javascript:这类非标准协议。这两个措施叠加,可以挡住绝大部分恶意场景。
还有一个细节值得分享:跳转时使用302临时重定向,而不是301永久重定向。表面上看301对SEO更友好,但301 Permanent Redirect会被浏览器缓存,意味着用户第一次访问后再次点击短链接,浏览器可能直接走本地缓存,根本不再请求你的服务器。这样你就永远失去了对这个链接的后续访问数据的掌控力。除非你确定这个短链接永不变化,否则一律用302。
6. 压测数据与性能调优经验分享
短链接系统上线后,我在本地和生产环境各做了一轮压测。压测工具用的wrk(一个轻量级HTTP压测工具),模拟真实的并发跳转场景。
6.1 压测结果:核心链路究竟能扛多大流量
压测环境是4核8G的云服务器,单实例部署,Redis和MySQL都跑在同一台机器的Docker里。用wrk发起2000个并发连接,持续压测60秒,测试接口是短链接跳转接口。
实测数据是:QPS稳定在12000左右,P99延迟在12毫秒内,没有出现超时或者错误响应。这个数据对于一个单实例短链接服务来说已经够用。如果把应用层和Redis分开部署,加上连接池调优,单实例的瓶颈还能再往上推一些。
性能瓶颈的分析很有意思——最终卡住QPS的不是应用代码,而是网络连接的文件描述符数量。Linux默认的ulimit是1024,意味着单进程最多只能同时打开1024个文件描述符,跑满后就无法接受新连接了。调优方式是执行ulimit -n 65535,同时把/etc/security/limits.conf里的 nofile 也改到65535。这个坑如果不提前踩到,上线前压测就会摸不着头脑。
6.2 Redis连接池与超时配置
通过压测发现,应用和Redis之间的连接管理对性能影响非常大。早期版本里我每次请求都新建一个Redis连接,压测直接出现大量cannot assign requested address错误——系统把所有可用的本地端口都占满了,连接无法建立。
换成Redis连接池后,问题迎刃而解。同时我把Redis的超时时间设置为“连接超时2秒,读取超时1秒”,这样极端情况下Redis不响应,请求会快速失败而不是卡住用户。更重要的是给Redis访问加了一层本地缓存(用bigcache,内存缓存库),同一个短码在几毫秒内被大量请求时,大部分请求命中本地缓存,Redis压力直线下降。
6.3 数据库连接池和批量写入调优
数据库写入侧的优化也有讲究。用Golang的GORM连接MySQL时,连接池参数直接影响写入吞吐:
sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(200) // 最大打开连接数 sqlDB.SetMaxIdleConns(50) // 最大空闲连接数 sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大复用时间SetMaxOpenConns 设置得太小会导致写入排队,太大则会增加MySQL的连接管理开销。200对我这个场景刚合适。短链接生成的批量写入场景,我用的是拼接多行VALUES插入,一次写500条,比一条一条写快了近10倍。
压测的最终结论是:在单实例部署的前提下,短链接系统的性能瓶颈几乎永远不会落在应用代码上,而是落在网络层、连接池、GC参数这些外围配置上。所以遇到性能问题先别急着优化业务逻辑,把连接池参数、内核参数、Redis配置都过一遍,往往能找到意想不到的收获。
7. 实际运维中的几个额外体会
写到这里,再分享几个我在实际维护中沉淀下来的判断,算是给后来者的一些“认知避坑”。
第一个是对缓存一致性的看法。短链接映射一旦写入,几乎永远不会变更(除非你做自定义短链编辑功能),所以不需要像普通缓存那样考虑复杂的双删策略。你只需要做到“数据库有记录,缓存迟早会补上”就行,最终一致性完全够用。别在这个简单系统里引入分布式事务或者复杂的一致性协议,那是给自己找麻烦。
第二个是对短链接系统的定位认知。在企业内部,短链接系统往往被纳入“基础工具”范畴,但它的核心价值其实在于“数据回收”。短链接是你在第三方渠道里唯一能埋下的“自己的眼睛”——用户在抖音、微博、短信里点了什么链接,带来多少次访问,都通过这些短链接回到你的系统里。所以统计能力、标签能力、渠道管理能力,值得花至少一半的精力去做。
第三个是安全防护的节奏感。安全建设不用第一天就追求“全副武装”,而是随着流量形态变化逐步加码。刚上线时做好基础限流和URL协议校验就够了;等到出现恶意遍历、刷量苗头时再补黑名单、空缓存这些手段。安全策略的演进应该是“按需加固”,而不是“一步到位”,后者往往会把前期的开发节奏拖垮。
短链接系统,论复杂程度远不如电商、社交这类大流量系统,但它是一个绝佳的“全链路练手项目”——麻雀虽小、五脏俱全,从算法到存储、从并发到安全、从业务到运维全部覆盖。把这个系统吃透,你至少能在“中等复杂度系统设计”这个级别上形成一套完整的直觉判断,这才是做这个项目最大的隐形收益。