DDD 实战:聚合根设计的五条血泪教训

2023 年,我主导了一个电商中台的 DDD 重构项目。从最初的「大泥球」单体架构到最终的领域驱动设计落地,我们踩了无数的坑。这篇文章不谈理论,只讲实战中用血泪换来的五条聚合根设计教训。

背景:那个让我们失眠的订单系统

老系统是一个典型的「上帝类」架构——OrderService 有 8000 多行代码,一个 createOrder 方法就包含了库存扣减、优惠券核销、积分计算、物流单创建等 12 个领域的逻辑。任何一个小改动都可能引发蝴蝶效应。

我们决定用 DDD 重构。但理论上的 DDD 和工程实践之间的鸿沟,远比教科书中描述的要深。

教训一:聚合根不是实体,是一致性边界

最初的设计中,我们把 Order(订单)设计成了一个巨大的聚合根,里面包含了订单项、收货地址、支付信息、物流跟踪、发票信息等所有关联实体。

// 错误示范:上帝聚合根
public class Order {
    private OrderId id;
    private List<OrderItem> items;          // 订单项
    private ShippingAddress address;        // 收货地址
    private PaymentInfo payment;            // 支付信息
    private List<LogisticsRecord> logistics;// 物流跟踪
    private InvoiceInfo invoice;            // 发票信息
    private CouponUsage couponUsage;        // 优惠券使用记录
    private List<OrderRemark> remarks;      // 订单备注
    
    // 一个方法改了 7 个实体的状态...
    public void confirmOrder() {
        // 验证库存、验证地址、创建支付单、生成物流单...
    }
}

问题很快暴露了

[阅读全文]

JVM 调优手记:GC 停顿时间从 500ms 压到 20ms

线上告警:P99 延迟从 80ms 飙升至 2000ms,每隔几分钟就有一次「毛刺」。排查了三天,最终发现罪魁祸首是 GC。这是一篇完整的 JVM 调优实战记录,从问题定位到参数调优,全程复盘。

事故现场

我们的实时推荐服务部署在 8 台 16C32G 的机器上,JDK 17,堆内存配置为 -Xmx12g -Xms12g,使用 G1 垃圾回收器。日均请求量 2 亿次,单机 QPS 约 3000。

某天凌晨,监控系统突然告警:

告警指标:
- P99 延迟:80ms → 2000ms(持续波动)
- GC 暂停时间:平均 500ms,峰值 3200ms
- Full GC 频率:每 3-5 分钟一次
- CPU 使用率:从 40% 飙升至 85%

用户侧的表现是:推荐列表偶尔加载极慢,页面出现空白等待。

第一步:收集 GC 日志

开启详细 GC 日志是所有调优工作的起点:

# JDK 17 的 GC 日志参数
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m

# 如果是 JDK 8(虽然很老了)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data/logs/gc.log

GCEasygchisto 分析日志后,问题一目了然:

[阅读全文]

分布式系统演进:从 CAP 到 PACELC 的架构思考

CAP 定理是分布式系统的「牛顿第一定律」——它定义了这个领域的基本约束。但在真实的工程实践中,CAP 定理过于粗粒度,以至于它无法指导我们在实际系统中做出正确的设计决策。我们需要一个更精细的模型。

CAP 定理的再理解

2000 年,Eric Brewer 提出 CAP 猜想;2002 年,Gilbert 和 Lynch 给出了形式化证明。CAP 定理告诉我们:

在一个分布式系统中,以下三个特性最多只能同时满足两个:

C (Consistency)  - 一致性:所有节点在同一时刻看到相同的数据
A (Availability) - 可用性:每个请求都能在合理时间内收到非错误响应
P (Partition Tolerance) - 分区容错:网络分区发生时系统仍能运行

大多数架构师对 CAP 的理解停留在「三选二」的层面。但这个理解有几个问题:

问题 1:P 不是一个可选项

在真实的分布式系统中,网络分区是必然会发生的事情——交换机故障、光缆被挖断、数据中心之间的网络抖动。所以 P 不是你可以「选择放弃」的特性,它是一个既定事实。

真正的选择是在 C 和 A 之间,而且这个选择只在分区发生时才有意义。

没有分区时:C 和 A 可以同时满足
发生分区时:必须在 C 和 A 之间选择

CP 系统:分区时拒绝服务,保证一致性
  例:ZooKeeper、etcd、HBase

AP 系统:分区时继续服务,可能返回过期数据
  例:Cassandra、DynamoDB、CouchDB

问题 2:CAP 是全局的,但需求是局部的

同一个系统中,不同的业务操作可能有不同的一致性需求:

电商系统的一致性需求:

操作 A:扣减库存
  → 需要强一致性(不能超卖)→ CP

操作 B:展示商品评价
  → 可以接受最终一致性(延迟几秒看到新评价无所谓)→ AP

操作 C:用户修改收货地址
  → 需要强一致性(改错了会寄错地方)→ CP

操作 D:商品浏览量计数
  → 可以接受最终一致性(少计几次没关系)→ AP

一个成熟的分布式系统不会在全局层面选择 CP 或 AP,而是在每个操作级别做精细的选择。

[阅读全文]

Redis 高并发缓存架构:击穿、穿透、雪崩全解

缓存是提升系统吞吐量最简单的手段,但它引入的问题也比你想象的更多。击穿、穿透、雪崩——这三个听起来像地质灾害的名词,是每一个高并发系统设计者必须跨越的三道坎。

缓存的基本模型

在深入三个核心问题之前,先建立一个标准的缓存读写模型:

标准缓存读取流程:

客户端 → 缓存层(Redis)→ [命中] → 直接返回
                         → [未命中] → 数据库层 → 写入缓存 → 返回

标准缓存写入流程:

方案 A(Cache Aside):先更新数据库,再删除缓存
方案 B(Read/Write Through):通过缓存层代理读写
方案 C(Write Behind):只写缓存,异步刷入数据库

在绝大多数互联网场景中,Cache Aside(旁路缓存)是最常用的模式——代码直接操作 Redis 和数据库,简单透明。但正是这种简单性,隐藏着三大问题。

问题一:缓存穿透(Cache Penetration)

定义:查询一个根本不存在的数据,缓存中没有,数据库中也没有。每次请求都会穿透缓存直接打到数据库。

正常请求:
  查询 user_id=123 → 缓存命中 → 返回(快)

穿透请求:
  查询 user_id=-1 → 缓存未命中 → 查数据库 → 数据库也没有 → 返回空
  查询 user_id=-1 → 缓存未命中 → 查数据库 → 数据库也没有 → 返回空
  查询 user_id=-1 → 缓存未命中 → 查数据库 → 数据库也没有 → 返回空
  
  如果每秒 10000 次这样的请求,数据库直接被压垮

穿透通常由恶意攻击或业务 Bug 引起——攻击者构造大量不存在的 ID,绕过缓存直接攻击数据库。

[阅读全文]

微服务治理演进:从 Netflix OSS 到 Service Mesh

微服务架构的流行带来了一个悖论:系统被拆分成更小的单元以降低复杂度,但服务之间的通信却创造了一个更复杂的分布式系统。如何治理这个「服务间的混沌」,是过去十年基础架构领域最重要的课题之一。

第一阶段:蛮荒时代(2014 之前)

在微服务概念刚刚兴起的时候,服务治理几乎等于「手写一切」:

服务 A 调用服务 B:

1. 自己实现服务发现(查 ZooKeeper / Nginx upstream)
2. 自己实现负载均衡(轮询 / 随机)
3. 自己实现重试(try-catch + 循环)
4. 自己实现超时(设置 HTTP timeout)
5. 自己实现熔断(状态机 + 计数器)

每一个服务的开发者都要重复实现这些逻辑,而且不同团队的实现质量参差不齐

这个阶段的核心问题是:治理逻辑和业务逻辑耦合在应用代码中。每一次修改治理策略(比如调整超时时间),都需要修改业务代码、重新编译、重新部署。

第二阶段:SDK 时代(2014-2017)

Netflix 开源了一整套微服务治理工具(Netflix OSS),标志着微服务治理进入 SDK 时代:

Netflix OSS 全家桶:

Eureka    → 服务注册与发现
Ribbon    → 客户端负载均衡
Hystrix   → 熔断器
Feign     → 声明式 HTTP 客户端
Zuul      → API 网关
Archaius  → 动态配置

Spring Cloud 在此基础上做了封装,让 Java 开发者可以用注解的方式快速接入:

@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
    @GetMapping("/users/{id}")
    User getUser(@PathVariable("id") Long id);
}

@Component
public class UserServiceFallback implements UserServiceClient {
    @Override
    public User getUser(Long id) {
        return User.defaultUser();  // 熔断降级
    }
}

SDK 模式的问题

Netflix OSS 解决了「重复造轮子」的问题,但它引入了新的痛点:

[阅读全文]

架构师的系统论:用复杂性科学重构团队认知

做了五年架构师之后,我逐渐意识到一个事实:技术上的挑战往往不是最难的部分。真正困难的是如何让一个团队对「什么是好的架构」达成共识。这篇文章尝试从系统论和复杂性科学的角度,重新审视架构师的工作方式。

架构师不是高级程序员

很多公司在晋升到「架构师」这个职级时,默认的评判标准还是技术深度:你对某个领域的源码理解有多深?你能不能设计出高并发的系统?你熟不熟悉各种中间件?

这些能力当然重要,但它们描述的是「高级工程师」,不是「架构师」。

一个高级工程师解决的问题是:怎么把这个功能做好?

一个架构师解决的问题是:这个系统应该长什么样?为什么?

两者的区别在于:工程师在约束内寻找最优解,架构师定义约束本身。

工程师思维:
  "我们需要一个消息队列来解耦。Kafka 和 RocketMQ 哪个更好?"
  → 在已知选项中选择最优解

架构师思维:
  "我们真的需要消息队列吗?如果用事件溯源,是否可以从根本上消除对 MQ 的依赖?"
  → 重新定义问题和约束

系统是一个复杂自适应系统(CAS)

复杂自适应系统(Complex Adaptive System, CAS)是圣塔菲研究所的核心研究对象。一个软件系统——包括它的代码、基础设施、开发团队、用户——本质上就是一个 CAS。

CAS 的四个核心特征:

1. 由大量自主主体组成
   主体:微服务、开发者、团队、用户
   每个主体有自己的目标和行为规则

2. 主体之间相互作用
   服务之间的 API 调用
   团队之间的沟通协调
   用户与系统的交互

3. 涌现(Emergence)
   宏观行为无法从微观行为简单推导
   例:每个服务都是高可用的,但整个系统却可能因为级联故障而宕机

4. 自适应(Adaptation)
   系统会根据环境变化调整自身行为
   例:自动扩缩容、熔断降级、团队根据事故复盘调整流程

理解了 CAS 的特性,就会明白为什么架构设计不能是「自上而下的完全规划」:

  • 涌现不可预测:你无法预知所有微服务组合在一起后会产生什么样的宏观行为。这就是为什么混沌工程(Chaos Engineering)如此重要——你需要在生产环境中主动注入故障,观察系统的涌现行为。

  • 过度控制会扼杀适应性:如果你把每一个服务的实现细节都规定死了,团队就失去了根据实际情况做调整的能力。好的架构应该定义边界和接口,而不是规定实现。

康威定律的工程解读

康威定律:
"设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。"

—— Melvin Conway, 1967

这不是一句口号,而是一个被反复验证的工程事实。

反例 1:单体团队做微服务

组织架构:一个 50 人的大团队,所有人向同一个 leader 汇报
技术架构:虽然拆分了 20 个微服务,但因为沟通无障碍,服务之间的耦合越来越多
结果:分布式单体(Distributed Monolith)——有微服务的所有缺点,没有微服务的任何优点

反例 2:跨团队共享数据库

组织架构:三个独立的业务团队
技术架构:三个团队共用同一个数据库
结果:任何一个团队的 Schema 变更都会影响其他团队,沟通成本随团队数量指数增长

正确的做法是让组织架构和技术架构对齐:

[阅读全文]

技术债务的拓扑学:如何量化和治理系统熵增

Ward Cunningham 在 1992 年提出「技术债务」这个概念时,他可能没想到三十年后这个词会成为每一个技术团队的噩梦。技术债务不像金融债务那样可以精确计算,它更像是一种熵——在看不见的地方悄悄增长,直到系统变得不可维护。

技术债务不是一种债

很多人把技术债务理解为「欠下的代码质量」,觉得只要花时间重构就能还清。这个理解有一个根本性的错误:技术债务不是贷款,而是复利。

金融债务:
  借 100 万,年利率 5%,每年还 5 万利息
  债务总额是确定的,还款计划是可预测的

技术债务:
  写了一段快速但丑陋的代码来赶 deadline
  第一个月:需要在这段代码旁边加个功能 → 多花了 2 小时
  第三个月:新人入职看不懂这段代码 → 多花了 2 天
  第六个月:这段代码的 Bug 引发了线上事故 → 多花了 1 周
  第十二个月:这段代码和其他模块的耦合太深,无法重构 → 被迫放弃

技术债务的利息不是线性的,而是指数增长的

这就是为什么技术债务不能「等有空了再还」——因为等你有空的时候,利息可能已经超过了本金。

技术债务的四种类型

Martin Fowler 在《Refactoring》中把技术债务分为四个象限:

                    鲁莽的                审慎的
              ┌──────────────────┬──────────────────┐
    故意的     │ "我们不需要测试"  │ "我们知道这里耦合   │
              │ "先 copy paste  │  太紧,但现在重构   │
              │  以后再重构"     │  会延误发布,下个月  │
              │                 │  专门处理"         │
              ├──────────────────┼──────────────────┤
    无意的     │ "什么是分层?"    │ "我们现在才理解    │
              │ "把 SQL 写在     │  应该怎么做"       │
              │  Controller 里"  │(随着认知深入发现   │
              │                 │  之前的设计有问题)  │
              └──────────────────┴──────────────────┘

鲁莽且故意的债务最危险——团队明知故犯,而且不考虑后果。

[阅读全文]

RAR/ZIP 压缩包密码忘了怎么办?2026 年完整恢复指南

作为技术人员,你一定经历过这样的噩梦:几个月前加密的压缩包,密码死活想不起来了。里面可能是重要的项目资料、珍贵的照片备份、或者是离职前交接给你的技术文档。别慌,这篇文章系统梳理了当前主流的压缩包密码恢复方案。

压缩包加密的基本原理

在讨论恢复方案之前,先理解压缩包是怎么加密的。这决定了哪些恢复方案可行,哪些不可行。

ZIP 加密

ZIP 格式支持两种加密方式:

传统 ZIP 加密(ZipCrypto):
  - 基于流密码的简单加密
  - 安全性较低,存在已知的明文攻击漏洞
  - 如果压缩包内有你知道内容的文件,可以通过已知明文攻击快速破解
  - 恢复难度:★★☆☆☆

AES-256 加密(WinZip AES):
  - 基于 AES-256-CBC 的现代加密
  - 目前没有已知的密码学漏洞
  - 只能通过暴力枚举或字典攻击恢复
  - 恢复难度:★★★★☆

RAR 加密

RAR3 加密:
  - AES-128-CBC
  - 密钥派生使用 PBKDF2,但迭代次数较低
  - 恢复速度:中等

RAR5 加密:
  - AES-256-CBC
  - 密钥派生使用 PBKDF2-HMAC-SHA256,迭代次数可配置
  - 抗暴力破解能力显著增强
  - 恢复速度:较慢

关键认知:现代加密算法(AES-256)本身是无法被「破解」的。 所有恢复方案本质上都是在尝试猜测密码——通过字典攻击、暴力枚举、规则变异等方式,逐一尝试可能的密码组合。

方案一:回忆与推理(零成本,优先尝试)

在动用任何工具之前,先花 10 分钟做密码推理。很多人忽略了这一步,直接上工具,浪费了大量时间。

密码回忆清单:

1. 检查你的密码管理器
   - 1Password / Bitwarden / KeePass 中是否保存过?
   - 浏览器密码管理器中是否有记录?

2. 常见密码模式
   - 你的常用密码 + 数字后缀(如 MyPass2024)
   - 项目名称 + 特殊字符(如 Project@Arch)
   - 日期组合(如 20240101、199506)
   - 手机号、身份证号的部分数字

3. 查看历史记录
   - 邮件中是否有发送过这个压缩包?附件里可能有密码
   - 聊天记录(微信/钉钉/Slack)中是否提到过密码?
   - 笔记软件(Notion/Obsidian/备忘录)中是否有记录?

4. 询问相关人
   - 谁给你的这个压缩包?
   - 当时是在什么场景下加密的?

方案二:本地恢复工具

如果回忆无果,可以使用本地工具进行密码恢复。

[阅读全文]

Word/Excel 文档加密密码恢复:从原理到实战

前两天帮一位做财务的朋友恢复了一个加密的 Excel 文件密码——那里面存着三年的税务数据,密码忘了但文件又不能丢。Office 文档的加密机制和压缩包完全不同,恢复策略也有很大差异。这篇文章把 Office 文档密码恢复的技术原理和实战方案完整梳理一遍。

Office 文档的加密演进

Office 文档的加密方案随着版本迭代发生了很大变化。了解加密版本是选择恢复方案的前提。

Office 97-2003(.doc / .xls / .ppt):
  加密方式:RC4(40-bit 密钥)
  安全性:极弱
  恢复难度:★☆☆☆☆
  说明:40-bit 密钥空间只有 2^40 ≈ 1 万亿种组合,现代 GPU 几小时即可穷举

Office 2007(.docx / .xlsx / .pptx):
  加密方式:AES-128 + SHA-1
  密钥派生:PBKDF2,迭代 50,000 次
  安全性:中等
  恢复难度:★★★☆☆
  说明:迭代次数显著增加了暴力破解的难度

Office 2010:
  加密方式:AES-128 + SHA-1
  密钥派生:PBKDF2,迭代 100,000 次
  安全性:中等偏上
  恢复难度:★★★☆☆

Office 2013-2021 / Microsoft 365:
  加密方式:AES-256 + SHA-512
  密钥派生:PBKDF2,迭代 100,000 次(可配置到 1,000,000)
  安全性:强
  恢复难度:★★★★☆
  说明:AES-256 + 高迭代次数,对暴力破解有很强的抵抗力

查看你的文档使用的加密版本

# 使用 msoffcrypto-tool 查看加密信息
import msoffcrypto

with open('encrypted.xlsx', 'rb') as f:
    file = msoffcrypto.OfficeFile(f)
    print(f"加密类型: {file.file_type}")
    print(f"密钥大小: {file.keyTypes}")
    # 或者使用命令行:
    # msoffcrypto-tool -t encrypted.xlsx

Office 的两种「加密」:别搞混了

很多用户混淆了 Office 的两种保护机制:

[阅读全文]

PDF 文件密码忘记了?加密 PDF 恢复方案全面对比

PDF 加密比其他文件格式更「诡异」——它有用户密码和所有者密码两套机制,安全性差异巨大。上周遇到一个案例:一份加密的 PDF 合同,打不开也打印不了,折腾了一下午终于搞定了。这篇文章把 PDF 密码恢复的技术细节彻底讲清楚。

PDF 加密的两种密码

PDF 的加密机制和其他文件格式最大的不同在于:它有两种独立的密码

用户密码(User Password / Open Password):
  作用:打开文件时必须输入
  效果:文件内容被加密,没有密码无法查看
  恢复难度:取决于加密算法版本
  这是真正意义上的「加密」

所有者密码(Owner Password / Permissions Password):
  作用:限制打印、复制、编辑等操作
  效果:文件可以打开查看,但不能执行受限操作
  恢复难度:★☆☆☆☆(几乎可以秒解)
  这不是真正的加密,只是一个「限制标记」

如何判断你的 PDF 是哪种密码?

场景 1:打开 PDF 时弹出密码输入框
  → 用户密码(需要正经恢复)

场景 2:可以打开查看,但打印按钮灰色/复制文字失败
  → 所有者密码(可以轻松移除)

场景 3:既需要密码打开,又有操作限制
  → 两种密码都设置了(先恢复用户密码,再移除所有者密码)

所有者密码:秒解方案

所有者密码不加密文件内容,只是在 PDF 元数据中设置了一个权限标记。有密码可以修改权限,没密码也可以直接移除。

方法 1:Ghostscript(命令行,免费)

# 安装 Ghostscript
# macOS: brew install ghostscript
# Ubuntu: sudo apt install ghostscript

# 移除所有者密码(重新生成 PDF)
gs -q -dNOPAUSE -dBATCH \
   -sDEVICE=pdfwrite \
   -dCompatibilityLevel=1.4 \
   -sOutputFile=unlocked.pdf \
   encrypted.pdf

# 原理:Ghostscript 读取 PDF 内容后重新生成一个新的 PDF
# 新 PDF 不包含任何权限限制

方法 2:qpdf(命令行,免费)

# 安装 qpdf
# macOS: brew install qpdf
# Ubuntu: sudo apt install qpdf

# 移除所有加密
qpdf --decrypt encrypted.pdf unlocked.pdf

# 如果同时有用户密码,需要提供密码
qpdf --password=yourpassword --decrypt encrypted.pdf unlocked.pdf

方法 3:在线工具

各种在线 PDF 工具(如 ilovepdf.com、smallpdf.com)都可以一键移除所有者密码限制。原理和 Ghostscript 一样——读取内容后重新生成。

[阅读全文]