第4章:平台架构设计——分层、微服务与DDD在水印中的实践
做水印平台这几年,我最大的感触是:架构设计不是画图比赛。你画得再漂亮,线上跑起来该崩还是崩。水印场景有个特点——高吞吐、低延迟、算法更新快。这三点叠加,对平台架构的考验就很大了。
我先说说整体思路。
4.1 分层架构:四层职责清晰
我习惯把水印平台分成四层:接入层、业务层、算法层、数据层。每层各司其职,层间通过明确接口通信。我早期吃过不分层的亏——所有逻辑揉在一起,改一个水印位置参数能牵连到数据库连接池,那滋味可不好受。
这四层具体干什么?
| 层级 | 核心职责 | 常见组件/技术 |
|---|---|---|
| 接入层 | 协议适配、认证限流、请求路由 | Nginx、Kong、Spring Cloud Gateway |
| 业务层 | 水印任务编排、模板管理、策略调度 | 微服务(任务服务、模板服务、策略服务) |
| 算法层 | 嵌入/提取、抗攻击、模型热部署 | Python/Java推理引擎、模型仓库、算子容器 |
| 数据层 | 水印元数据存储、算法模型、操作审计 | MySQL、MongoDB、Redis、OSS |
分层不是目的,解耦才是。我给你看一个我在实际项目中遇到的问题。
你想想看,如果业务层能集中收口所有策略逻辑,算法层只用专注算力,数据层只用管存储,每一层改东西时心理负担会小很多。
4.2 微服务拆分原则:按边界切,别按功能切
水印平台要不要拆微服务?我的答案是:拆,但要找准边界。我见过有人按功能拆——"搞个打水印服务、搞个验水印服务、搞个日志服务"。结果呢?验水印时得查模板,打水印时又得调日志——服务之间互相调来调去,延迟翻倍。说白了,按功能拆是"伪微服务"。
我个人习惯用三条原则来判断拆分粒度:
- 独立变更原则: 两个功能如果经常一起改,就不该拆成两个服务。
- 独立扩展原则: 两个功能如果有不同的吞吐需求,就应该拆开。
- 数据主权原则: 一个服务应该拥有自己完整的数据边界,而不是共享数据库。
举个例子:水印任务执行和模板管理。任务执行需要低延迟、高并发,模板管理则相对低频。这两个自然拆开,独立部署、独立扩缩容。模板改了,任务执行不用重启。
4.3 领域驱动设计在水印场景的落地
讲DDD,我不喜欢上来就讲"实体、值对象、聚合根"。对我来说,DDD的核心就一句话:用业务语言来构建代码模型。水印场景里,我们可以这么干。
4.3.1 核心领域概念
我把水印平台的领域模型做了一张图,你看完就懂了——
为什么要这么建模?
举个例子。水印位置为什么是值对象?因为"左上角"、"右下角"没有唯一标识,只看坐标值。两个坐标相同的值对象就是相等的。水印任务为什么是聚合根?因为任务的启动、暂停、取消都依赖它的状态,你得经过它才能操作子实体。
另外,领域事件在水印平台里特别好用。比如用户一直在问:"水印嵌入后,我什么时候能拿到图片?"以前的做法是轮询数据库,负载很高。后来我改成事件驱动——算法层嵌入完成后,抛一个WatermarkEmbeddedEvent,业务层订阅它,更新任务状态,再通知接入层回调用户。数据和逻辑都解耦了。
4.3.2 代码层如何映射?
下面是我在一个项目里的真实代码结构,不一定适合所有人,但你可以参考:
// 聚合根
public class WatermarkTask {
private TaskId id;
private List<WatermarkEmbed> embeds; // 实体集合
private WatermarkPosition position; // 值对象
private TaskStatus status;
public WatermarkTask(TaskId id, WatermarkPosition position) {
this.id = id;
this.position = position;
this.status = TaskStatus.CREATED;
}
public void start() {
// 业务逻辑校验...
this.status = TaskStatus.RUNNING;
// 发布领域事件
DomainEventPublisher.publish(new TaskStartedEvent(this.id));
}
}
// 值对象(不可变)
public class WatermarkPosition {
private final int x;
private final int y;
public WatermarkPosition(int x, int y) {
this.x = x;
this.y = y;
}
// equals() & hashCode() 只比较 x, y
}
// 领域服务
public class WatermarkEmbedService {
public void executeTask(WatermarkTask task) {
// 调用算法层嵌入水印
// 更新实体状态
// 发布领域事件
}
}
// 仓储接口
public interface WatermarkTaskRepository {
WatermarkTask findById(TaskId id);
void save(WatermarkTask task);
}
WatermarkTask的save()方法统一持久化。这样才能保证一致性。
4.4 小结
这一章内容不少,我简单收一下:
- 分层架构让团队并行开发成为可能,但层间接口一定要约定清楚。
- 微服务拆分要按业务边界,别按功能模块,多想想"变更影响面"。
- DDD在水印场景的核心是把业务含义显式化,用领域事件解耦跨服务通信。
架构没有银弹。我分享的这些,都是我在真实项目里摔过跟头后总结出来的。你应用时一定要结合自己的场景。比如你只管做大图量水印,可能算法层比你想象的重要;如果你做的是电商小图,可能接入层的流量控制更关键。嗯,先看你的业务长什么样,再决定架构怎么搭。