关键决策

开篇

回头看,有 10 个关键决策塑造了今天的系统。每个决策背后都有纠结。

三年前,当我决定构建这个短链接服务时,我以为最难的是技术实现。但真正困难的,是在无数个深夜里做出的那些选择。每一个决策都像是一次赌博——赌错了,可能满盘皆输;赌对了,也不过是继续前行。

这三年,我经历过数据库崩溃的恐慌,也体会过系统平稳运行的满足。每一次选择,都是在当时的信息和约束下,我能做出的最好决定。

让我带你回到那些关键的时刻,看看我是如何纠结、如何选择的。


决策一:SQLite vs MySQL——省钱还是省心

当时的情况

项目刚启动的时候,我口袋里没多少钱。服务器预算每月只有 50 块。

那时候的用户量?说实话,我都不确定会不会有人用。

我的纠结

用 SQLite 吧,省钱。

SQLite 不需要额外的数据库服务器,一个文件就能搞定。对于囊中羞涩的独立开发者来说,简直是天选之子。

但 SQLite 真的能撑住吗?

我查了很多资料。有人说 SQLite 单机能处理 10 万 QPS,有人说并发写入会有锁竞争。我想象着:如果有一天,真的有大量用户同时创建短链接,数据库会不会锁死?

用 MySQL 吧,省心。

MySQL 是成熟的数据库,并发处理能力强。但代价是需要额外的服务器资源,需要更多的运维精力。

每月 50 块的预算,买完应用服务器,剩下的钱够买 MySQL 服务器吗?

我在两个选择之间摇摆了一个星期。

“如果用 SQLite,能省下每月 30 块。一年就是 360 块。”

“但如果遇到并发问题,重构的成本是多少?迁移数据的时间是多少?”

最终让我下定决心的是一个论坛帖子:

“我用 SQLite 起步,用户量到 10 万时遇到性能问题。迁移到 MySQL 花了两周,期间服务不可用。如果重来,我会直接选 MySQL。“

最终选择

我选择了 MySQL。

理由很简单:我不想让数据库成为系统的瓶颈。即使初期多花点钱,也比将来重构划算。

我租了一台最低配置的云服务器,装了 MySQL 5.7。配置能省则省:单实例、每天一次备份、内存参数调到最低。

现在看来

这个决定是对的。

第一年结束时,系统已经有了稳定的流量。如果当初用 SQLite,可能早就遇到并发问题了。

但我也付出了代价:运维成本比预期高。数据库偶尔会挂,需要手动重启。

记得有一次,凌晨三点我被告警短信吵醒。数据库 CPU 100%,所有请求都在排队。我登录服务器,发现是一个慢查询导致的。花了半小时才修复。

那一刻我真想:要是用 SQLite,是不是就没这回事了?

但转念一想:如果用 SQLite,可能早就因为并发问题崩溃了,连被吵醒的机会都没有。

如果重来一次,我可能会选择 SQLite 起步,等用户量上来再迁移。毕竟,premature optimization is the root of all evil。

核心权衡:初期成本 vs 长期可扩展性


决策二:MD5 vs 自增 ID——冲突的教训

当时的情况

确定了数据库之后,下一个问题是:短码怎么生成?

我最初的想法很简单:用 MD5。把长 URL 哈希一下,取前 8 位,不就是短码了吗?

我的纠结

MD5 方案看起来很完美:

  • 相同的 URL 总是生成相同的短码
  • 无需存储映射关系
  • 实现简单,几行代码搞定

我甚至已经想好了第一版流程:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

但在上线前,我突然想到了一个问题:冲突怎么办?

MD5 取前 8 位,总共 16^8 = 42 亿种可能。根据生日悖论,当 URL 数量达到约 6.5 万时,冲突概率就达到 50%。

6.5 万个 URL!对于一个小项目来说,这看起来很多。但如果真的做大了呢?

我想象着那个场景:用户 A 创建了一个短链接,用户 B 也创建了同一个短码——但指向不同的 URL。系统该怎么办?

另一种方案是自增 ID。每条记录一个 ID,从 1 开始递增,然后用 Base62 编码转换成短码。

但自增 ID 也有问题:

  • 可预测:用户知道 ID 是连续的,可以遍历所有短链接
  • 分布式困难:多服务器时,ID 会冲突

一位从业十年的工程师在博客里写道:

“我们试过哈希方案,冲突问题让我们吃了大亏。后来改用自增 ID,虽然要处理分布式问题,但至少不会有冲突。”

这句话让我下定决心。

最终选择

我选择了自增 ID + Base62 编码。

冲突的代价太高了。自增 ID 虽然有可预测的问题,但可以通过其他方式缓解。

Base62 编码让短码更短、更友好。100 万的 ID 编码后只有 4 位:4c92。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

现在看来

这个决定避免了潜在的冲突问题,但带来了另一个问题:分布式 ID 生成。

当系统扩展到多台服务器时,自增 ID 就不够用了。我后来引入了 Snowflake 算法,才解决这个问题。

如果重来一次,我会从一开始就使用分布式 ID 生成器。虽然复杂一点,但避免了后续的重构。

核心权衡:实现简单 vs 冲突风险


决策三:301 vs 302——统计 vs 性能

当时的情况

第一个用户反馈来了:“为什么我的短链接跳转后,浏览器地址栏还是短域名?”

我复盘当时的选择,发现用的是 301 永久重定向。

我的纠结

301 是永久重定向。浏览器会缓存这个重定向关系,下次直接跳转到目标 URL,不再请求短链接服务。

好处是性能更好,减少一次请求。坏处是无法统计后续点击。

302 是临时重定向。浏览器每次都会请求短链接服务。

好处是可以统计每次点击。坏处是每次都要请求服务器,增加压力。

我纠结了很久。

从技术角度,301 更优雅。但从产品角度,302 更实用。点击统计是核心功能之一,用户需要知道有多少人点击了他们的链接。

我找来几个朋友,问他们的意见。

做产品的朋友说:“肯定要 302。没有统计数据,用户怎么知道链接效果?”

做运维的朋友说:“301 更好。少一次请求,服务器轻松很多。“

最终选择

我选择了 302。

理由很现实:点击统计是我们的核心卖点。如果为了性能牺牲这个功能,产品就失去了竞争力。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

现在看来

这个决定是正确的。

很多用户告诉我们,点击统计是他们选择我们的重要原因。如果当初用了 301,这些用户可能就流失了。

但我也付出了代价:服务器压力比预期大。高峰期,重定向接口占了 80% 的流量。

记得有一次大促,流量突然暴涨。服务器 CPU 飙升,响应时间从 10ms 变成 100ms。我紧急扩容,才扛过去。

后来我引入了多级缓存,才缓解了这个问题。

如果重来一次,我可能会提供一个选项:让用户选择 301 或 302。不同的场景,不同的需求。

核心权衡:统计能力 vs 跳转性能


决策四:本地缓存 vs Redis——简易 vs 专业

当时的情况

系统运行半年后,数据库开始吃不消了。每次重定向都要查数据库,数据库的 CPU 经常飙到 90%。

我的纠结

加缓存是必然的。但用什么缓存?

用本地缓存吧,简单。就是应用内存里的一个字典,访问速度极快。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

测试时,响应时间从 50ms 降到了 1ms。效果惊人。

但本地缓存有问题:多服务器时不共享,服务器重启会丢失。

我想象着:如果有两台服务器,用户访问服务器 A 创建的短链接,但请求到了服务器 B,缓存就没有了。

用 Redis 吧,专业。

Redis 支持分布式,多服务器可以共享。但需要额外的服务器资源,增加网络延迟,运维复杂度也增加。

我查了 Redis 的资料,发现它不仅仅是缓存:

  • 可以用来做分布式锁
  • 可以用来做计数器
  • 可以用来做队列

这些功能我将来可能都用得上。

但我又担心:引入 Redis 会不会增加太多复杂度?我现在连 MySQL 都还没搞利索,再加一个 Redis,能维护过来吗?

最终选择

我选择了 Redis。

理由是多服务器扩展。虽然现在只有一台服务器,但我想为未来做准备。

我租了一台 Redis 云服务,每月 20 块。迁移过程很顺利。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

上线后,数据库 CPU 从 90% 降到了 20%。效果立竿见影。

现在看来

这个决定是正确的。

第二年,我们扩展到了三台服务器。如果没有 Redis,缓存同步会是个大问题。

但我也发现,本地缓存并没有完全废弃。我采用了多级缓存策略:

  • L1:本地缓存(进程内)
  • L2:Redis 缓存(共享)
  • L3:数据库(持久化)

这个架构的性能比单纯用 Redis 更好。L1 缓存命中时,响应时间不到 1ms。

如果重来一次,我会从一开始就设计多级缓存。但当时的我,连单级缓存都没想明白。

核心权衡:实现简单 vs 分布式能力


决策五:单机 vs 分布式——够用就好 vs 未雨绸缪

当时的情况

第一年结束,系统运行平稳。每天几十万的重定向请求,数据库和缓存都能扛住。

有人问我:“什么时候考虑分布式?“

我的纠结

上分布式吧,未雨绸缪。

分布式架构可以水平扩展,理论上可以支撑无限流量。但分布式是有代价的:

  • 复杂度指数级增长
  • 需要解决一致性问题
  • 需要处理网络分区
  • 调试困难

我看过太多分布式系统的坑:数据不一致、网络分区、死锁、调试困难……

不上分布式吧,够用就好。

现在的架构简单清晰:一台应用服务器、一台数据库服务器、一个 Redis 实例。维护成本低,出了问题也容易定位。

如果真的流量大增,再加机器也不迟。

我想起一句话:“Don’t scale until you need to.”

但我也担心:等需要的时候再加,会不会太晚?重构的成本会不会更高?

我找来一个做过分布式系统的朋友,问他:“你觉得我应该什么时候上分布式?”

他说:“等你真的需要的时候。”

“那怎么判断需不需要?”

“当单机性能到极限,或者运维成本超过分布式成本时。“

最终选择

我选择了单机架构,但预留扩展空间。

具体来说:

  • 应用层无状态,可以水平扩展
  • 数据库读写分离,可以加从库
  • 缓存用 Redis,支持集群

我特意把应用设计成无状态的。所有状态都放在 Redis 和数据库里,应用服务器不存任何状态。

这样,将来需要扩展时,直接加机器就行,不用改主流程。

现在看来

这个决定是明智的。

第二年流量确实增长了,但还没有到必须分布式的程度。我们加了一台应用服务器,用 Nginx 做负载均衡,就解决了问题。

数据库方面,我们做了读写分离。写操作走主库,读操作走从库。性能提升明显。

如果一开始就搞分布式,可能会陷入过度设计的陷阱。微服务、服务发现、配置中心……这些东西会增加多少复杂度?

我记得有一次,有个候选人来面试。他之前在一家大厂做分布式系统,讲了很多高大上的架构。

我问他:“你们为什么要搞这么复杂?”

他说:“其实很多时候没必要。但大厂有 KPI,要做技术储备。”

我笑了。至少我没有 KPI 压力,可以按需设计。

核心权衡:当前简单 vs 未来扩展


决策六:Snowflake vs Redis INCR——复杂 vs 简单

当时的情况

当我们扩展到三台应用服务器时,ID 冲突问题出现了。

三台服务器都在生成自增 ID,不可避免地会产生重复。

我的纠结

用 Redis INCR 吧,简单。

Redis 是单线程的,INCR 操作是原子的。所有服务器都从 Redis 获取 ID,保证唯一性。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

看起来一步就能完成。

但 Redis INCR 有问题:Redis 成为单点,每次生成 ID 都要网络请求,性能受限。QPS 大概 5 万左右。

用 Snowflake 吧,复杂但强大。

Snowflake 是 Twitter 开源的分布式 ID 生成算法。每台服务器独立生成 ID,不会冲突。

Snowflake ID 结构:41 位时间戳 + 5 位数据中心 ID + 5 位机器 ID + 12 位序列号。

但 Snowflake 实现复杂:需要处理时钟回拨,需要管理机器 ID 分配,需要处理序列号溢出。

我看了 Snowflake 的源码,发现有一个致命问题:时钟回拨。如果服务器时间突然回退,生成的 ID 可能和之前的重复。

最终选择

我选择了 Snowflake。

理由是性能和可靠性。

Redis INCR 虽然简单,但每次生成 ID 都要访问 Redis。当 ID 生成量大时,Redis 会成为瓶颈。

Snowflake 在本地生成 ID,几乎没有延迟。性能是 Redis INCR 的几十倍。

而且 Snowflake 不依赖外部服务,可靠性更高。即使 Redis 挂了,ID 生成也不受影响。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

现在看来

这个决定是正确的。

Snowflake 的性能确实出色。单台服务器每秒可以生成 400 万 ID,完全满足需求。

但 Snowflake 也不是完美的。我们遇到过时钟回拨问题:服务器时间突然回退,导致生成的 ID 重复。

那是个凌晨,监控系统报警:ID 重复。我发现 NTP 服务把时间校准了,导致回拨了几毫秒。

我紧急修复,加了时钟回拨检测。后来我们把 NTP 服务配置得更保守,问题才解决。

如果重来一次,我可能会先用 Redis INCR 过渡,等 ID 生成成为瓶颈再换 Snowflake。

核心权衡:实现简单 vs 生成性能


决策七:自建审核 vs 第三方——成本 vs 效果

当时的情况

有用户举报了:有人用我们的短链接传播不良内容。

我们立刻下架了相关链接,但问题已经造成。媒体开始报道,用户开始质疑我们的审核能力。

我的纠结

自建审核系统吧,可控。

自建审核系统可以完全掌控:审核规则自己定、审核流程自己设计、审核结果自己决定。

但自建审核系统成本高:

  • 需要开发审核接口
  • 需要训练识别模型
  • 需要人工审核团队

我估算了一下,自建审核系统至少需要 2 个开发人员开发 1 个月、1 个算法工程师训练模型、3 个审核人员 7x24 小时轮班。每月成本至少 10 万。

用第三方审核吧,省心。

第三方审核服务成熟,效果好。但代价是按调用量收费、数据要传给第三方、依赖外部服务。

我调研了几家第三方服务商:

  • 阿里云内容安全:0.5 元/千次
  • 腾讯云内容安全:0.4 元/千次
  • 百度内容安全:0.3 元/千次

我们每天创建 10 万个短链接,每月就是 300 万次。按 0.3 元/千次计算,每月 900 块。

这个数字还能接受。

但问题是:第三方审核的准确率如何?误杀率如何?

我测试了几家,发现效果参差不齐。有些明显的不良内容没识别出来,有些正常内容被误杀。

最终选择

我选择了混合方案:第三方初审 + 人工复审。

具体来说:

  • 所有新创建的短链接,先过第三方审核
  • 第三方判断可疑的,转人工复审
  • 用户举报的,优先处理

这个方案平衡了成本和效果。

我接入了阿里云内容安全,同时建了一个审核后台,供人工审核使用。

现在看来

这个决定是合理的。

第三方审核拦截了 90% 的不良内容,人工复审处理剩下的 10%。效果不错,成本也可控。

但我也遇到了问题:第三方服务偶尔会挂。有一次,阿里云内容安全接口超时,导致大量短链接创建失败。

我紧急加了降级策略:第三方服务不可用时,先放行,后续异步审核。

这个策略有风险,但没办法。业务不能停。

如果重来一次,我可能会更早建立用户举报机制。很多不良内容都是用户先发现的,审核系统反而滞后。

核心权衡:控制力 vs 成本效率


决策八:免费 vs 付费——用户增长 vs 收入

当时的情况

系统运行一年后,有了一定的用户基础。但收入几乎为零。

服务器成本每月几千块,全靠我自己贴钱。

我的纠结

继续免费吧,用户增长。

免费策略可以快速获客,建立品牌。等用户量大了,再考虑变现。

但免费有代价:服务器成本持续增加、没有收入难以持续、用户质量参差不齐。

开始收费吧,有收入。

收费可以覆盖成本,甚至盈利。但代价是:用户增长放缓、部分用户流失、需要完善的付费功能。

我纠结了很久。

免费用户说:“这么好的服务,为什么收费?”

付费潜力用户说:“如果收费,有什么高级功能?”

我夹在中间,左右为难。

我调研了竞品:

  • bit.ly:免费版有限制,付费版功能多
  • tinyurl:完全免费,靠广告
  • 短链服务 A:免费 + 付费混合

看起来,混合模式是主流。

最终选择

我选择了分级收费模式:

  • 免费版:基础功能,有限制
  • 个人版:每月 9.9 元,更多功能
  • 团队版:每月 99 元,协作功能
  • 企业版:定制价格,专属服务
功能免费版个人版团队版企业版
短链接数量100 个1000 个10000 个无限
点击统计7 天90 天1 年永久
自定义短码10 个/月100 个/月无限
API 访问

这个模式兼顾了用户增长和收入。

现在看来

这个决定是正确的。

收费后,收入覆盖了成本,还有盈余。用户也没有大量流失,因为免费版依然可用。

但我也遇到了问题:付费转化率低。100 个免费用户里,只有 1-2 个会付费。

我尝试了很多方法提高转化率:免费试用付费功能、推送个性化优惠、优化付费页面。效果有一些,但不明显。

如果重来一次,我可能会更早开始收费。免费用户养成习惯后,更难转化。

但当时的我,没有这个底气。用户量不够,收费怕吓跑人。

核心权衡:用户规模 vs 商业可持续性


决策九:MySQL 分区 vs 分库分表——何时拆分

当时的情况

第二年结束,数据量达到了 5 亿条。

单表查询开始变慢,即使有索引,某些查询也要几秒钟。

我的纠结

用 MySQL 分区吧,简单。

MySQL 分区是在数据库层面做的,应用层感知不到。实现简单,迁移成本低。

但 MySQL 分区有局限:单实例瓶颈还在、分区数量有限、运维复杂。

用分库分表吧,彻底。

分库分表可以水平扩展,理论上没有上限。但代价是:应用层要感知路由规则、需要中间件支持、迁移成本高。

我调研了分库分表中间件:

  • ShardingSphere:功能强大,学习曲线陡
  • MyCAT:轻量级,功能有限
  • 自研:灵活,但开发成本高

我估算了一下,分库分表的迁移成本:开发时间 2-3 个月、测试时间 1 个月、迁移时间 1 周(可能需要停机)。

这个成本太高了。

但如果不拆分,系统撑不了多久。

最终选择

我选择了 MySQL 分区过渡,准备未来分库分表。

具体来说:

  • 按创建时间分区,每月一个分区
  • 历史数据归档到冷存储
  • 预留分库分表接口

分区后,查询性能提升明显。但我知道,这只是暂时的。

现在看来

这个决定是务实的。

分区解决了眼前的问题,为分库分表争取了时间。

但分区也有问题:跨分区查询慢,运维复杂。

我们计划在第三年进行分库分表改造。按短码首字母分片,6 个分片,每个分片独立数据库。

如果重来一次,我可能会更早规划分库分表。但当时的数据量,确实没必要。

核心权衡:短期解决 vs 长期规划


决策十:自运维 vs 云服务——控制权 vs 效率

当时的情况

第三年,团队扩大了。有专职运维,有开发,有产品。

但运维工作依然繁重:服务器监控、故障处理、备份恢复……

我的纠结

继续自运维吧,控制力强。

自运维可以完全掌控:配置自己定、优化自己做、问题自己排查。

但自运维成本高:需要专职运维人员、故障要自己处理、精力分散。

用云服务吧,效率高。

云服务省心:自动扩缩容、自动备份、专业运维团队。

但云服务有代价:成本可能更高、厂商锁定、控制权减弱。

我算了一笔账:

自运维成本:运维人员工资每月 3 万 + 服务器成本每月 1 万 = 每月 4 万。

云服务成本:云数据库每月 2 万 + 云缓存每月 5 千 + 云服务器每月 1 万 = 每月 3.5 万。

云服务反而便宜?

最终选择

我选择了混合模式:核心自运维,非核心云服务。

具体来说:

  • 应用服务器:云服务(弹性伸缩)
  • 数据库:自运维(核心数据)
  • 缓存:云服务(Redis 云)
  • 存储:云服务(对象存储)

这个模式平衡了控制权和效率。

现在看来

这个决定是合理的。

云服务确实省心很多。应用服务器自动扩缩容,大促时不用手动加机器。

但核心数据库还是自运维,因为数据太重要,不敢完全交给云。

如果重来一次,我可能会更早拥抱云服务。自运维的隐性成本很高,不仅仅是钱。

核心权衡:控制权 vs 运维效率


总结

没有一个决策是完美的,每个都有取舍。

系统设计的本质就是在约束条件下做最优选择。

这 10 个决策,塑造了今天的系统。它们不是最好的选择,但是当时我能做出的最好选择。

回头看,有些决定现在看是错的。但站在当时的角度,那已经是我能想到的最优解。

这或许就是工程的魅力:没有标准答案,只有权衡和取舍。

如果你也在做系统设计,记住:

  1. 不要追求完美:完美是优秀的敌人
  2. 数据驱动决策:用数据说话,不是直觉
  3. 预留扩展空间:今天的简单,可能是明天的负担
  4. 接受不完美:每个选择都有代价

愿你做出的每个决策,都能在回头看时,少一点遗憾。


下一节

在最后一节《设计原则》中,我们将总结从实战中提炼的系统设计核心原则。

👉 继续阅读:设计原则