0%

写在前面

因为研发进入公司后都给开了cursor帐号,虽然大多数人都知道cursor也简单试用过但是可能没有花心思研究它,所以导致最近一个多月用下来我发现大家的使用方式存在较大问题,且token消耗很快,简单说就是大多使用的时候都毫无技巧,就力大砖飞的方式跟对话框干上了的感觉。

所以我利用站会的一点时间简单给大家讲了一下使用技巧,让大家能把cursor用的更好同时也能适当降低token消耗。

Cursor 有哪些使用技巧?

核心目标:用最少的 Request 消耗,换取最准确的代码产出。
核心原则:AI 不懂就问(Interactive Feedback),人要想好再说(PVE 模式)。

🚀 第一部分:省流攻略 (Save Requests & Tokens)

1. 拒绝 “Auto” 模式,手动分级

痛点:默认的 “Auto” 模式经常在简单问题上杀鸡用牛刀,浪费宝贵的 Fast Request。

  • 最佳实践
    • **日常开发 (80%)**:强制使用 GPT-4o-mini 或 Claude 3.5 Haiku。用于改 Bug、写注释、简单函数。
    • **攻坚时刻 (20%)**:手动切换到 Claude 3.5 Sonnet。仅用于架构设计、复杂重构。
  • 操作:使用快捷键 Cmd + / (Mac) 或 Ctrl + / (Win) 快速切换模型,不要依赖自动路由 1。

2. 启用 Interactive Feedback (MCP) —— 关键技巧

痛点:需求模糊时,AI 靠猜。猜错 = 重写 = 浪费 2-3 次 Request。
原理:利用 MCP 协议,让 AI 在同一个请求内暂停并向你提问,直到确认清楚再写代码。

  • 最佳实践
    • 配置 interactive-feedback-mcp 服务。
    • Rule设置:在 .cursorrules 中加入:“如果指令不明确,必须调用 interactive_feedback 询问,禁止盲目猜测。”
  • 收益:将原本需要 3 轮“生成-纠错-再生成”的交互,压缩为 1 次请求 [User Input, 13]。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 1. clone interactive-feedback-mcp
git clone https://github.com/noopstudios/interactive-feedback-mcp.git
cd interactive-feedback-mcp

# 2. 使用 Python 3.11 创建标准虚拟环境
/opt/homebrew/bin/python3.11 -m venv .venv

# 3. 激活环境(为了确保 pip 指向正确)
source .venv/bin/activate

# 4. 新建requirements.txt,安装依赖
pip install requirements.txt

# 5. 检查是否能成功运行
./.venv/bin/python feedback_ui.py --project-directory "." --prompt "环境安装完毕测试" --output-file "debug_output.json"

# 6. cursor配置mcp
"interactive-feedback-mcp": {
"command": "/xxxx/interactive-feedback-mcp/.venv/bin/python",
"args": [
"/xxxx/interactive-feedback-mcp/server.py"
],
"cwd": "/xxxx/interactive-feedback-mcp",
"timeout": 600,
"autoApprove": [
"interactive_feedback"
]
}

3. PVE 工作流 (Plan-Verify-Execute)

痛点:想到哪写到哪(Vibe Coding),导致 AI 反复修改同一段代码,Token 爆炸。

  • 最佳实践
    • **P (Plan)**:先用免费模型(或 mini)让 AI 列出修改计划。
    • **V (Verify)**:人工确认计划无误。
    • **E (Execute)**:开启 Composer (Cmd+I),一次性把确认好的计划发给 Sonnet 执行。
  • 收益:一次成型,拒绝返工 3。

⚡ 第二部分:上下文工程 (Context Management)

1. 必须配置 .cursorignore

痛点:AI 读了 node_modules 或 lock 文件,导致 Token 耗尽且回答变慢。

  • 操作
    • 在根目录新建 .cursorignore。
    • 填入:node_modules/, dist/, *.json (大文件), logs/。
    • 效果:强制 AI 只看源码,Token 节省 30% 以上 4。

2. 精通 .cursorrules (配置即代码)

痛点:每次都要重复喊“用 TypeScript”、“不要用 Class 组件”。

  • 操作
    • 根目录新建 .cursorrules 文件。
    • 由简入繁
      1. 角色:You are a senior expert in [技术栈].
      2. 绝对禁止:No class components. No placeholder comments.
      3. 格式:Only output changed lines. (只输出修改行,极大节省 Output Token) 6。

3. “一次性” 会话原则

痛点:在一个 Chat 窗口聊几天,上下文充满了过时的旧代码,导致 AI 变笨且极其费钱。

  • 操作
    • Task 完成即销毁:一个功能点 = 一个 Chat。功能做完,立刻 Cmd+L 开新窗口。
    • 及时 Unpin:如果文件不再需要修改,立刻取消 Pin 状态 8。

🛠️ 第三部分:核心工具与技巧 (Tools & Tricks)

1. Composer (Cmd + I) > Chat (Cmd + L)

  • Chat 是咨询师(只看不写):用于问原理、查 Bug。
  • Composer 是工程师(直接写):用于多文件编辑
  • 技巧
    • Combo 连招:不要分三次改。直接在 Composer 输入:“在 User 表加字段,同步更新 API DTO,并修改前端类型定义。” —— 3 个文件变动,只需 1 次 Request 1。

2. 善用 Plan Mode (Shift + Tab)

  • 场景:大功能开发。
  • 操作:在 Composer 输入框按 Shift + Tab 进入 Plan Mode。AI 会先扫描代码库,生成 Markdown 格式的待办清单,你确认后它再动手。这比直接写代码更稳,更省重试次数。

📝 总结:开发者自查清单

在开始 coding 前,请确认:

  1. [ ] 模型对了吗? 简单问题切回 mini 了吗?
  2. [ ] 规则有了吗? .cursorrules 和 .cursorignore 配置了吗?
  3. [ ] 模式选对了吗? 多文件修改是否使用了 Composer?
  4. [ ] 反馈机制:是否启用了 Interactive Feedback 以防止 AI 瞎猜?
  5. [ ] 会话清理:上个任务结束了吗?结束了请开新 Chat。

参考资料

  1. Pricing | Cursor Docs
  2. Models | Cursor Docs
  3. In Cursor, Context is King - DEV Community
  4. Codebase Indexing | Cursor Docs
  5. Ignore files | Cursor Docs
  6. How to Use Cursor More Efficiently - r/ChatGPTCoding
  7. .cursorrules · GitHub Gist
  8. Understanding Cursor Token Usage - Reddit
  9. 4 hacks to turbocharge your Cursor productivity | LaunchDarkly

数学 交易

引言

说出来不怕笑话,我从小数学就差。

高中那会儿,数学老师看着我直摇头,我也确实没那个基因。从兴趣盎然到索然无味,只用了半个学期。谁知道工作这么多年后,我又对数学这玩意儿产生了兴趣——又菜又爱说的就是我。

这篇文章是因为最近看了 The Math of Trading - The Shmuts,很有感触,所以梳理一篇。


赌场为什么永远赚钱?

先问个问题:赌场靠什么赚钱?

你可能觉得靠运气、靠荷官的手法、靠游戏规则不公平。

其实都不是。赌场赚钱的秘密就一个字:

赌场不在乎某一局谁赢谁输。他们知道的是:只要玩的人够多,数学上的优势就会让他们稳赚不赔。

举个简单的例子,抛硬币。正面你赢100块,反面你输100块。长期玩下来,大家都不输不赢。

但如果规则改成:正面你赢90块,反面你输100块呢?

你可能觉得”才差10块而已”,但数学不会撒谎。长期玩下来,赌场稳赚。

这就是为什么赌场:

  • 欢迎:你玩100把,每把下注5块
  • 拒绝:你只玩1把,下注100万

因为单把赌博,概率优势还没来得及发挥作用。赌场怕的不是你赢,是怕你在概率发挥作用之前就不玩了。

把赌场思维换个场景,就是:那些懂数学的人,不和老天赌单次输赢,他们赌的是长期概率。


一个公式,看清所有赌局

交易里有个概念叫”期望值”(Expected Value),公式简单得要命:

1
期望值 = (赢的概率 × 赢的时候拿多少) - (输的概率 × 输的时候赔多少)

别被公式吓到,看个例子。

游戏A:抛硬币,正面赢3块,反面输2块

  • 期望值 = (50% × 3) - (50% × 2) = 1.5 - 1 = +0.5块

这个游戏值得玩,长期玩下去你稳赚。

游戏B:抛硬币,正面赢1块,反面输2块

  • 期望值 = (50% × 1) - (50% × 2) = 0.5 - 1 = -0.5块

这个游戏别玩,长期玩下去你稳输。

这两个游戏都是50%胜率,但结果天差地别。所以关键不是胜率,是赢了拿多少、输了赔多少。


赢大赔小,胜率低也能赚钱

这里有个很多人不知道的事:你不需要高胜率也能赚钱

看个极端例子:

游戏C

  • 10%概率赢1000块
  • 90%概率输100块

期望值 = (10% × 1000) - (90% × 100) = 100 - 90 = +10块

胜率只有10%,但长期玩下去,你每把平均赚10块。

反过来呢?

游戏D

  • 90%概率赢100块
  • 10%概率输2000块

期望值 = (90% × 100) - (10% × 2000) = 90 - 200 = -110块

胜率高达90%,但长期玩下去,你每把平均输110块。

生活中处处如此。

  • 创业:失败率高,但成功一次收益大
  • 稳定工作:成功率高,但每次收益有限
  • 婚姻:离婚率不低,但找到一个好伴侣收益无限

所以别盯着胜率看,要算算期望值。


为什么短期结果说明不了问题

这里有个坑,很多人踩过。

你玩个游戏,连赢10把,觉得自己找到了规律。结果呢?接下来连输20把,心态崩了。

问题出在哪儿?样本量太小

抛硬币,你抛10次可能8次都是正面。但这不代表硬币有问题。你抛1000次还能800次正面,那才有问题。

这个道理叫”大数定律”:玩的次数越多,结果就越接近真实的概率。

生活中到处都是这个坑:

  • 某个股票连涨3天,你觉得会继续涨——赌徒谬误
  • 某个方法试了2次没效果,你就放弃了——样本太小
  • 看到别人成功一次,你就想模仿——幸存者偏差

要判断一个东西到底行不行,得看够大的样本。专业交易员都要模拟交易几个月到一年半,就是为了积累足够的样本量。

急着下结论,那是跟自己过不去。


活着才能赢

有个概念叫”破产风险”(Risk of Ruin),简单说就是:你把钱亏完的概率。

举个栗子:

你有1万块本金,玩一个期望值为正的游戏。

玩法A:每次押100块

  • 就算连输100次,你也只是把本金输光,不会倒欠
  • 破产风险几乎为零

玩法B:每次押5000块

  • 连输2次,游戏就结束了
  • 破产风险明显上升

看出来了吗?同样的游戏,玩法不同,结果天差地别。

这就是为什么老手常说”活得久比赚得多重要”。死在半路上,后面再大的机会也跟你没关系了。

巴菲特是怎么说的?

“投资第一条原则:不要亏钱。”
“投资第二条原则:记住第一条。”

不是因为他胆小,是因为他懂这个数学:亏50%需要涨100%才能回本。


亏损的代价,比你想的大得多

刚才说了:亏50%需要涨100%才能回本。

不是50%,是100%。

1
2
3
4
5
亏10%需要涨11%回本
亏25%需要涨33%回本
亏50%需要涨100%回本
亏75%需要涨300%回本
亏90%需要涨900%回本

这个数学事实意味着什么?

大亏损对复利的伤害是毁灭性的。

看个对比:

  • 策略A:每年稳定赚15%
  • 策略B:一年赚50%,下一年亏30%,这样循环

十年下来:

  • 策略A翻了4倍
  • 策略B只翻了1.8倍

策略B看起来更刺激,但长期输给了稳扎稳打的策略A。

这让我想起一句话:那些追涨杀跌、大起大落的操作,长期来看跑不过稳定的龟速增长。


写在最后

数学这东西,从小我就怕。

但现在我发现,它其实挺有意思的。它不会骗人,也不会跟你耍心眼。它只是静静地告诉你:这样做会赢,那样做会输。

不管你炒股不炒股,这些数学思维都用得上:

  1. 别盯着单次输赢 —— 看长期期望值
  2. 别急着下结论 —— 样本量要够大
  3. 别孤注一掷 —— 活着才能赢
  4. 别忽视大亏损 —— 回本的代价比你想象的大

当然,道理都懂,做到很难。人性这东西,才是最大的对手。

但至少,从今天开始,你可以少一点”我觉得”,多一点”算算看”。


参考文章:The Math of Trading - The Shmuts

RAG 第一篇

后续团队需要做企业大脑,会用到RAG,先比较完整的了解下RAG,把零碎知识补的完整一些,做个记录。

### 标准RAG(Retrieval-Augmented Generation)的运行流程图

标准RAG系统的运行流程可以分为两大阶段:索引阶段(Indexing Phase)(预处理数据)和运行时阶段(Runtime Phase)(查询处理)。以下是详细的流程描述,我会结合常见的流程图示例来说明。RAG的核心是先从外部知识库中检索相关信息,然后用这些信息增强LLM的生成,以减少幻觉并提供更准确的回答。

这个流程图展示了RAG的高层结构:从数据源到最终输出的完整路径。

1. 索引阶段(Indexing Phase):预构建知识库

这一阶段发生在系统启动前,用于将外部数据转化为可检索的形式。通常是离线过程,目的是创建向量数据库以支持高效查询。

  • 步骤1: 数据采集(Data Ingestion)
    从各种来源(如文档、数据库、网页)收集原始数据。这些数据可以是结构化(e.g., SQL表)或非结构化(e.g., PDF、文本文件)。
  • 步骤2: 数据分块(Chunking)
    将长文档切分成小块(chunks),通常每个chunk 100-500 tokens,以匹配嵌入模型的输入限制。分块策略包括固定长度、按句子或语义分块(e.g., 使用LLM检测自然断点)。
  • 步骤3: 嵌入生成(Embedding Generation)
    使用嵌入模型(如BERT、OpenAI的text-embedding-ada-002)将每个chunk转化为向量表示(dense vector,e.g., 768维)。这捕捉语义相似性。
  • 步骤4: 索引存储(Indexing and Storage)
    将向量和对应的原始chunk存储到向量数据库(Vector DB,如FAISS、Pinecone、Milvus)。同时可能添加元数据(如来源、时间戳)。索引使用近似最近邻(ANN)算法如HNSW来加速检索。

这一阶段的输出是一个可查询的向量数据库。

这个图更详细地展示了索引和检索的子步骤,包括分块、嵌入和重排序。

2. 运行时阶段(Runtime Phase):实时查询处理

这是用户交互时的核心流程,对应你之前描述的“检索 + 生成”。

  • 步骤1: 用户输入(User Query Input)
    用户提供查询(query),e.g., “什么是RAG?”。
  • 步骤2: 查询嵌入(Query Embedding)
    使用相同的嵌入模型将query转化为向量。
  • 步骤3: 检索(Retrieval)
    在向量数据库中计算query向量与所有chunk向量的相似度(e.g., 余弦相似度)。返回Top-K个最相似的chunks(原始文本 + 元数据)。可选:重排序(Re-ranking)使用另一个模型(如Cross-Encoder)进一步过滤,提高相关性。
  • 步骤4: 提示构建(Prompt Construction)
    将检索到的Top-K chunks(原始文本)与query拼接成一个Prompt。模板示例:
    1
    2
    3
    4
    系统提示: 你是一个助手,使用以下上下文回答问题。
    上下文: [chunk1] [chunk2] ... [chunkK]
    问题: [query]
    回答:
    注意:这里不直接传递向量,只传递文本。Prompt长度需控制在LLM上下文窗口内(e.g., 8K-128K tokens)。
  • 步骤5: 生成(Generation)
    将Prompt输入LLM(e.g., GPT-4、LLaMA),LLM基于Prompt生成回答。生成过程使用解码算法如beam search或greedy decoding。
  • 步骤6: 输出响应(Output Response)
    返回生成的文本给用户。可选:后处理,如引用来源或验证事实。

结构化和非结构化数据的融合,以及Prompt + Context的输入到LLM。

详细流程的潜在变体和优化

  • 高级检索:可结合关键字搜索(BM25)与向量搜索的混合检索(Hybrid Search),或使用查询重写(Query Rewriting)来扩展query。
  • 多跳检索:对于复杂查询,多次检索(e.g., 先检索实体,再检索关系)。
  • 性能考虑:检索延迟通常<1s,生成取决于LLM大小。常见问题:噪声chunk(无关信息)导致Prompt过长,可用压缩(如摘要)优化。
  • 工具与框架:实现时常用LangChain、Haystack或LlamaIndex。嵌入模型:Sentence Transformers;向量DB:Weaviate。

总结

一句话概括RAG就像给AI装了个”外挂搜索引擎”,问啥先从知识库里找答案,再让AI组织回答。

整个流程就两步大动作:

1️⃣ 准备阶段(一次性干完)

  • 把一堆文档(PDF、网页、数据库)切成小块(像切西瓜一样)
  • 给每块内容**打个”指纹”**(向量嵌入,类似数字DNA)
  • 把这些”指纹+原文”存进搜索引擎(向量数据库)

比喻:就像给图书馆每本书都贴上标签,方便以后快速找书。

2️⃣ 回答问题时(每次提问都这样)

1
2
3
4
用户问问题 → AI先去"图书馆"翻书 → 找到相关内容 → 组织答案给用户
↓ ↓ ↓ ↓
"什么是RAG?" 搜"指纹"匹配 挑出3-5段 "RAG是检索增强生成..."
找相关段落 最相关的书 用这些内容回答

详细拆解

  1. 你问问题 → AI把你的问题也打个”指纹”
  2. AI去搜 → 在图书馆里找跟你问题”指纹”最像的几本书
  3. 挑重点 → 不把整本书都拿出来,只拿相关段落
  4. 拼答案 → 把这些段落+你的问题一起给AI,让它重新组织回答
  5. 输出 → AI用找到的”参考资料”给你准确回答

关键点:

  • 向量(指纹)只用来”找书”不直接给AI看
  • AI真正读的是书里的原文,不是那些数字指纹
  • 找书快(秒级),读懂写回答慢(几秒到几十秒)

为啥要这样?

  • 不RAG:AI只能凭记忆瞎编,容易胡说八道
  • 有RAG:AI像查字典一样先找事实,再组织语言回答

通俗理解:RAG就是让AI**”不瞎编,先查资料”**,回答前先翻书确认事实!

参考文档

https://www.6clicks.com/resources/blog/understanding-rag-retrieval-augmented-generation-explained
https://danielp1.substack.com/p/navigating-retrieval-augmented-generation

架构师学习 第0篇

写在前面

这是一条非常值得尊敬的道路。既然你明确拒绝“速成”,那就意味着你已经做好了打持久战的准备,这正是成为一名真正优秀架构师的前提。

架构师的成长不是线性的,而是螺旋上升的。我为你规划了一条从“点(代码)”到“线(模块)”再到“面(系统)”最后到“体(业务与人)”的系统进阶路径。


第一阶段:筑基——代码与微观设计(The Code)

目标: 写出哪怕过了一年别人也能看懂、敢修改的代码。如果地基不稳,上层架构设计得再漂亮也是空中楼阁。

  1. 编程范式与设计模式:
    • 内容: 除了你之前关注的 23种设计模式,还要深入理解 面向对象编程 (OOP)函数式编程 (FP) 的本质区别与结合。
    • 必修: SOLID 原则(架构师的宪法)。
    • 书籍: 《设计模式》(GoF)、《代码整洁之道》、《重构》。
  2. 数据结构与算法的工程应用:
    • 内容: 不仅仅是刷 LeetCode,而是理解时间/空间复杂度对系统性能的深远影响。
    • 场景: 为什么要用 B+ 树做数据库索引?为什么 Redis 用跳表?HashMap 的扩容机制如何影响延迟?
  3. 语言底层机制:
    • 内容: 无论你用 Java、Go 还是 Python,必须精通至少一门语言的底层。包括内存模型(Memory Model)、垃圾回收(GC)、并发模型(Thread vs Goroutine)。

第二阶段:深入——系统原理与中间件(The System)

目标: 不再把数据库和消息队列当“黑盒”使用,而是理解其内部原理,知道它们的极限在哪里。

  1. 操作系统与网络:
    • 内容: I/O 模型(BIO/NIO/AIO/Epoll/IO_URING),TCP/IP 协议栈(三次握手、拥塞控制),Zero-Copy 技术。
    • 思考: 为什么 Nginx 性能这么高?Netty 是怎么做到高并发的?
  2. 数据库内核级原理:
    • 内容: 事务隔离级别(脏读/幻读底层实现)、锁机制(MVCC)、存储引擎(LSM-Tree vs B-Tree)、WAL(预写日志)。
    • 必修书籍: 《数据密集型应用系统设计》 (DDIA) —— 再次强调,这是此阶段的必读书。
  3. 分布式理论基石:
    • 内容: CAP 定理(不仅仅是背概念,而是理解权衡)、BASE 理论、分布式共识算法(Paxos, Raft, ZAB)。
    • 场景: 当网络分区发生时,你的系统是保可用性(AP)还是保一致性(CP)?

第三阶段:宏观——架构模式与业务建模(The Architecture)

目标: 跳出技术细节,开始从业务视角拆解系统,解决复杂性问题。

  1. 架构风格演进:

    • 内容: 单体 -> SOA -> 微服务 -> Service Mesh -> Serverless。
    • 关键: 重点不是学习怎么搭建微服务,而是学习什么时候不该用微服务。理解每种架构风格的优缺点(Trade-offs)。
    • 书籍: 《软件架构模式》(O’Reilly)、《微服务架构设计模式》。
  2. 领域驱动设计 (DDD):

    • 内容: 战略设计(限界上下文、通用语言、子域划分)与战术设计(聚合根、实体、值对象)。
    • 价值: 这是架构师与业务方沟通的桥梁,解决“业务复杂性”的终极武器。
    • 书籍: 《领域驱动设计》(蓝皮书 - 难读但经典)、《实现领域驱动设计》(红皮书 - 实战推荐)。
  3. 高可用与高并发架构:

    • 内容: 缓存策略(穿透/雪崩/击穿)、分库分表、读写分离、异地多活、限流熔断降级。
    • 实战: 试着去推演像“双11”秒杀系统或 12306 订票系统的架构设计。

第四阶段:落地——工程化与治理(The Engineering)

目标: 架构不仅是画图,更是要保证系统能稳定、高效地运行和迭代。

  1. DevOps 与 SRE(站点可靠性工程):
    • 内容: CI/CD 流水线设计、容器化(Docker/K8s)、可观测性(Logging, Tracing, Metrics - 如 Prometheus/Grafana/ELK)。
    • 思维: 架构设计必须包含“可测试性”和“可监控性”。
  2. 技术选型与决策:
    • 能力: 如何在两个看起来差不多的技术(比如 RabbitMQ vs Kafka,PostgreSQL vs MySQL)中做选择?需要建立多维度的评估模型(成本、运维难度、社区活跃度、适用场景)。
  3. 遗留系统治理:
    • 内容: 如何在不停止业务的情况下重构一个跑了10年的老系统?这是最考验架构师功力的地方。

第五阶段:升维——软技能与商业思维(The Wisdom)

目标: 从“解决技术问题”转变为“解决商业问题”。

  1. 沟通与领导力:
    • 架构师需要向老板解释为什么这个功能要开发一个月,向团队解释为什么要用这个新技术。
    • 学会画图(UML, C4 Model),学会写文档(ADR)。
  2. 商业敏感度:
    • 技术是为了服务业务的。理解成本(Cloud Cost Optimization)、理解上市时间(Time to Market)。
    • 最高境界: 甚至能指出业务流程的不合理之处,用技术反向驱动业务创新。

总结:如何执行?

不要试图并行学习所有内容。建议采用 “T型人才” 策略:

  1. 竖线(深): 先在某一个领域扎得足够深(比如你精通 Java 并发编程,或者精通 MySQL 调优)。这能建立你的技术自信和威信。
  2. 横线(广): 然后慢慢扩展知识面,了解前端、运维、大数据、AI 等领域的基础概念。

现在的你,既然想从设计模式入手,那就先在第一阶段扎实待上几个月,把《Head First 设计模式》吃透,并在工作中强迫自己去识别和重构坏代码。

这条路很长,很苦,但风景最好。加油。

Linux netplan

初创团队,各方面有限,我们是saas+硬件,但是我们只有单个公网IP、一个一级域名,所以为了短时间适配生产、研发、测试三个环境,同时支持SaaS+硬件通信,我们需要做前端入口的流量管理,团队的小伙伴选择了OPNsense。
这一篇是我为了了解该技术方案而简单整理的。

# 解决Ubuntu Server 24.04删除网卡后的Netplan问题

引言

在Ubuntu Server 24.04中,Netplan是默认的网络配置工具,使用YAML文件管理网络设置。最近,我在虚拟机中配置了双网卡(一张内网,一张外网),但删除一张网卡后,网络无法正常工作。经过调试,我通过手动更新Netplan配置文件解决了问题,以下是我的经验分享。

问题描述

我的虚拟机最初配置了两张网卡:enp0s3(外网,静态IP)用于访问外部网络,enp0s8(内网,DHCP)用于本地通信。删除enp0s8后,运行netplan apply没有生效,ip a显示enp0s3未正确分配IP。日志(journalctl -u systemd-networkd)提示Netplan仍尝试配置已删除的网卡。

1
2
4: ens38: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:30:26:8c:78:57 brd ff:ff:ff:ff:ff:ff

解决方案

以下是解决步骤:

  1. 启动网卡
1
ip link set ens38 up
  1. 检查现有Netplan配置
    查看/etc/netplan/目录中的配置文件(通常为00-installer-config.yaml)。原始配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    network:
    version: 2
    ethernets:
    enp0s3:
    dhcp4: no
    addresses: [172.162.1.100/24]
    gateway4: 172.162.1.1
    nameservers:
    addresses: [8.8.8.8, 8.8.4.4]
    enp0s8:
    dhcp4: yes
  2. 更新配置文件
    删除enp0s8相关配置,仅保留enp0s3。修改后的文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    network:
    version: 2
    ethernets:
    enp0s3:
    dhcp4: no
    addresses: [172.162.1.100/24]
    gateway4: 172.162.1.1
    nameservers:
    addresses: [8.8.8.8, 8.8.4.4]

    注意:确保YAML缩进为2个空格,避免格式错误。

  3. 验证并应用配置
    检查配置语法:

    1
    sudo netplan --debug apply

    确认无错误后,应用配置:

    1
    sudo netplan apply
  4. 测试网络
    验证接口状态和连通性:

    1
    2
    ip a
    ping 8.8.8.8
  5. 检查日志
    如果仍不生效,查看日志:

    1
    journalctl -u systemd-networkd

总结

删除虚拟机网卡后,Netplan不会自动更新配置,需手动移除无效网卡的配置条目。关键是检查YAML文件、确保格式正确,并使用netplan --debug apply定位问题。在多网卡场景下,建议定期验证网卡名称(ip link)和配置文件一致性。遇到类似问题?欢迎留言分享你的经验!

网络安全 软路由

初创团队,各方面有限,我们是saas+硬件,但是我们只有单个公网IP、一个一级域名,所以为了短时间适配生产、研发、测试三个环境,同时支持SaaS+硬件通信,我们需要做前端入口的流量管理,团队的小伙伴选择了OPNsense。
这一篇是我为了了解该技术方案而简单整理的。

OPNsense的防火墙模块基于FreeBSD的pf(Packet Filter),提供了强大的NAT功能,包括Port Forward(转发规则)、Outbound NAT(出站NAT)和NPTv6(IPv6前缀转换)。从我的截图可以看到,Port Forward页面列出了WAN到LAN的TCP流量规则(如Web服务和RDP),而Outbound NAT页面显示了自动生成的出站规则。这些功能让我在单一公网IP下实现了多环境隔离和外部访问。
NAT策略:Port Forward与Outbound NAT的协同
最初我以为Port Forward能解决所有需求,但实践证明,仅靠它处理入站流量是不够的。Outbound NAT才是出站流量的关键,尤其在多VLAN和硬件通信场景中,两者需协同工作。

Port Forward:

用途:处理入站流量,将公网端口映射到内部IP。例如,我配置了WAN:443到192.168.10.10:443,让外部通过prod.example.com访问生产环境。
局限:不管理出站流量,硬件向SaaS发送数据时需依赖Outbound NAT。
配置:Firewall → NAT → 转发,添加规则(Protocol: TCP;Destination: WAN address:443;Redirect to: 192.168.10.10:443)。

Outbound NAT:

位置:Firewall → NAT → Outbound,当前为自动模式,自动为WAN出站流量分配公网IP。
优化:切换到手动模式,为每个VLAN设置规则,确保出站流量隔离。
我的经验:自动模式曾因端口冲突导致硬件API请求失败,切换到手动后问题解决。

找到并配置Outbound NAT
Outbound NAT是管理出站流量的关键,位于Firewall → NAT → Outbound页面。从我的截图可以看到,默认使用“自动生成规则”,为LAN和Loopback网段分配WAN地址。但对于复杂场景,我切换到手动模式以满足需求。

手动配置:
点击“切换到手动规则”(Switch to Manual Outbound NAT rule generation),保存。
添加规则:Interface: WAN;Source: 192.168.10.0/24(生产),Translation: WAN地址,描述:“Production Outbound”。
依次为研发(192.168.20.0/24)和测试(192.168.30.0/24)设置规则。
硬件场景:为生产VLAN的硬件(如192.168.10.10)添加规则,确保其API请求(如curl https://prod.example.com/api/v1/data)顺利出站。

优化:启用NAT Reflection(Firewall → Advanced),让内部设备用公网IP访问暴露服务。
经验:备份配置(System → Config History)后切换模式,避免误操作。日志监控(Firewall → Log Files)帮助我定位流量问题。

下一步行动项

实战配置:综合NAT策略

入站:Port Forward规则处理prod.example.com的HTTPS请求,映射到生产环境的Web服务器。
出站:手动Outbound NAT为每个VLAN配置规则,保障出站流量隔离。我的硬件通过生产VLAN的规则上传数据,测试显示吞吐量稳定。
硬件场景:结合Port Forward和Outbound NAT,硬件既能接收SaaS命令(入站),又能上传数据(出站),单IP利用率显著提升。
NPTv6(未来扩展):当前用IPv4,但NPTv6为IPv6网络的前缀转换提供了可能,适合ISP支持IPv6时升级。

硬件通信:NAT与子域名的结合
我的SaaS硬件通过prod.example.com与服务通信,NAT策略确保其双向通信。

入站:Port Forward映射WAN:443到192.168.10.10:443,配合HAProxy根据子域名分发流量。
出站:Outbound NAT规则让硬件出站请求使用公网IP,日志显示连接正常。
安全:用别名(Firewall → Aliases)定义硬件IP范围,限制未授权访问。
实践:我用curl测试硬件请求,确认数据成功上传到SaaS。

VPN: 团队vpn后续我会研究是否能用OPNsense

架构师学习 第4篇

写在前面

说实话,刚开始接触SOLID原则的时候,我觉得这些都是些”虚头巴脑”的理论。直到临时接手一个离职同事的代码——一个类上千行,改一行bug到处冒,而且代码完全没有秩序,我才真正体会到这些原则的价值。

SOLID原则是我CLAUDE.md其中一个规范,是由Robert C. Martin(Uncle Bob)提出的五个面向对象设计原则。简单来说,它们能帮我写出更好维护、更好扩展、更好测试的代码。

因为在软件开发的早期,我们往往更关注功能的实现,而忽视了代码的设计质量。随着项目规模的增长,糟糕的代码设计会变得越来越难以维护,每一次修改都可能引发意想不到的问题。SOLID原则正是为了解决这些问题而诞生的。

为什么要学习SOLID原则?

SOLID原则能够帮助我们:

  1. 降低代码耦合度:让模块之间的依赖关系更加清晰
  2. 提高代码可维护性:单一职责让修改更加聚焦
  3. 增强代码可扩展性:开闭原则让功能扩展更安全
  4. 改善代码可测试性:依赖倒置让单元测试更容易编写

学习资料

单一职责原则(Single Responsibility Principle,SRP)

原则定义

一个类应该只有一个引起它变化的原因,换句话说,一个类应该只有一个职责。

直观理解

想象一个瑞士军刀,它有太多功能:刀、剪刀、开瓶器等。虽然功能强大,但当你只需要用刀的时候,带着整个军刀就显得笨重了。同样,一个类承担太多职责时,任何职责的变化都可能影响其他职责,导致系统变得脆弱。

反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 违反单一职责原则的用户服务类
* 该类同时负责用户业务逻辑和日志记录
*/
public class UserService {

// 职责1:用户管理
public void register(String username, String password) {
// 注册逻辑
System.out.println("用户注册:" + username);
}

public void login(String username, String password) {
// 登录逻辑
System.out.println("用户登录:" + username);
}

// 职责2:日志记录 - 这不应该属于用户服务
public void logToFile(String message) {
try {
FileWriter writer = new FileWriter("app.log", true);
writer.write(message + "\n");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}

// 职责3:邮件发送 - 这也不应该属于用户服务
public void sendEmail(String to, String subject, String content) {
System.out.println("发送邮件给:" + to);
System.out.println("主题:" + subject);
System.out.println("内容:" + content);
}
}
问题分析: 1. UserService类承担了用户管理、日志记录、邮件发送三个职责 2. 修改日志格式需要修改UserService 3. 更换邮件服务提供商需要修改UserService 4. 这违反了单一职责原则,导致类的变化原因过多

正例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 符合单一职责原则的用户服务类
* 只负责用户相关的业务逻辑
*/
public class UserService {
private Logger logger;
private EmailService emailService;

public UserService(Logger logger, EmailService emailService) {
this.logger = logger;
this.emailService = emailService;
}

// 只关注用户管理职责
public void register(String username, String password) {
logger.log("用户注册:" + username);
// 注册逻辑
emailService.sendEmail(username, "注册成功", "欢迎注册");
}

public void login(String username, String password) {
logger.log("用户登录:" + username);
// 登录逻辑
}
}

/**
* 专门的日志服务类
*/
public class Logger {
public void log(String message) {
try {
FileWriter writer = new FileWriter("app.log", true);
writer.write(message + "\n");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 专门的邮件服务类
*/
public class EmailService {
public void sendEmail(String to, String subject, String content) {
System.out.println("发送邮件给:" + to);
System.out.println("主题:" + subject);
System.out.println("内容:" + content);
}
}
重构收益: 1. 每个类都有明确的单一职责 2. 修改日志实现只需修改Logger类 3. 更换邮件服务只需修改EmailService类 4. UserService类保持稳定,不受其他职责变化影响

开闭原则(Open-Closed Principle,OCP)

原则定义

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭

这意味着当我们需要添加新功能时,应该通过扩展现有代码来实现,而不是修改已有的代码。

直观理解

想象一个插线板,它有多个插座。当你需要使用新电器时,你只需要插上新的插头,而不需要拆开插线板重新布线。开闭原则就是让我们的代码像插线板一样,能够轻松”插入”新功能而不需要修改核心代码。

反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 违反开闭原则的折扣计算器
* 每次新增折扣类型都需要修改这个类
*/
public class DiscountCalculator {

public double calculateDiscount(String discountType, double price) {
switch (discountType) {
case "NONE":
return price;
case "TEN_PERCENT":
return price * 0.9;
case "TWENTY_PERCENT":
return price * 0.8;
// 每次新增折扣类型都需要在这里添加新的case
// 这违反了开闭原则
default:
return price;
}
}
}
违反开闭原则的后果: 1. 每次添加新的折扣类型都需要修改DiscountCalculator类 2. 修改已有代码可能引入新的bug 3. 需要重新测试整个折扣计算功能 4. 违反了对修改封闭的原则

正例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 折扣策略接口
*/
public interface DiscountStrategy {
double calculate(double price);
}

/**
* 无折扣策略
*/
public class NoDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price;
}
}

/**
* 10%折扣策略
*/
public class TenPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.9;
}
}

/**
* 20%折扣策略
*/
public class TwentyPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.8;
}
}

/**
* 符合开闭原则的折扣计算器
*/
public class DiscountCalculator {
private DiscountStrategy discountStrategy;

public DiscountCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}

public double calculate(double price) {
return discountStrategy.calculate(price);
}

// 可以动态切换折扣策略
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
}

// 使用示例
public class Main {
public static void main(String[] args) {
DiscountCalculator calculator = new DiscountCalculator(new NoDiscount());
System.out.println("无折扣价格:" + calculator.calculate(100));

// 切换到10%折扣
calculator.setDiscountStrategy(new TenPercentDiscount());
System.out.println("10%折扣后价格:" + calculator.calculate(100));

// 切换到20%折扣
calculator.setDiscountStrategy(new TwentyPercentDiscount());
System.out.println("20%折扣后价格:" + calculator.calculate(100));

// 添加新的折扣类型只需创建新的策略类,无需修改DiscountCalculator
calculator.setDiscountStrategy(new ThirtyPercentDiscount());
System.out.println("30%折扣后价格:" + calculator.calculate(100));
}
}

/**
* 新增30%折扣策略 - 无需修改任何现有代码
*/
public class ThirtyPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.7;
}
}
重构收益: 1. 添加新的折扣类型只需创建新的策略类 2. DiscountCalculator类无需修改,符合开闭原则 3. 通过策略模式实现了对扩展开放、对修改封闭 4. 每个折扣策略都是独立的,易于测试和维护

里氏替换原则(Liskov Substitution Principle,LSP)

原则定义

所有引用基类的地方必须能够透明地使用其子类的对象,子类可以替换父类出现在父类能够出现的任何地方,而不破坏程序的正确性。

直观理解

如果你有一个正方形和一个长方形,从几何上讲,正方形是特殊的长方形。但在编程中,如果让正方形继承长方形类,可能会出现问题。因为正方形的长宽必须相等,这违反了长方形”长宽可以不同”的基本约定。

反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 长方形类
*/
public class Rectangle {
protected double width;
protected double height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

public void setWidth(double width) {
this.width = width;
}

public void setHeight(double height) {
this.height = height;
}

public double getWidth() {
return width;
}

public double getHeight() {
return height;
}

public double getArea() {
return width * height;
}
}

/**
* 正方形类继承长方形
* 这违反了里氏替换原则
*/
public class Square extends Rectangle {

public Square(double size) {
super(size, size);
}

@Override
public void setWidth(double width) {
this.width = width;
this.height = width; // 正方形长宽必须相等
}

@Override
public void setHeight(double height) {
this.width = height; // 正方形长宽必须相等
this.height = height;
}
}

/**
* 测试类
*/
public class LSPTest {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(5, 10);
System.out.println("长方形面积:" + rectangle.getArea()); // 50

rectangle.setWidth(10);
rectangle.setHeight(5);
System.out.println("长方形面积:" + rectangle.getArea()); // 50

// 使用正方形替换长方形
Rectangle square = new Square(5);
System.out.println("正方形面积:" + square.getArea()); // 25

square.setWidth(10);
square.setHeight(5);
System.out.println("正方形面积:" + square.getArea()); // 25, 而不是50!

// 这里违反了里氏替换原则
// 当使用Square替换Rectangle时,程序的行为发生了变化
}
}
违反里氏替换原则的问题: 1. Square继承Rectangle后,破坏了Rectangle的行为约定 2. 当使用Square替换Rectangle时,程序结果不一致 3. 客户端代码无法正确预测继承后的行为 4. 这种继承关系在设计上就是错误的

正例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 形状接口
*/
public interface Shape {
double getArea();
}

/**
* 长方形类实现形状接口
*/
public class Rectangle implements Shape {
private double width;
private double height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

public void setWidth(double width) {
this.width = width;
}

public void setHeight(double height) {
this.height = height;
}

@Override
public double getArea() {
return width * height;
}
}

/**
* 正方形类实现形状接口
*/
public class Square implements Shape {
private double size;

public Square(double size) {
this.size = size;
}

public void setSize(double size) {
this.size = size;
}

@Override
public double getArea() {
return size * size;
}
}

/**
* 形状工具类 - 可以处理任何实现Shape接口的对象
*/
public class ShapeUtils {
public static void printArea(Shape shape) {
System.out.println("形状面积:" + shape.getArea());
}
}

/**
* 测试类
*/
public class LSPTestCorrect {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(5, 10);
Square square = new Square(5);

// Rectangle和Square可以透明替换
ShapeUtils.printArea(rectangle); // 形状面积:50.0
ShapeUtils.printArea(square); // 形状面积:25.0

// 使用多态
List<Shape> shapes = Arrays.asList(rectangle, square);
for (Shape shape : shapes) {
ShapeUtils.printArea(shape);
}
}
}
重构收益: 1. Rectangle和Square都实现Shape接口,各自保持独立性 2. 任何使用Shape的地方都可以透明地使用Rectangle或Square 3. 避免了不合理的继承关系 4. 符合里氏替换原则,保证程序行为的正确性

接口隔离原则(Interface Segregation Principle,ISP)

原则定义

客户端不应该被迫依赖于它不使用的接口,接口应该被拆分为更小和更具体的部分,这样客户端只需要知道它们所需的部分。

直观理解

想象一个万能遥控器,上面有电视、空调、音响等各种设备的按钮。当你只需要控制电视时,面对这么多无关的按钮会很困扰。接口隔离原则就是让每个接口都专注于特定的功能,避免”胖接口”。

反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 臃肿的机器接口 - 包含所有可能的操作
*/
public interface Machine {
void print(String document);
void fax(String document);
void scan(String document);
void photocopy(String document);
}

/**
* 打印机类 - 被迫实现不需要的方法
*/
public class OldPrinter implements Machine {

@Override
public void print(String document) {
System.out.println("打印:" + document);
}

@Override
public void fax(String document) {
// 老式打印机不支持传真,但被迫实现这个方法
throw new UnsupportedOperationException("不支持传真功能");
}

@Override
public void scan(String document) {
// 老式打印机不支持扫描,但被迫实现这个方法
throw new UnsupportedOperationException("不支持扫描功能");
}

@Override
public void photocopy(String document) {
// 老式打印机不支持复印,但被迫实现这个方法
throw new UnsupportedOperationException("不支持复印功能");
}
}
违反接口隔离原则的问题: 1. Machine接口过于臃肿,包含了太多操作 2. OldPrinter只需要打印功能,但被迫实现其他方法 3. 客户端可能调用不支持的方法,导致运行时异常 4. 接口设计不够灵活,无法适应不同的设备组合

正例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* 打印接口
*/
public interface Printer {
void print(String document);
}

/**
* 传真接口
*/
public interface Fax {
void fax(String document);
}

/**
* 扫描接口
*/
public interface Scanner {
void scan(String document);
}

/**
* 复印接口
*/
public interface Photocopier {
void photocopy(String document);
}

/**
* 简单打印机 - 只实现打印功能
*/
public class SimplePrinter implements Printer {
@Override
public void print(String document) {
System.out.println("打印:" + document);
}
}

/**
* 多功能打印机 - 实现多个接口
*/
public class MultiFunctionPrinter implements Printer, Fax, Scanner, Photocopier {

@Override
public void print(String document) {
System.out.println("打印:" + document);
}

@Override
public void fax(String document) {
System.out.println("传真:" + document);
}

@Override
public void scan(String document) {
System.out.println("扫描:" + document);
}

@Override
public void photocopy(String document) {
System.out.println("复印:" + document);
}
}

/**
* 打印扫描一体机 - 只实现需要的接口
*/
public class PrintScanCombo implements Printer, Scanner {

@Override
public void print(String document) {
System.out.println("打印:" + document);
}

@Override
public void scan(String document) {
System.out.println("扫描:" + document);
}
}

// 使用示例
public class ISPTest {
public static void main(String[] args) {
// 简单打印机只需要Printer接口
Printer simplePrinter = new SimplePrinter();
simplePrinter.print("简单文档");

// 多功能打印机可以实现所有功能
MultiFunctionPrinter mfp = new MultiFunctionPrinter();
usePrinter(mfp);
useFax(mfp);
useScanner(mfp);

// 打印扫描一体机只需要打印和扫描功能
PrintScanCombo combo = new PrintScanCombo();
usePrinter(combo);
useScanner(combo);
}

// 客户端只需要依赖它需要的接口
public static void usePrinter(Printer printer) {
printer.print("使用打印机");
}

public static void useFax(Fax fax) {
fax.fax("使用传真机");
}

public static void useScanner(Scanner scanner) {
scanner.scan("使用扫描仪");
}
}
重构收益: 1. 接口被拆分为多个小而专注的接口 2. 客户端只依赖它需要的接口,避免不必要的方法 3. 实现类可以选择性地实现需要的接口 4. 符合接口隔离原则,提高了系统的灵活性和可维护性

依赖倒置原则(Dependency Inversion Principle,DIP)

原则定义

高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

直观理解

想象一个公司组织架构,总经理(高层)不应该直接管理员工(低层)的每一个具体工作。相反,总经理应该制定标准和接口,员工按照这些标准工作。这样,更换员工不会影响公司的整体运作。

反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* MySQL数据库连接类 - 低层模块
*/
public class MySQLConnection {
public void connect() {
System.out.println("连接到MySQL数据库");
}

public void executeQuery(String sql) {
System.out.println("在MySQL中执行查询:" + sql);
}
}

/**
* 用户服务类 - 高层模块
* 直接依赖具体的MySQLConnection
*/
public class UserService {
private MySQLConnection dbConnection;

public UserService() {
this.dbConnection = new MySQLConnection(); // 高层依赖低层具体实现
}

public void getUser(String userId) {
dbConnection.connect();
dbConnection.executeQuery("SELECT * FROM users WHERE id = " + userId);
}
}
违反依赖倒置原则的问题: 1. UserService直接依赖MySQLConnection具体类 2. 如果要更换数据库(如PostgreSQL),需要修改UserService 3. 高层模块被低层模块的具体实现所束缚 4. 难以进行单元测试(无法mock数据库连接)

正例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* 数据库连接接口 - 抽象层
*/
public interface DatabaseConnection {
void connect();
void executeQuery(String sql);
}

/**
* MySQL数据库连接实现
*/
public class MySQLConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("连接到MySQL数据库");
}

@Override
public void executeQuery(String sql) {
System.out.println("在MySQL中执行查询:" + sql);
}
}

/**
* PostgreSQL数据库连接实现
*/
public class PostgreSQLConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("连接到PostgreSQL数据库");
}

@Override
public void executeQuery(String sql) {
System.out.println("在PostgreSQL中执行查询:" + sql);
}
}

/**
* MongoDB数据库连接实现
*/
public class MongoDBConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("连接到MongoDB数据库");
}

@Override
public void executeQuery(String sql) {
System.out.println("在MongoDB中执行查询:" + sql);
}
}

/**
* 符合依赖倒置原则的用户服务类
* 依赖抽象接口,而不是具体实现
*/
public class UserService {
private DatabaseConnection dbConnection;

// 通过构造器注入依赖
public UserService(DatabaseConnection dbConnection) {
this.dbConnection = dbConnection; // 依赖抽象而非具体实现
}

public void getUser(String userId) {
dbConnection.connect();
dbConnection.executeQuery("SELECT * FROM users WHERE id = " + userId);
}

// 也可以通过setter注入
public void setDbConnection(DatabaseConnection dbConnection) {
this.dbConnection = dbConnection;
}
}

// 使用示例
public class DIPTest {
public static void main(String[] args) {
// 可以轻松切换不同的数据库实现
DatabaseConnection mysqlConnection = new MySQLConnection();
DatabaseConnection pgConnection = new PostgreSQLConnection();
DatabaseConnection mongoConnection = new MongoDBConnection();

UserService userService1 = new UserService(mysqlConnection);
userService1.getUser("1");

UserService userService2 = new UserService(pgConnection);
userService2.getUser("2");

UserService userService3 = new UserService(mongoConnection);
userService3.getUser("3");

// 运行时切换数据库实现
userService1.setDbConnection(mongoConnection);
userService1.getUser("4");
}
}
重构收益: 1. UserService依赖DatabaseConnection抽象接口,而非具体实现 2. 可以轻松切换不同的数据库实现,无需修改UserService 3. 高层模块和低层模块都依赖抽象,降低了耦合度 4. 便于单元测试,可以轻松mock DatabaseConnection

面试中被问到SOLID原则怎么办?

有次在Reddit上看到一个吐槽帖,说某公司招聘人员让候选人”凭空解释5个SOLID原则”,结果博主直接拒绝回答。其实这问题挺常见的,尤其是面试初级到中级岗位的时候。

答题思路

别死记硬背。先说总体理解,然后逐个简述:

原则 一句话解释
S 一个类只做一件事
O 加功能别改老代码,用扩展加
L 子类能完美替代父类
I 接口别太臃肿,该拆就拆
D 依赖接口,别依赖具体实现

然后补一句:”这些原则我实际项目里也在用,比如最近做的XX项目,用策略模式实现XX功能,就体现了开闭原则…”

这样回答既展示了理论知识,又证明了实践经验。

记忆技巧

  • SRP:Single Responsibility → 单一职责
  • OCP:Open/Closed → 对扩展开放,对修改封闭
  • LSP:Liskov Substitution → 里氏替换
  • ISP:Interface Segregation → 接口隔离
  • DIP:Dependency Inversion → 依赖倒置(依赖抽象)

总结

SOLID原则是面向对象设计的基石,它们相互关联、相互补充。掌握这些原则能够帮助我们设计出更加优雅、灵活、可维护的软件系统。

SOLID原则的关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────┐
│ SOLID 原则 │
├─────────────────────────────────────────────────┤
│ │
│ SRP ──────── 单一职责 ───────── 一个类只有一个职责 │
│ ↓ │
│ OCP ──────── 开闭原则 ────── 对扩展开放,对修改封闭 │
│ ↓ │
│ LSP ──────── 里氏替换 ──── 子类可以透明替换父类 │
│ ↓ │
│ ISP ──────── 接口隔离 ──── 接口应该小而专注 │
│ ↓ │
│ DIP ──────── 依赖倒置 ── 依赖抽象而非具体实现 │
│ │
└─────────────────────────────────────────────────┘

实践建议

如何在项目中应用SOLID原则:

  1. 逐步重构:不要试图一次性重构所有代码,逐步应用这些原则
  2. 识别坏味道:学会识别违反SOLID原则的代码坏味道
  3. 设计优先:在编写新代码时,优先考虑SOLID原则
  4. 团队共识:确保团队成员都理解并认同这些原则
  5. 适度应用:不要过度设计,根据实际情况灵活应用
重要提示: SOLID原则是指导原则,不是绝对的规则。在实际项目中,需要根据具体情况灵活应用。过度遵循这些原则可能导致过度设计,增加系统复杂度。

架构师学习 第3篇

设计模式 (Design Patterns)

  • 核心概念与原则

    • 定义:一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
    • 目的:提高代码的可重用性,使代码更容易被他人理解,保证代码可靠性。
    • 核心原则
      • 针对接口编程:客户无须知道对象的特定类型,只需知道对象有客户所期望的接口。
      • 优先使用对象组合:优先使用对象组合(黑箱复用),而不是类继承(白箱复用)。
    • MVC模式案例:Smalltalk中的MVC(模型/视图/控制器)体现了观察者、组合和策略模式的综合应用。
  • 一、创建型模式 (Creational Patterns)

    • 关注点:对象的创建过程,将对象的创建与使用分离。
    • 1. 工厂模式 (Factory)
      • **简单工厂 (Simple Factory)**:(非GoF标准,但常用) 定义一个用于创建对象的接口,由工厂类决定创建哪一种产品实例(如“司机开车”的例子)。
      • **工厂方法 (Factory Method)**:定义创建对象的接口,让子类决定实例化哪一个类。使实例化延迟到子类。
      • **抽象工厂 (Abstract Factory)**:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
    • 2. 单例模式 (Singleton)
      • 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
      • 实现方式
        • 饿汉式:类加载时初始化。
        • 懒汉式:第一次使用时初始化(需注意线程同步)。
        • 注册表方式:通过HashMap维护实例。
    • 3. 建造者模式 (Builder)
      • 定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
      • 角色:指导者 (Director)、抽象建造者 (Builder)、具体建造者 (ConcreteBuilder)、产品 (Product)。
    • 4. 原型模式 (Prototype)
      • 定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
      • 特点:利用Java的clone()方法,分为深克隆和浅克隆。
  • 二、结构型模式 (Structural Patterns)

    • 关注点:类或对象的组合,形成更大的结构。
    • 1. 适配器模式 (Adapter)
      • 定义:将一个类的接口转换成客户希望的另外一个接口,解决接口不兼容问题。
      • 分类:类适配器(继承)、对象适配器(组合)。
    • 2. 桥接模式 (Bridge)
      • 定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
      • 应用:如Java AWT框架,将组件与其在不同操作系统下的实现分离。
    • 3. 组合模式 (Composite)
      • 定义:将对象组合成树形结构以表示“部分-整体”的层次结构,使用户对单个对象和组合对象的使用具有一致性。
      • 应用:文件系统、JUnit中的TestCase与TestSuite。
    • 4. 装饰模式 (Decorator)
      • 定义:动态地给一个对象添加一些额外的职责。比生成子类更为灵活。
      • 特点:透明围栏,客户分不出组件和装饰后的组件的区别。
    • 5. 外观/门面模式 (Facade)
      • 定义:为子系统中的一组接口提供一个一致的界面,定义高层接口使子系统更易使用。
      • 目的:降低客户与子系统之间的耦合。
    • 6. 享元模式 (Flyweight)
      • 定义:运用共享技术有效地支持大量细粒度的对象。
      • 关键:区分内蕴状态(共享)和外蕴状态(不共享)。
    • 7. 代理模式 (Proxy)
      • 定义:为其他对象提供一种代理以控制对这个对象的访问。
      • 类型:远程代理、虚拟代理、保护代理、智能引用等。
  • 三、行为型模式 (Behavioral Patterns)

    • 关注点:对象间的交互和职责分配。
    • 1. 责任链模式 (Chain of Responsibility)
      • 定义:使多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有对象处理它。
    • 2. 命令模式 (Command)
      • 定义:将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;支持排队、日志和撤销操作。
    • 3. 解释器模式 (Interpreter)
      • 定义:给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。
    • 4. 迭代器模式 (Iterator)
      • 定义:提供一种方法顺序访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
    • 5. 中介者/调停者模式 (Mediator)
      • 定义:用一个中介对象来封装一系列的对象交互,使各对象不需要显式地相互引用。
    • 6. 备忘录模式 (Memento)
      • 定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
    • 7. 观察者模式 (Observer)
      • 定义:定义对象间的一种一对多的依赖关系,当一个对象状态改变时,所有依赖者都得到通知并自动更新。
      • 模型:推模型(广播详情) vs 拉模型(观察者主动获取)。
    • 8. 状态模式 (State)
      • 定义:允许一个对象在其内部状态改变时改变它的行为。
      • 对比:与策略模式结构相似,但意图不同(状态是内在变化,策略是外部选择)。
    • 9. 策略模式 (Strategy)
      • 定义:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。
    • 10. 模板方法模式 (Template Method)
      • 定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
    • 11. 访问者模式 (Visitor)
      • 定义:表示一个作用于某对象结构中的各元素的操作,使你可以在不改变各元素的类的前提下定义新操作。
      • 机制:依赖于“双重分派”技术。

常用的设计模式(10个)

一、 创建型模式 (Creational Patterns)

这类模式主要关注对象的创建过程,旨在将对象的创建与使用分离。

1. 单例模式 (Singleton)

  • 概念:保证一个类仅有一个实例,并提供一个访问它的全局访问点。通常用于代表系统中本质上唯一的组件。
  • 示例
    • 系统资源管理:如文件系统、打印机假脱机程序或窗口管理器,在系统中通常只应有一个实例存在。
    • 代码实现:可以通过私有化构造函数,并提供一个静态方法(如 getInstance)来返回唯一的实例(可以是饿汉式或懒汉式实现)。

2. 工厂方法模式 (Factory Method)

  • 概念:定义一个用于创建对象的接口,让子类决定实例化哪一个类。这使得一个类的实例化延迟到其子类。
  • 示例
    • 文档应用框架:一个抽象的 Application 类负责管理文档,但它不知道具体的文档类(如 DrawingDocumentTextDocument)。它定义一个 CreateDocument 的工厂方法,由子类来实现具体的文档创建逻辑。
    • 暴发户坐车:在这个例子中,工厂方法模式用来创建不同品牌的汽车(如奔驰、宝马),不同的司机子类(工厂子类)负责创建对应的汽车实例。

3. 抽象工厂模式 (Abstract Factory)

  • 概念:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。主要用于处理产品族的问题。
  • 示例
    • 多视感标准 UI:支持多种界面风格(如 Motif 和 Presentation Manager)的工具包。定义一个 WidgetFactory 接口,包含创建滚动条、窗口、按钮的操作。具体的子类 MotifWidgetFactory 创建 Motif 风格的组件,而 PMWidgetFactory 创建 PM 风格的组件,客户仅需通过抽象接口与工厂交互。

二、 结构型模式 (Structural Patterns)

这类模式关注类和对象的组合。

4. 适配器模式 (Adapter)

  • 概念:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  • 示例
    • 绘图编辑器:想要复用一个已有的 TextView 类来显示文本,但它的接口与编辑器期望的 Shape 接口不匹配。可以定义一个 TextShape 类(适配器),它继承 Shape 的接口并持有一个 TextView 的实例,将 Shape 的请求(如 BoundingBox)转换为 TextView 的对应操作(如 GetExtent)。
    • USB 转接口:给只提供 USB 充电口的 MP3 播放器配一个充电器转接头,使其能通过普通电源充电。

5. 装饰模式 (Decorator)

  • 概念:动态地给一个对象添加一些额外的职责。相比生成子类,这种方式更为灵活。
  • 示例
    • 图形界面组件:一个文本显示视图 TextView 缺省没有滚动条。如果需要添加滚动条或边框,无需创建子类,而是将 TextView 放入 ScrollDecoratorBorderDecorator 中。对客户而言,装饰后的对象仍是可视组件,但拥有了新功能。
    • JUnit 测试TestDecorator 可以给测试用例添加额外行为,例如 RepeatedTest 装饰器可以让一个测试用例重复运行多次。

6. 代理模式 (Proxy)

  • 概念:为其他对象提供一种代理以控制对这个对象的访问。代理可以在访问实体前进行预处理或控制。
  • 示例
    • **图片懒加载 (虚代理)**:文档编辑器打开包含大型图片的文档时,为了速度不立即加载图片,而是先创建一个 ImageProxy 替代。只有当用户滚动到该图片需要显示时,代理才真正创建并加载图像对象。
    • **权限控制 (保护代理)**:在论坛系统中,通过代理对象判断用户权限(如注册用户与游客),控制是否允许执行“发帖”等操作。

7. 组合模式 (Composite)

  • 概念:将对象组合成树形结构以表示“部分-整体”的层次结构,使用户对单个对象和组合对象的使用具有一致性。
  • 示例
    • 图形系统Picture(组合对象)可以包含 LineRectangle(基本对象)或其他 Picture。用户可以对整个 Picture 调用 Draw 操作,它会自动递归调用所有子部件的 Draw
    • JUnitTestSuite 可以包含多个 TestCase 或其他 TestSuite,运行 TestSuite 时会自动运行其包含的所有测试。

三、 行为型模式 (Behavioral Patterns)

这类模式关注对象间的通信、职责分配和算法封装。

8. 策略模式 (Strategy)

  • 概念:定义一系列算法,把它们封装起来,并且使它们可相互替换。该模式让算法独立于使用它的客户而变化。
  • 示例
    • 文本换行算法:一个文本排版系统可能支持多种换行策略(如简单换行、TeX 优化换行、数组式换行)。将这些算法封装在不同的 Compositor 子类中,排版对象 Composition 可以根据需要动态切换使用的策略。
    • 布局管理器:Java AWT 中的 LayoutManager 接口有多种实现(FlowLayout, GridLayout),容器将布局行为委托给具体的策略对象。

9. 观察者模式 (Observer)

  • 概念:定义对象间的一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  • 示例
    • 数据与图表:一个电子表格数据对象(目标)可能有多个展示图表(观察者,如柱状图、饼图)。当数据改变时,数据对象通知所有图表,图表自动重绘以反映最新数据。
    • JUnitTestResult 维护一个 TestListener 列表。当测试失败或结束时,它会通知所有注册的监听器(如打印结果的界面)。

10. 模板方法模式 (Template Method)

  • 概念:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
  • 示例
    • 打开文档:抽象类 Application 定义了 OpenDocument 的流程(检查文档、创建对象、读取数据),其中具体的步骤如 DoCreateDocumentDoRead 由子类实现,但整体流程由父类控制。
    • JUnit 测试运行TestCase 类定义了 runBare 方法,依次执行 setUp(初始化)、runTest(运行测试)、tearDown(清理)。用户只需要重写这三个步骤的具体实现,而无需改变执行顺序。

资料

https://refactoringguru.cn/design-patterns
https://design-patterns.readthedocs.io/zh-cn/latest/
https://jueee.github.io/design-patterns/

架构师学习 第2篇
## **写在前面**
DDD 我在上一家公司的时候就专门学习过,也做了一些实践,可能因为我当时没有太深入的了解,所以总觉得好像有些重,有一种概念大于实操的感觉,不过有些概念定义对我还是很有用的,刚好当下做架构回过头再好好学习下

示例

构建一个在线电子商务系统(E-Shop)的设计示例。这个示例将从宏观的战略设计(如何划分系统边界)到微观的战术设计(代码层面的核心元素),全面展示 DDD 的核心思想。

1. 战略设计:界定的上下文 (Bounded Contexts)

DDD 的首要原则是软件必须植根于领域,并且模型需要有清晰的边界。

场景: 假设我们要构建一个大型在线商店,不仅涉及用户下单,还需要处理库存发货和销售报表。

传统问题: 许多团队会试图创建一个包含所有属性(如用户、订单、商品、库存、报表)的单一庞大模型。这会导致模型臃肿,不同职能的团队互相干扰。

DDD 解决方案:
我们将系统划分为两个独立的界定的上下文(Bounded Contexts),:

  1. 在线交易上下文(E-Shop Context): 关注客户下单、购物车、结账。这里的“商品”关注价格和描述。
  2. 报表上下文(Reporting Context): 关注销售趋势、库存周转。这里的“商品”可能只关注销售数量和成本,不需要描述信息。

核心思想:

  • 消除歧义: 同一个词(如“商品”)在不同上下文中可能有不同的含义和属性。通过划分上下文,我们可以保证模型在各自边界内的纯洁性和一致性。
  • 上下文映射(Context Map): 我们定义这两个上下文的关系。例如,报表系统需要从交易系统获取数据,它们可能通过客户-供应商(Customer-Supplier)模式交互,或者通过防崩溃层(Anticorruption Layer)来转换数据,确保报表系统的模型不受交易系统模型变更的直接破坏,。

2. 战术设计:领域模型的核心要素

在“在线交易上下文”内部,我们使用战术模式来构建领域模型。

A. 通用语言 (Ubiquitous Language)

开发人员与业务专家(如销售经理)共同制定一套语言。

  • 示例: 大家不再说“插入一条记录到订单表”,而是统一说“提交订单(Submit Order)”。
  • 价值: 这消除了沟通障碍,代码中的类名和方法名将直接反映业务意图,。

B. 分层架构 (Layered Architecture)

为了隔离关注点,我们将系统分为四层,:

  1. 用户界面层: 展示商品页面,接收用户点击。
  2. 应用层: 协调任务(如“协调结账流程”),但不包含业务逻辑。
  3. 领域层(核心): 包含 OrderCustomer 等业务对象和规则。这是软件的心脏
  4. 基础设施层: 处理数据库持久化、发送邮件等技术实现。

C. 实体 (Entities) 与 值对象 (Value Objects)

这是领域模型的基本构建块。

  • 实体(Entity):

    • 示例: Order(订单)。
    • 设计理由: 订单有生命周期(从创建到支付到发货),并且需要被追踪。即使两个订单的内容完全一样,只要 ID 不同,它们就是不同的对象。因此,Order 是一个实体,必须有唯一的标识符(Identity),。
  • 值对象(Value Object):

    • 示例: Address(送货地址)。
    • 设计理由: 我们只关心地址的属性(街道、城市),而不关心它的唯一标识。如果两个客户住在同一地址,这在业务上是等价的。Address 应该是不可变的(Immutable),如果客户搬家了,我们是用一个新的 Address 对象替换旧的,而不是修改旧对象,。

D. 聚合 (Aggregates)

为了保证数据一致性,我们需要划定修改数据的边界。

  • 示例: 一个 Order(订单)可能包含多个 OrderItem(订单项)。
  • 设计: Order 是这个聚合的根(Aggregate Root)
  • 规则: 外部对象只能引用根(Order),不能直接引用内部的 OrderItem。如果想修改某个订单项的数量,必须通过根的方法(如 order.updateItemQuantity())来进行。这确保了订单总价等不变量(Invariants)在修改过程中始终保持一致,。

E. 服务 (Services)

有些动作不属于特定的对象。

  • 示例: CheckoutService(结账服务)或 FundTransferService(转账服务)。
  • 设计理由: 结账可能涉及订单状态更新、库存扣减、支付网关调用等。这些行为放入 OrderCustomer 都不合适,因此我们创建一个无状态的领域服务来封装这些操作,。

F. 资源库 (Repositories)

为了解耦领域模型与数据库。

  • 示例: OrderRepository
  • 设计: 领域层只定义接口 findOrder(id),不关心底层是 SQL Server 还是 Oracle。资源库负责从数据库中检索数据并将其重建为领域对象(如 Order 聚合)。这让开发人员可以像从内存集合中获取对象一样获取领域对象,而无需在业务逻辑中编写 SQL,。

3. 系统运作流程示例

结合上述概念,一个“用户修改收货地址”的业务场景在代码设计中如下流转:

  1. 应用层接收请求,调用资源库CustomerRepository)。
  2. 资源库利用基础设施层从数据库检索数据,重建 Customer 聚合(实体)。
  3. 应用层调用 Customer 实体的业务方法(如 customer.moveTo(newAddress))。
  4. 在方法内部,Customer 实体将旧的 Address 值对象替换为新的 Address 值对象
  5. 资源库将更新后的 Customer 聚合保存回数据库。

总结与比喻

这个设计示例体现了 DDD 的核心:通过将软件实现与业务领域模型紧密绑定,来应对复杂性。

为了巩固理解,我们可以用书中提到的汽车制造来类比这个系统设计:

  • 领域模型就像是汽车的设计蓝图。工人在造车前必须先有精准的图纸,同样,开发软件前必须先理解并建模业务领域。
  • 实体与聚合就像是汽车的发动机和底盘。它们是核心部件,有独立的标识和生命周期,必须作为一个整体来组装和维护。
  • 分层架构就像是汽车的不同系统(传动系统、电子系统、内饰)。内饰(UI)的变化不应直接影响发动机(领域逻辑)的运作。
  • 通用语言就像是工程师团队之间的技术术语。如果有人把“方向盘”叫成“转弯器”,制造过程就会混乱;同样,代码必须精确使用业务术语。

学习资料

https://github.com/Sairyss/domain-driven-hexagon?tab=readme-ov-file

AI提效 Claude Sonnet 4
最近产品上线的前期准备,小团队+AI编程,各项工作几乎手撮。因为产品投入市场后很可能会有一个迅速从100到10000的过程,所以我要前置考虑一些事情,慢慢 查漏补缺。此篇是我对于数据灾备的参考播客之一。基于ByteByteGo博客+一些实操+AI辅助而输出该博客。

数据库复制指南:核心概念与策略

Database Replication Guide: Key Concepts and Strategies

引言 | Introduction

每个现代应用程序都依赖于数据,用户期望数据快速、实时且始终可访问。然而,数据库并不是魔法,它们可能会失败或在负载下变慢。它们也会遇到物理和地理限制,这就是复制变得必要的地方。

Every modern application relies on data, and users expect that data to be fast, current, and always accessible. However, databases are not magic. They can fail or slow down under load. They can also encounter physical and geographic limits, which is where replication becomes necessary.

数据库复制意味着在多台机器上保持相同数据的副本。这些机器可以位于同一个数据中心,也可以分布在全球各地。目标很简单:

  • 提高容错性
  • 扩展读取能力
  • 通过将数据移近需要的地方来减少延迟

Database Replication means keeping copies of the same data across multiple machines. These machines can sit in the same data center or be spread across the globe. The goal is straightforward:

  • Increase fault tolerance
  • Scale reads
  • Reduce latency by bringing data closer to where it’s needed

复制的重要性 | The Importance of Replication

复制是任何旨在在不丢失数据或令用户失望的情况下从故障中恢复的系统的核心。无论是毫秒级更新的社交动态、处理限时抢购的电商网站,还是处理全球交易的金融系统,复制确保系统即使在部分组件故障时也能继续运行。

Replication sits at the heart of any system that aims to survive failures without losing data or disappointing users. Whether it’s a social feed updating in milliseconds, an e-commerce site handling flash sales, or a financial system processing global transactions, replication ensures the system continues to operate, even when parts of it break.

然而,复制也带来了复杂性。它迫使我们在一致性、可用性和性能之间做出艰难的决定。数据库可能正常运行,但滞后的副本仍可能提供过时的数据。网络分区可能使两个主节点认为它们在负责,导致脑裂写入。围绕这些问题进行设计并非易事。

However, replication also introduces complexity. It forces difficult decisions around consistency, availability, and performance. The database might be up, but a lagging replica can still serve stale data. A network partition might make two leader nodes think they’re in charge, leading to split-brain writes. Designing around these issues is non-trivial.

复制策略概述 | Overview of Replication Strategies

在分布式数据库中,有三种主要的复制策略:

In distributed databases, there are three main replication strategies:

1. 单主复制 (Single-Leader Replication)

工作原理 | How It Works:

  • 一个主节点接收所有写入操作
  • 主节点将更改复制到多个从节点
  • 从节点提供读取服务

优势 | Advantages:

  • 简单且易于理解
  • 强一致性保证
  • 避免写入冲突

劣势 | Disadvantages:

  • 主节点成为单点故障

  • 写入性能受限于单个节点

  • 主节点故障时需要故障转移

  • One primary node accepts all writes

  • Primary replicates changes to multiple secondary nodes

  • Secondary nodes serve read requests

  • Simple and easy to understand

  • Strong consistency guarantees

  • Avoids write conflicts

  • Primary node becomes a single point of failure

  • Write performance limited to single node

  • Requires failover when primary fails

2. 多主复制 (Multi-Leader Replication)

工作原理 | How It Works:

  • 多个主节点可以接受写入
  • 主节点之间相互复制更改
  • 需要冲突检测和解决机制

优势 | Advantages:

  • 高写入可用性
  • 更好的性能和容错性
  • 适合多数据中心部署

劣势 | Disadvantages:

  • 写入冲突需要解决

  • 复杂的一致性模型

  • 需要冲突解决策略

  • Multiple primary nodes can accept writes

  • Primaries replicate changes to each other

  • Requires conflict detection and resolution

  • High write availability

  • Better performance and fault tolerance

  • Suitable for multi-datacenter deployments

  • Write conflicts need resolution

  • Complex consistency model

  • Requires conflict resolution strategies

3. 无主复制 (Leaderless Replication)

工作原理 | How It Works:

  • 所有副本都是对等的
  • 客户端可以向任何副本写入
  • 使用仲裁机制确保一致性

优势 | Advantages:

  • 高可用性
  • 简单的故障处理
  • 良好的可扩展性

劣势 | Disadvantages:

  • 最终一致性

  • 复杂的读取修复

  • 需要仲裁机制

  • All replicas are peers

  • Clients can write to any replica

  • Uses quorum mechanisms for consistency

  • High availability

  • Simple failure handling

  • Good scalability

  • Eventual consistency

  • Complex read repair

  • Requires quorum mechanisms

复制延迟的挑战 | Challenges of Replication Lag

复制延迟是分布式数据库面临的一个关键挑战。当主节点接收写入并将更改传播到副本时,存在时间延迟。这种延迟可能导致:

Replication lag is a key challenge faced by distributed databases. When the primary node receives a write and propagates changes to replicas, there’s a time delay. This lag can lead to:

读取后写入不一致 | Read-After-Write Inconsistency

用户写入数据后立即读取可能看到旧数据。

Users might see stale data when reading immediately after writing.

单调读取问题 | Monotonic Read Issues

用户可能看到数据”倒退”,即先看到新数据后看到旧数据。

Users might see data “go backwards” - seeing newer data then older data.

因果关系违反 | Causality Violations

相关事件可能以错误的顺序出现。

Related events might appear in the wrong order.

选择合适的复制策略 | Choosing the Right Replication Strategy

何时选择单主复制 | When to Choose Single-Leader Replication

  • 需要强一致性的应用

  • 写入量相对较低

  • 简单的故障转移需求

  • Applications requiring strong consistency

  • Relatively low write volume

  • Simple failover requirements

何时选择多主复制 | When to Choose Multi-Leader Replication

  • 多数据中心部署

  • 高写入可用性需求

  • 可以容忍冲突解决的复杂性

  • Multi-datacenter deployments

  • High write availability requirements

  • Can tolerate conflict resolution complexity

何时选择无主复制 | When to Choose Leaderless Replication

  • 最终一致性可接受

  • 需要高可用性

  • 简单的扩展需求

  • Eventual consistency is acceptable

  • High availability is needed

  • Simple scaling requirements

实现考虑因素 | Implementation Considerations

一致性模型 | Consistency Models

  • 强一致性: 所有副本始终同步

  • 最终一致性: 副本最终会收敛

  • 因果一致性: 保持事件的因果关系

  • Strong Consistency: All replicas always in sync

  • Eventual Consistency: Replicas eventually converge

  • Causal Consistency: Maintains causality between events

冲突解决策略 | Conflict Resolution Strategies

  • 最后写入获胜 (LWW): 基于时间戳的简单策略

  • 应用层解决: 让应用程序处理冲突

  • 合并策略: 自动合并冲突的更改

  • Last Write Wins (LWW): Simple timestamp-based strategy

  • Application-level resolution: Let application handle conflicts

  • Merge strategies: Automatically merge conflicting changes

网络分区处理 | Network Partition Handling

  • CAP定理: 在一致性、可用性和分区容忍性之间选择

  • 脑裂预防: 使用仲裁和租约机制

  • 分区检测: 监控网络连接状态

  • CAP Theorem: Choose between consistency, availability, and partition tolerance

  • Split-brain prevention: Use quorum and lease mechanisms

  • Partition detection: Monitor network connectivity

现实世界的例子 | Real-World Examples

单主复制系统 | Single-Leader Systems

  • MySQL主从复制: 传统的主从架构

  • PostgreSQL流复制: 支持同步和异步复制

  • MongoDB副本集: 自动故障转移

  • MySQL Master-Slave: Traditional master-slave architecture

  • PostgreSQL Streaming: Supports sync and async replication

  • MongoDB Replica Sets: Automatic failover

多主复制系统 | Multi-Leader Systems

  • MySQL集群: 多主动主配置

  • CouchDB: 文档数据库的多主复制

  • Cassandra: 分布式NoSQL数据库

  • MySQL Cluster: Multi-active master configuration

  • CouchDB: Multi-master replication for document databases

  • Cassandra: Distributed NoSQL database

无主复制系统 | Leaderless Systems

  • Amazon DynamoDB: 无主键值存储

  • Apache Cassandra: 对等复制

  • Riak: 分布式键值存储

  • Amazon DynamoDB: Leaderless key-value store

  • Apache Cassandra: Peer-to-peer replication

  • Riak: Distributed key-value store

监控和维护 | Monitoring and Maintenance

关键指标 | Key Metrics

  • 复制延迟: 主副本之间的时间差

  • 吞吐量: 每秒处理的操作数

  • 可用性: 系统正常运行时间百分比

  • Replication Lag: Time difference between primary and replicas

  • Throughput: Operations processed per second

  • Availability: System uptime percentage

维护最佳实践 | Maintenance Best Practices

  • 定期备份和恢复测试

  • 监控复制状态

  • 计划故障转移演练

  • Regular backup and recovery testing

  • Monitor replication status

  • Plan failover drills

PostgreSQL复制实战经验 | PostgreSQL Replication Practical Experience

为什么选择PostgreSQL | Why Choose PostgreSQL

在实际项目中,PostgreSQL作为企业级开源数据库,在复制、扩展功能方面有着独特的优势。我的上一家公司的几个项目选用的就是PostgreSQL,有以下深刻体会:

In real projects, PostgreSQL as an enterprise-grade open-source database has unique advantages in replication. Through my experience with PostgreSQL replication in multiple projects, I have the following insights:

PostgreSQL的复制优势 | PostgreSQL Replication Advantages:

  • 流复制稳定可靠: 相比MySQL的binlog复制,PostgreSQL的流复制更加稳定,延迟更低

  • 逻辑复制灵活: 支持表级复制,可以选择性复制部分数据

  • 强一致性保证: 同步复制模式下可以确保零数据丢失

  • 丰富的监控工具: pg_stat_replication视图提供详细的复制状态信息

  • Stable streaming replication: Compared to MySQL’s binlog replication, PostgreSQL’s streaming replication is more stable with lower latency

  • Flexible logical replication: Supports table-level replication, allowing selective data replication

  • Strong consistency guarantees: Synchronous replication mode ensures zero data loss

  • Rich monitoring tools: pg_stat_replication view provides detailed replication status information

PostgreSQL复制最佳实践 | PostgreSQL Replication Best Practices

基于实际运维经验,我总结了以下PostgreSQL复制的最佳实践:

Based on practical operational experience, I’ve summarized the following PostgreSQL replication best practices:

1. 流复制配置建议 | Streaming Replication Configuration Recommendations

主库配置要点 | Primary Configuration Key Points:

1
2
3
4
5
6
-- postgresql.conf
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
synchronous_commit = on # 根据业务需求调整
synchronous_standby_names = '*' # 同步复制

从库配置要点 | Standby Configuration Key Points:

1
2
3
4
-- postgresql.conf
hot_standby = on
max_standby_streaming_delay = 30s
wal_receiver_status_interval = 1s

2. 监控和告警策略 | Monitoring and Alert Strategies

关键监控指标 | Key Monitoring Metrics:

  • 复制延迟: 通过pg_stat_replication.replay_lag监控
  • WAL发送状态: 监控pg_stat_replication.state
  • 磁盘空间: WAL日志积累可能导致磁盘满
  • 网络连接: 复制连接的稳定性

告警阈值建议 | Recommended Alert Thresholds:

  • 复制延迟超过10秒告警

  • WAL发送异常立即告警

  • 主从连接断开超过1分钟告警

  • Replication lag: Monitor via pg_stat_replication.replay_lag

  • WAL sender status: Monitor pg_stat_replication.state

  • Disk space: WAL log accumulation may cause disk full

  • Network connection: Stability of replication connections

  • Replication lag exceeding 10 seconds

  • WAL sender exceptions immediate alert

  • Primary-standby connection lost for more than 1 minute

3. 故障切换实践 | Failover Practices

自动故障切换工具推荐 | Recommended Automatic Failover Tools:

  • Patroni: 基于etcd/consul的高可用解决方案
  • repmgr: 轻量级的复制管理工具
  • Stolon: 云原生的PostgreSQL高可用方案

手动故障切换步骤 | Manual Failover Steps:

  1. 确认主库真正故障
  2. 提升从库为主库:pg_promote()
  3. 重新配置应用连接
  4. 修复原主库并重建复制
  • Patroni: High availability solution based on etcd/consul
  • repmgr: Lightweight replication management tool
  • Stolon: Cloud-native PostgreSQL high availability solution
  1. Confirm primary database is truly failed
  2. Promote standby to primary: pg_promote()
  3. Reconfigure application connections
  4. Repair original primary and rebuild replication

我的技术观点 | My Technical Perspectives

关于复制策略选择 | On Replication Strategy Selection

单主复制依然是主流 | Single-Leader Replication Remains Mainstream

虽然多主复制和无主复制在理论上很吸引人,但在实际生产环境中,我发现单主复制仍然是最可靠的选择,特别是对于需要强一致性的业务场景。原因如下:

While multi-leader and leaderless replication are theoretically attractive, in actual production environments, I find single-leader replication is still the most reliable choice, especially for business scenarios requiring strong consistency. Here’s why:

  1. 复杂性可控: 单主复制的逻辑简单,故障排查容易

  2. 一致性保证: 避免了复杂的冲突解决机制

  3. 工具成熟: PostgreSQL的单主复制工具链非常成熟

  4. 性能可预测: 读写分离的性能模式清晰

  5. Manageable complexity: Single-leader replication logic is simple, easy to troubleshoot

  6. Consistency guarantee: Avoids complex conflict resolution mechanisms

  7. Mature tooling: PostgreSQL’s single-leader replication toolchain is very mature

  8. Predictable performance: Clear read-write separation performance pattern

关于同步vs异步复制 | On Synchronous vs Asynchronous Replication

混合模式是最佳选择 | Hybrid Mode is the Best Choice

在实际项目中,我通常采用”同步+异步”的混合复制模式:

In actual projects, I usually adopt a “synchronous + asynchronous” hybrid replication mode:

  • 关键业务: 使用同步复制,确保数据安全
  • 读取扩展: 使用异步复制,提供更多读取能力
  • 跨地域备份: 使用异步复制,降低网络延迟影响

配置示例 | Configuration Example:

1
synchronous_standby_names = 'FIRST 1 (standby1), standby2, standby3'
  • Critical business: Use synchronous replication to ensure data safety
  • Read scaling: Use asynchronous replication for more read capacity
  • Cross-region backup: Use asynchronous replication to reduce network latency impact

关于PostgreSQL版本选择 | On PostgreSQL Version Selection

推荐PostgreSQL 14+版本 | Recommend PostgreSQL 14+ Versions

基于我的使用经验,PostgreSQL 14及以上版本在复制功能上有显著改进:

Based on my experience, PostgreSQL 14 and above versions have significant improvements in replication features:

  1. 逻辑复制增强: 支持二进制格式,性能提升30%以上

  2. 复制监控改进: 更丰富的统计信息和监控视图

  3. 故障恢复优化: 崩溃恢复时间大幅缩短

  4. 安全性增强: 支持更细粒度的复制权限控制

  5. Logical replication enhancements: Support for binary format, 30%+ performance improvement

  6. Replication monitoring improvements: Richer statistics and monitoring views

  7. Failover optimization: Significantly reduced crash recovery time

  8. Security enhancements: Support for more granular replication permission control

网络和安全配置 | Network and Security Configuration

网络优化 | Network Optimization:

  • 使用专用网络进行复制
  • 配置合适的TCP参数优化
  • 监控网络带宽使用情况

安全配置 | Security Configuration:

  • 使用SSL加密复制连接

  • 配置防火墙规则

  • 定期更新密码和证书

  • Use dedicated network for replication

  • Configure appropriate TCP parameter optimization

  • Monitor network bandwidth usage

  • Use SSL encryption for replication connections

  • Configure firewall rules

  • Regularly update passwords and certificates

结论 | Conclusion

数据库复制是构建可靠、可扩展系统的基础技术。选择正确的复制策略取决于应用程序的具体需求,包括一致性要求、可用性目标和性能期望。理解每种策略的权衡是设计成功分布式系统的关键。

Database replication is a fundamental technology for building reliable, scalable systems. Choosing the right replication strategy depends on your application’s specific requirements, including consistency needs, availability goals, and performance expectations. Understanding the trade-offs of each approach is crucial for designing successful distributed systems.

基于我在PostgreSQL复制方面的实战经验,我强烈建议:从简单开始,逐步优化。先建立稳定的单主复制架构,然后根据业务增长和性能需求,逐步引入更复杂的复制策略。PostgreSQL作为企业级数据库,其复制功能完全能够满足大多数业务场景的需求。

Based on my practical experience with PostgreSQL replication, I strongly recommend: Start simple, optimize gradually. First establish a stable single-leader replication architecture, then gradually introduce more complex replication strategies based on business growth and performance requirements. PostgreSQL as an enterprise-grade database, its replication features can fully meet the needs of most business scenarios.

无论选择哪种策略,都需要仔细考虑实现细节、监控系统状态,并为故障情况做好准备。随着应用程序的发展,复制策略也可能需要演进以满足新的需求。

Regardless of which strategy you choose, careful consideration of implementation details, monitoring system health, and preparing for failure scenarios is essential. As applications evolve, replication strategies may need to evolve as well to meet new requirements.


本文基于ByteByteGo的数据库复制指南编写,旨在为开发者提供全面的复制策略参考。

参考:This article is based on ByteByteGo’s database replication guide, aimed at providing developers with comprehensive reference for replication strategies.