系统设计原则

1 标准协议

通信协议优先使用 HTTP、gRPC、WebSocket、MQTT 这些标准协议,自研协议的维护成本远高于第一版开发成本。

数据格式优先使用 JSON、Protobuf、MessagePack、CSV,不要随便搞 xxx.json2xxx.bin 这种只有自己认识的格式。

加密解密直接使用 TLS、AES、ChaCha20、SHA-256、Ed25519 等成熟方案,不要自己发明加密算法、Hash、签名算法。

各领域基础设施已经有非常多成熟的方案,例如:Nginx、Redis、MySQL、MinIO等等。

2 简单

不要为了未来设计不存在的需求。

YAGNI, You Aren’t Gonna Need It.

例如,现在只有

user_id
name

不要因为“以后可能需要多租户、国际化、ABAC、组织树”,第一版就设计成:

tenant_id
organization_id
region_id
locale
namespace
scope
policy
...

未来真的需要多话,再演进。

不要过度抽象。

例如,当前

func CreateUser()
func CreateOrder()
func CreateProduct()

不要立刻设计

AbstractEntityService
GenericDomainManager
BaseBusinessProcessor
UniversalCRUDService

重复不一定是坏事,错误的抽象才是。

不要过早微服务化。

如果一个单体代码量可能就那么 1000 行代码,不要设计成多个微服务运行。微服务解决的是组织规模、部署隔离、扩展性等问题,不是代码多了就拆。

3 优先降低耦合

模块之间尽量依赖接口,而不是具体实现。替换实现、测试和演进都会容易很多。

不要让业务层依赖基础设施细节。业务真正关心的是缓存数据,而不是我必须对接Redis。

不要让模块共享太多内部状态。避免使用全局变量,不然任何一个模块修改状态都会产生蝴蝶效应。

4 显式

不要依赖魔法

例如:

config.Get("xxx")

到处偷偷读取全局变量。

更推荐:

NewService(cfg)

依赖是什么,一眼就能看到。

不要让函数产生隐藏副作用。

例如:

func LoadConfig() {
  ...
}

执行的时候:至于加载了配置写到哪里去了,还得看内部细节。

不要依赖隐式顺序。

不要假设

A 一定先启动
B 一定后启动
C 一定已经初始化

系统应该尽可能明确表达依赖关系。

这在 Kubernetes、systemd、微服务中尤其重要。

5 可观测

任何关键操作都应该能够回答。

发生了什么?
什么时候发生?
谁发起的?
处理了多久?
为什么失败?
影响了谁?

所以系统设计阶段就应该考虑:

  • 日志
  • Metrics
  • Trace
  • Request ID
  • Audit Log
  • Error Code

而不是上线以后才补。

错误不要只返回“失败”

不推荐:

ERROR

至少应该能够区分:

INVALID_ARGUMENT
NOT_FOUND
TIMEOUT
PERMISSION_DENIED
CONFLICT
INTERNAL_ERROR

错误是系统 API 的一部分。

6 可演进

API 一旦发布,就把它当成长期契约

例如:

/api/v1/users

不要轻易修改:

{
  "name": "张三"
}

变成:

{
  "username": "张三"
}

然后要求所有客户端同时升级。

更好的方式是:

V1
 ↓
V2
 ↓
逐步迁移
 ↓
废弃 V1

数据库 Schema 要考虑向后兼容

特别是线上系统。

推荐:

增加字段
 ↓
发布代码
 ↓
新代码开始使用
 ↓
旧字段逐渐废弃
 ↓
最后删除

而不是:

ALTER TABLE
删除字段
 ↓
然后发现旧版本还在访问
 ↓
生产事故

不要让一个版本升级必须“全世界同时升级”

理想状态:

Client V1 ───┐
             ├── Server V2
Client V2 ───┘

而不是:

Client V1 × Server V2
Client V2 × Server V1

这叫做兼容性设计,也是为什么协议设计非常重要。

7 失败可控

系统一定会失败,要设计失败模式。

不要问:

“怎么保证 Redis 永远不会挂?”

而应该问:

“Redis 挂了以后系统怎么办?”

例如:

Redis 挂掉
 ↓
缓存失效
 ↓
是否降级?
 ↓
数据库是否被打爆?
 ↓
是否限流?
 ↓
是否熔断?

不要让局部故障扩散成全局故障

这是分布式系统非常重要的原则:

服务 A 故障
   ↓
服务 B 等待
   ↓
服务 C 等待
   ↓
线程池耗尽
   ↓
整个系统雪崩

应该设计:

Timeout
Retry
Circuit Breaker
Bulkhead
Rate Limit
Fallback

重试必须谨慎

很多人认为:

请求失败 → retry

但如果:

请求支付
 ↓
服务端已经扣款
 ↓
响应丢失
 ↓
客户端 retry
 ↓
再次扣款

就出事故了。

所以:

重试 + 幂等,是一对必须一起设计的东西。

8 数据设计

不要让多个系统同时成为同一份数据的“主人”

例如:

A → user 表
B → user 表
C → user 表

最后:

到底谁说了算?

更好的设计是明确:

User Service
    ↓
User 数据唯一事实来源

Single Source of Truth


不要为了性能过早引入缓存

先:

Database

发现:

QPS 不够

再:

Database
    ↑
   Cache

否则你马上获得一个新问题:

数据库里的数据和缓存里的数据到底哪个是真的?


不要把一致性当成免费的

你选择:

强一致

通常意味着:

性能 ↓
可用性 ↓
复杂度 ↑

选择:

最终一致

则意味着:

性能 ↑
可用性 ↑
业务处理复杂度 ↑

一致性是架构决策,不是数据库默认行为。

9 总结

原则 核心思想
Don’t reinvent the wheel 不重复造轮子
YAGNI 不为不存在的需求设计
KISS 能简单就不要复杂
Avoid premature optimization 不要过早优化
Prefer standard 优先标准方案
Explicit over implicit 显式优于隐式
Loose coupling 降低耦合
Backward compatibility 保证向后兼容
Fail gracefully 优雅处理失败
Observability first 系统必须可观测

但如果只让我选三个最高级的原则,我会选:

① 不要重新解决已经被解决的问题。
② 不要为尚不存在的问题增加复杂度。
③ 永远假设依赖会失败、数据会异常、需求会变化。

Author: ismdeep
License: Copyright (c) 2025 CC-BY-NC-4.0 LICENSE