资讯动态

从零实现高可用短链接系统:短码生成、缓存穿透与统计埋点实战

发布时间:2026/9/26 11:41:01 来源:尧图企业网站定制
废话不多说这次聊个很多人问过我的东西短链接系统。你可能觉得它就是个把长链接变短的小工具没什么技术含量。但你真自己去从零搭一套试试就会明白一个能扛住流量、能统计、能防滥用的短链接服务涉及的东西远比想象的多短码怎么生成、存储怎么设计、跳转走 301 还是 302、统计怎么埋点、碰撞怎么处理、恶意请求怎么防。这篇文章我会按我实际做这套系统的思路从模块拆解到代码实现再到上线后遇到的真实坑完整过一遍。适合那些打算自己写一个短链接服务、或者在公司内部搭一套带统计功能的短链中间件的开发者也适合刚接触后端架构、想看看一个完整项目怎么落地的小伙伴。1. 整体链路设计与核心模块拆解短链接系统的核心链路其实特别简单一句话就能说清用户拿一个短码来访问服务端把短码还原成原始长链接然后让浏览器跳转过去。但在这个简单链路的背后每一环都有可以深入做的东西。1.1 一个请求从发起到落地经历了什么我先把整个链路的请求流程画个文字版方便对照着看后面的模块拆解用户在网页或 App 上提交一条原始长链接比如一篇文章的 URL。短链接服务接收请求校验这个 URL 是否合法是否在屏蔽名单内。通过一个短码生成算法得到一串唯一短码并把这个映射关系写入存储。服务把完整短链接返回给客户端比如https://your.domain/xK9f2p。用户把这个短链分享出去别人点击时请求到达短链接服务。服务根据路径中的短码去存储里找到对应的原始 URL。服务下发 302 或 301 重定向响应浏览器自动跳到长链接。在这个过程中异步记录点击日志谁点的、什么时间、哪个 UA、从哪个页面来的。你会发现这个系统本质上就是一个键值映射服务但因为它处于整个互联网访问链路中所以对性能、稳定性、数据准确性都有要求。如果只是单机玩那随便写写就行但一旦要面对线上流量就必须好好设计。1.2 为什么不能图省事把全链路做成同步串行可能有人会想既然链路这么短直接同步处理不就完了记录点击日志的时候在跳转响应之前先写一条数据库然后才返回 302这样数据就一点不丢了。思路没问题但代价是点击延迟变高。短链的访问往往是突发性的比如一个营销活动发出去几十分钟内几十万次点击如果每次点击都同步写库数据库压力非常大同时用户跳转也会变慢。我当时的做法是把请求路径和统计路径拆开核心查询走缓存能扛住高并发点击统计走异步消息或异步任务队列先返回跳转再慢慢把日志落库。请求路径用 Redis 或本地缓存顶着统计落库在后台批量完成。这样用户在点击时体感没有变化日志数据也不会丢。1.3 系统模块划分我从项目一开始就把系统拆成了五个模块边界很清晰后续做扩展也很省事短码生成模块负责生成全局唯一短码并保证碰撞概率可控。存储模块负责短码与长链接映射的增删改查支持缓存加速。跳转模块负责处理前台访问请求完成短码解析和重定向。统计模块负责记录点击日志维护来源、UV、PV 等维度的统计数据。管理模块负责链接的创建、过期时间设置、屏蔽名单管理、数据查询接口。2. 短码生成方案选型与存储设计短码怎么生成是整个系统里最有技术含量、也最容易出问题的地方。短码的长度、字符集、碰撞处理方式直接决定了系统的上限和实现的复杂度。2.1 短码生成的三种主流方案对比在主流的做法中短码生成方案基本可以归为三类哈希截取法、发号器法、随机字符碰撞检测法。我分别说下它们的思路和坑。哈希截取法就是拿原始 URL 做 MD5 或 SHA1 哈希然后截取其中一段作为短码。好处是不需要全局协调编码简单坏处是存在碰撞概率且不可控一旦两个 URL 哈希截出来的片段相同后写入的数据会覆盖先写入的数据。解决碰撞要么做短码冲突检测后重新截取片段要么在存储里加原始 URL 的唯一索引通过查询去重这些都是额外逻辑。发号器法就是维护一个全局自增序列每次创建一个链接就取一个号然后用进制转换把这个号转成短码。比如用 62 进制26 个字母大小写加 10 个数字表示数字数值越小短码越短。这个方案的优点是绝对不碰撞、短码短、性能高缺点是需要保证发号器的高可用和顺序。常见的发号器实现有数据库自增 ID、Redis INCR、Snowflake 算法、以及美团 Leaf 那种分段发号方案。我做的时候选了数据库自增理由是公司内部量级不大单库单表足够支撑千万级别短码部署和运维成本很低。随机字符碰撞检测法就是直接随机生成长度为 N 的字符串写入之前先查重冲突就换一个再试。这个方案实现起来最简单但随机字符串没有信息量随着数据量增大碰撞概率快速上升查询次数也会增加。我最终选择的是分段发号器预取一批 ID 到本地内存用完了再去数据库取下一批。这样数据库的压力分散到各实例还能保证全局不重复。如果你不确定自己该选哪种我给一个参考量级在百万以下且不想引入额外组件直接选数据库自增量级在千万以上且对短码长度敏感用分段发号器如果是离线清洗场景下的内部分析哈希截取法也能凑合用。2.2 存储设计一张表解决映射再加一张表解决统计存储这块我觉得没必要搞复杂两张核心表就够。第一张是链接映射表字段我设计成了这几个CREATE TABLE t_short_link ( id BIGINT AUTO_INCREMENT PRIMARY KEY, short_code VARCHAR(16) NOT NULL COMMENT 短码, original_url VARCHAR(2048) NOT NULL COMMENT 原始URL, expire_time DATETIME DEFAULT NULL COMMENT 过期时间NULL表示永久, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_short_code (short_code), KEY idx_created_at (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个细节容易踩坑。第一个是original_url字段别用太短的长度有些长链接带着一堆参数几百个字符很正常。第二个是short_code一定要建立唯一索引这不仅是查询需求也是防止短码重复的最底层兜底手段。第二张是点击统计表我的设计如下CREATE TABLE t_click_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, short_code VARCHAR(16) NOT NULL COMMENT 短码, ip VARCHAR(64) DEFAULT NULL, user_agent VARCHAR(512) DEFAULT NULL, referer VARCHAR(512) DEFAULT NULL, click_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_short_code_time (short_code, click_time), KEY idx_short_code (short_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;看到这里可能有细心的同学会问点击日志里的同一秒点击怎么算这就是我说统计要另外设计的第二个原因如果同一短码在同一秒内有大量点击单独一条日志记录一次统计维度会很乱。所以我建议在日志表里同时维护一个cnt计数字段按“短码 秒级时间戳”聚合这样既不会出现唯一索引冲突也天然做了批量写入的合并。2.3 缓存与持久化的配合策略短链查询是典型的读多写少场景。绝大多数短链创建一次后会被反复点击查询而创建频率远低于查询频率。所以我在短码解析链路里加了一层 Redis 缓存缓存 key 设计为short:link:{code}value 存储长链接 JSON。逻辑如下先查 Redis命中直接返回。未命中则查数据库查到了回填缓存设置过期时间。数据库也查不到直接返回 404。这个方案的好处很直观热点短链的访问路径从“查库”变成了“查缓存”单机 QPS 轻松上几千。但我实话说在真正落地的时候缓存这套东西比想象中更容易出问题不是技术难而是容易忽略一些边界。比如缓存穿透一个不存在的短码被大量请求打过来时每次都到数据库查一遍缓存完全没起到保护作用。我在后面第三个大章节会专门讲这个问题先在这里埋个伏笔。3. 跳转链路优化与统计埋点短链解析和跳转的代码逻辑不复杂但这一环里的技术选型会直接影响到数据准确性和用户体验。最典型的就是 302 和 301 的选择。3.1 为什么我选了 302 而不是 301重定向状态下301 是永久移动302 是临时移动。从搜索引擎优化和用户体验角度这两个状态码有很大区别。如果你选 301浏览器的行为是缓存这个重定向结果下次再访问同一个短链时浏览器直接跳转不再请求短链服务这个短链的后续点击统计就全部丢失了。如果你选 302每次访问都会先请求短链服务然后再跳转。代价是多一次网络请求但好处是每一次点击都能被统计到。我做的是带统计功能的系统所以毫无疑问选了 302。如果你做的是纯内部工具、完全不关心统计数据那选 301 会让访问速度稍快一些。不过我不能不说302 其实也没有慢多少现代浏览器对 302 的响应处理非常快用户体验差别基本可以忽略。3.2 跳转接口的具体实现要点跳转接口的实现我给出一个最精简的版本GetMapping(/{shortCode}) public void redirect(PathVariable String shortCode, HttpServletRequest request, HttpServletResponse response) { String originalUrl shortLinkService.getOriginalUrl(shortCode); if (originalUrl null) { response.setStatus(HttpStatus.SC_NOT_FOUND); return; } // 异步记录点击日志 clickLogService.asyncRecord(shortCode, request); response.setStatus(HttpStatus.SC_FOUND); response.setHeader(Location, originalUrl); }你要注意一个细节response.setStatus(HttpStatus.SC_FOUND)对应 302很好记。另外跳转之前对原始 URL 也要做一次安全性校验防止它被篡改成javascript:之类的危险协议。这两个点看似小事但真上线后都是事故高发区。3.3 统计埋点的异步处理方案点击日志的记录我强烈建议异步化不要在跳转链路里阻塞响应。我最初的实现是直接在跳转逻辑里查库写入没多久就发现创建链接接口的耗时也受到影响。后来我把日志记录丢进了内存队列由后台线程批量消费攒够了一批再写库。这里还要处理一个问题日志字段的标准化。比如 IP 地址往往需要从请求头里读取CDN 环境下客户端真实 IP 通常在X-Forwarded-For头里必须加以判断和兼容User-Agent 也要做截断处理有些 UA 字符串特别长数据库字段不够就会被截断报错。统计数据的准确性很多时候不是靠后端的算法而是靠这些前期细节是否到位。3.4 短链排重与重复创建问题用户拿同一条长链接来创建短链应该返回同一个短码还是新建一个短码这是一个很实际的产品决策。如果允许重复创建好处是每次创建的短码都可以附带不同的过期时间、来源渠道标签方便追踪不同渠道的效果坏处是存储浪费也会让历史统计数据被拆散。我的做法是默认做原始 URL 的排重创建前先查一次库如果存在且没过期直接返回原短码。这个逻辑要精确匹配 URL包括协议、域名、路径和查询参数不能做模糊匹配。但也要提供接口参数可以强制绕过排重因为有些场景确实需要同一个链接生成多个短码来区分投放渠道。这个设计是使用灵活性和存储成本之间的一个平衡点。4. 缓存穿透、过期策略与恶意请求的攻防这一章节算是我踩坑最多的部分也是整个系统上线后真正考验性能和安全性的地方。短链系统看起来人畜无害但它本质上是暴露在公网上的服务任何人拿到一个短链都可以发起大量请求。4.1 缓存穿透怎么查怎么防刚才提过缓存穿透这个问题。一个恶意或随机构造的短码根本不存库如果并发量很大每次请求都会穿透 Redis 直达数据库。虽然数据库查询很快但量大了一样会把连接池打满。我试过一个非常极端的场景刷一个不存在的短码每秒发起几千个请求数据库连接直接占满最终整个应用线程阻塞。我的防护措施是加布隆过滤器。在启动时加载所有有效短码到布隆过滤器里查询前先判断短码是否存在不存在直接返回 404根本不会去查 Redis 和数据库。布隆过滤器的特点是判断“不存在”是百分百准确的判断“存在”则有一定误判率恰好适合这个场景。如果你不想引入布隆过滤器还有一个更简单的兜底方案对不存在的短码做空值缓存也就是把查询结果为 null 的情况也缓存起来设置一个很短的过期时间比如 30 秒到 60 秒。这样即使恶意请求很多也只会穿透到缓存不会每次都打到数据库。我建议两种方案结合用效果最好。4.2 有效期控制与过期清理短码的过期时间是一个很容易被忽视的设计点。在实现时我建议用“逻辑过期 物理清理”两层策略。逻辑过期就是查询的时候判断expire_time是否早于当前时间早于则视为不存在返回 404。物理清理是后台定期任务把已经过期的短码从表里删掉避免表无限膨胀。缓存层面的处理也要配合写入缓存时把过期时间设为短码的剩余有效期同时加一个上限。比如剩余有效期超过 24 小时缓存就按 24 小时算防止一个长期有效的短码在缓存里占用太久剩余有效期小于 5 分钟就按 5 分钟算防止缓存提前失效打爆数据库。这个细节看起来很小实际对系统稳定性影响很大。4.3 恶意请求识别与限流短链系统最怕的恶意行为有几类高频访问同一短码消耗带宽和连接批量遍历短码扫出有敏感内容的链接恶意构造短码试图撞出别人的链接。对于前两类限流是必须的。我当时的限流方案比较朴素基于 IP 的滑动窗口限流内存或 Redis 里记录每个 IP 在最近一分钟内的访问次数超过阈值就返回 429 或者直接拒绝。单机应用用本地限流就够了分布式部署则要用 Redis 的INCR加过期时间来实现。另外所有访问周期性地检查请求 Header 里的 UA如果命中明显的爬虫关键词直接拒绝并使用户得不到跳转。4.4 链接安全管理这个环节不只是链路安全也是对用户负责。有些用户可能会恶意提交一些钓鱼或非法链接短链服务如果不管就相当于给这些内容做了跳转背书。我做了两个层面的管理一个是创建时基于 URL 白名单和关键词黑名单做校验明显有问题的直接拒绝另一个是定期对存量链接做重新扫描发现异常就批量下线。域名地址的校验要注意不能只校验“是否包含某个合法域名”因为evil.example.com也包含example.com。正确的做法是解析 URL 的 host 部分然后取最后两级域名做精确匹配比如example.com本身。另外也要限制可提交的 URL 协议只允许 HTTP 或 HTTPS其余直接屏蔽这能堵住一大半的注入攻击。5. 常见问题排查与上线后的经验笔记最后这部分是真正来自实战的记录。我把自己在开发和上线过程中遇到的高频问题整理成速查笔记这些问题每一个都是真实花费过时间和精力排查过的希望对你有借鉴意义。5.1 短码穿过 Redis 到数据库接口耗时飙高现象某些短码平时访问很快偶尔一次要几百毫秒甚至超时。排查发现这些短码的缓存刚好过期而同一时刻又有大量请求进来全部回源到数据库。处理先把缓存过期时间调大对热点短码额外做过期时间延长的逻辑。更关键的是设置缓存击穿保护当缓存为空时查询 DB 之前先加一个分布式锁只有拿到锁的线程能查 DB 并回填缓存其他线程短暂等待后从缓存读取。这个改动上线后回源频率大幅下降接口耗时稳定在 10ms 以内。5.2 点击统计数据出现重复或丢失现象统计数据总是和实际访问量对不上。排查后定位到两个原因一部分是用户在抓取或预加载时产生了额外的请求另一部分是服务重启时内存队列里的日志丢失了。处理数据准确性必须靠去重策略兜底。我在日志表里加了基于“短码 IP UserAgent 秒级时间”的唯一键重复数据的写入会报错后台任务据此丢弃重复记录。至于服务重启丢失的问题把内存队列升级为异步批量写入同时落一份本地文件作为临时缓冲区重启后可以回放补录。5.3 创建短链接口被刷爆现象大量创建请求让数据库写入压力激增数据增长异常。处理创建接口单独做了限流统一接入系统的网关限流策略。另外把创建逻辑中的排重查询加上 Redis 缓存重复创建的请求在缓存层就能被拦截不必每次查库。5.4 自定义短码的支持很多内部系统都希望支持自定义短码比如品牌推广或特定业务场景。这个功能不难做但碰撞处理逻辑要谨慎。我预留了short_code的可写接口创建时先做唯一性检查冲突则返回提示。同时我给系统内置生成的短码加上一个前缀比如a开头自定义短码不允许使用这个前缀这样就隔离了两套编码空间降低了冲突概率。5.5 监控告警的提前部署短链服务一旦接入了线上流量它的稳定性和可用性就直接影响引流效果。我给系统接入了三层监控基础资源监控CPU、内存、磁盘、网络带宽、应用层监控接口 QPS、响应时间、错误率、数据层监控缓存命中率、数据库慢查询、消息积压量。其中缓存命中率是我最关注的指标命中率一旦从 90% 掉到 60%我就知道缓存策略或访问模式发生了变化会立刻排查。我还发现一个特别容易被忽视的坑数据库连接池的初始大小。上线初期流量不大默认连接池 10 个就够了但遇到一次活动流量高峰连接池直接被打满大量请求排队等待连接。后来我把连接池的最小空闲数和最大连接数都做了调整并且加上了连接池耗尽告警这样问题就能在影响用户之前暴露出来。最后的实操建议如果我今天重新做一遍短链接系统我不会一上来就堆模块、堆框架而是先用最简单的方式把全链路跑通再逐步加上缓存、统计、安全机制。短链系统最大的价值不在代码多么花哨而在于它能否稳定地扛住真实的访问流量并在关键时刻提供准确的数据支撑。给别人一个建议先从核心链路动手一张表、一个发号器、一个重定向接口先跑起来之后再逐步迭代。每一步都留下监控和日志出了问题才能快速定位。这套系统我在线上跑了很久最大的心得体会就是架构可以简单但细节必须抠到位每一个看似不起眼的小问题在流量放大之后都会变成大事故。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑