第一个用户
我花了两个周末写完短链接服务,又花了一个周末部署上线。然后在 V2EX、掘金、少数派上各发了一篇介绍帖。
三天过去,访问量 47。注册用户:0。
一周过去,访问量 112。注册用户:还是 0。
说实话,我有点沮丧。每天打开服务器监控面板,看到的只有我自己测试时留下的请求。那些 /a3f8c2、/b7d9e1 的跳转记录,全是我自己的 IP。
第二周结束时,我认真考虑要不要把这台每月 ¥68 的云服务器退掉。反正也没人用,省点钱不好吗?
那天晚上,我正准备登录云服务商控制台,Gmail 弹出了一封新邮件。
发件人:王志远(小王)
我的心跳快了半拍。
嗨,我是小王,一个独立开发者。
我刚刚发布了自己的第一个 SaaS 产品——一个在线简历生成工具。需要在社交媒体上推广,但产品链接实在太长了:
https://www.myresumebuilder.com/landing?utm_source=twitter&utm_medium=social&utm_campaign=launch我在 V2EX 上看到你的短链接服务,想试试!请问怎么使用?
另外说一句,你的服务真的很快,页面秒开。
我激动得差点从椅子上跳起来。不是因为它是一封”用户反馈”——而是因为真的有人,一个活生生的人,在互联网的某个角落,用了我写的东西。
我花了五分钟冷静下来,然后认真写了一封回信,附上了完整的 API 调用示例。
十分钟后,我的服务器日志开始滚动了。
小王按照我给的文档,发出了他的第一个请求:
验证要点
- 用
curl -i发一次创建请求,检查返回体里的短码和 HTTP 状态码。- 再用
curl -i访问返回的短链,确认响应是 302 且Location指向原始长链接。- 响应时间同样要看:这次验证要确认的是返回状态、跳转目标和响应时间三项都对。
服务器记录了全过程:
落地思路
- 输入是创建短链接的请求:目标 URL、期望的自定义后缀,以及请求方身份。
- 服务先校验 URL 合法性再落库,校验不过直接返回错误,不留下半条记录。
- 落库成功后返回短码和完整短链,用户拿到的短链立即可跳转。
返回结果:
配置要点
- 这一版的环境差异只有一处:短链域名从本机测试地址换成正式域名
short.url。- 数据库连接和缓存地址从环境变量读,同一份代码在开发机和服务器上只差配置。
然后我盯着终端等了整整十分钟。终于在 10:33,日志里出现了第一行来自外部用户的访问记录:
落地思路
- 这一行日志要能看到:来源 IP、请求路径里的短码、命中的目标 URL、响应状态码。
- 同时记录 UA 和 Referer——小王带来的流量结构和本地自测完全不同,这两项后面要用来判断真实使用场景。
- 访问记录先落应用日志文件,统计聚合是后面才要做的事,此时链路越简单越好。
302 跳转,成功了。
那一刻我才真正理解了一件事:服务在自己电脑上跑通是一回事,让别人在真实世界里用上是另一回事。那种感觉完全不同。有人在 Twitter 上点了 short.url/a3f8c2,然后在浏览器里看到了他自己的产品页面。我的服务,完成了这次连接。
一周后,小王发来了一封很长的反馈邮件:
太棒了!短链接服务很好用!
我在 Twitter、Facebook、LinkedIn 都分享了短链接,效果很好。
不过,我有几个建议:
- 点击统计:我想知道我的短链接被点击了多少次——哪个渠道来的多,哪个渠道没人点。这对营销来说太重要了。
- 自定义短链接:我想要一个更好记的,比如
short.url/myresume,可以印在名片上。- 有效期设置:有些推广活动只搞一周,链接能不能自动过期?
- API 文档:最好有更详细的 API 文档和接入说明,我打算集成到我的产品里。
我读完邮件,靠在椅背上想了很久。
原来用户需要的不只是”缩短链接”这么简单。他们要的是可追踪、可管理、可集成的链接服务。缩短只是最基础的功能——就像搜索引擎的搜索框只有一个,但背后有排名、有推荐、有广告。
我打开一个空白文档,列出了新的开发计划。但在此之前,我需要先处理另一个更紧迫的事情。
就在我觉得一切顺利的时候,小王发来了一条消息:
你好,我发现了一个问题:
我创建的短链接
https://short.url/d7f3b2在某些浏览器中无法跳转。具体表现:
- Chrome:正常跳转 ✅
- Firefox:正常跳转 ✅
- Safari:无法跳转 ❌
- 微信内置浏览器:无法跳转 ❌
能否帮忙看看?
收到这条消息的时候,我的心凉了半截。我只有一个用户,一个!而且他已经遇到了 Bug。
我马上打开服务器开始排查。
问题定位
首先检查重定向的代码逻辑——看起来没什么问题:
落地思路
- 跳转代码的判断链只有三步:短码是否为空、查库是否命中、命中后返回 302 并带 Location 头。
- 关键确认点:查库用的是精确匹配还是模糊匹配,以及传入的短码有没有做过大小写归一。
- 这一步的结论是「代码逻辑本身没问题」,把嫌疑转向「输入被改了」。
然后我去查不同浏览器的访问日志:
落地思路
- 按 User-Agent 分组看这几个短码的请求:Chrome、Firefox 命中,Safari、微信内置浏览器返回 404。
- 对比失败请求里数据库查的是什么值——如果查的是大写短码而库里存的是小写,问题就定位了。
- 复现方式:在同一台机器上分别用四种浏览器访问同一个短码,保留请求路径原文。
Safari 和微信都返回了 404?但 Chrome 和 Firefox 没问题?
我盯着这几行日志看了半天,突然一个念头闪过。
根因:大小写敏感
落地思路
- 输入是 URL 路径里的短码,Safari 和微信内置浏览器可能把它转成大写后再发请求。
- 库里存的是小写 d7f3b2,而查询语句是 WHERE short_code = ‘D7F3B2’,大小写敏感自然查不到。
- 查不到记录就返回 404,用户看到的不是「链接坏了」,而是一个明确的错误页。
原来 Safari 和微信内置浏览器在某些场景下会把 URL 路径转为大写(或保持用户输入的原始大小写)。而我的数据库查询用的是 SQLite,默认是大小写敏感的。WHERE short_code = 'D7F3B2' 当然找不到 d7f3b2。
找到了!就是它。
修复方案
我列了三个方案:
落地思路
- 方案 1:在每个读取短码的接口里手动转小写——改一处漏一处,新接口就会复现同样的 Bug。
- 方案 2:应用入口统一做一次归一——比方案 1 好,但归一逻辑仍然只存在于代码里,绕不过数据库。
- 方案 3:把比较规则交给数据库,让 short_code 的比较本身大小写不敏感——规则落在数据层,任何入口都绕不掉。
- 三个方案的差别不是写法,而是「这条规则该由谁来保证」。
方案 1 和方案 2 都是在应用层做转换,简单但容易遗漏——万一哪个新接口忘了加 .lower() 就又出 Bug。
我选择了方案 3:数据库层面解决,一次性堵死:
数据设计要点
- 核心是在
urls里保存业务事实,而不是把规则散落在应用逻辑里。- 关键字段包括
id、short_code、long_url、created_at,它们决定了后续查询和管理能力。
COLLATE NOCASE 让 SQLite 在比较 short_code 时自动忽略大小写。存储时保留原始大小写,查询时大小写不敏感,索引也能正常使用。一劳永逸。
我改完表结构,重新部署,给小王回了一封邮件:“已修复,请再试试。”
五分钟后他回复:“Safari 和微信都正常了!”
我长舒一口气。
Bug 修完了,但这次事故让我意识到:我的系统太简陋了。一个大小写问题就让我手忙脚乱。如果以后用户多了,还不知道会出什么幺蛾子。
我决定在加新功能之前,先把系统架构升级一下。
当前系统的现状
先看看现在的处理链路有多朴素:
落地思路
- 每一次访问都直接查数据库,没有命中缓存的路径,也没有负缓存。
- 每次访问都同步写一条访问记录,写入耗时直接叠加在用户等待跳转的时间里。
- 统计没有独立出口,所有查询都要回到同一张表上现场聚合。
没有缓存,没有异步处理,没有统计。每次重定向都直接查数据库。
升级方案:缓存 + 异步日志
我引入了两项关键改进:Redis 缓存和异步日志队列。
落地思路
- Redis 缓存放的是「短码 → 目标 URL」的映射,命中就不查库,是降低数据库压力的第一道闸。
- 异步日志队列承接访问记录:跳转请求只负责投递,统计写入由后台慢慢做。
- 两者的边界是:缓存可以丢(回源重建),队列里的待写记录不能丢,所以队列要配持久化与重试。
为什么是异步日志?因为每次重定向都要写数据库的话,一旦数据库变慢,用户等的就是跳转——那是体感最明显的延迟。把日志扔进队列,后台线程慢慢写,用户完全无感知。
落地思路
- 消费端按批取走:攒够一批或到达时间上限就触发一次写入,把逐条 INSERT 变成批量 INSERT。
- 写入失败不重试到阻塞消费端:失败的批次进重试队列,超过重试次数就落到本地文件等补写。
- 消费端要能安全重启:重启时从上次提交位点继续,避免重复统计和漏统计。
改进后的数据库表结构也做了调整:
数据设计要点
- 核心是在
urls里保存业务事实,而不是把规则散落在应用逻辑里。- 索引服务于高频查询:
idx_short_code支撑按短码查映射,idx_clicked_at支撑按时间倒序翻访问明细。- 关键字段包括
id、short_code、long_url、created_at、clicked_at、ip、user_agent、referer,它们决定了后续查询和管理能力。
用户使用场景观察
上线后我观察了几天,发现用户的使用方式比我预想的丰富得多:
- 社交媒体营销:像小王这样的独立开发者,在 Twitter、LinkedIn 分享产品链接,链接被大量点击,需要稳定性。
- 短信营销:每条短信按字符计费,链接越短越省钱,5-6 个字符的短码是刚需。
- 二维码生成:线下推广场景,短链接生成的二维码更简单,更容易扫描。
- API 集成:有些开发者想把短链接功能嵌入自己的产品,需要 API 和文档。
一周后,我统计了一下数据:
落地思路
- 口径先定清楚:独立用户按去重 IP 算,重定向次数按返回 302 的请求算,错误指 5xx。
- 可用性按「成功请求数 / 总请求数」算,采样窗口取整周,避免只看某个时段。
- 这三个口径要固定下来写进监控面板,否则每周的数字互相不可比。
23 个独立用户,1243 次重定向,0 错误,99.9% 可用性。
成本:¥68/月(云服务器)+ ¥0(Redis 还跑在同一台机器上)。
收入:¥0。
但至少有人在用了。而且他们似乎还愿意继续用下去。
发现内容有问题?
告诉我们哪里需要改进。
