第4章:平台架构设计——分层、微服务与DDD在水印中的实践

做水印平台这几年,我最大的感触是:架构设计不是画图比赛。你画得再漂亮,线上跑起来该崩还是崩。水印场景有个特点——高吞吐、低延迟、算法更新快。这三点叠加,对平台架构的考验就很大了。

我先说说整体思路。

4.1 分层架构:四层职责清晰

我习惯把水印平台分成四层:接入层、业务层、算法层、数据层。每层各司其职,层间通过明确接口通信。我早期吃过不分层的亏——所有逻辑揉在一起,改一个水印位置参数能牵连到数据库连接池,那滋味可不好受。

这四层具体干什么?

层级 核心职责 常见组件/技术
接入层 协议适配、认证限流、请求路由 Nginx、Kong、Spring Cloud Gateway
业务层 水印任务编排、模板管理、策略调度 微服务(任务服务、模板服务、策略服务)
算法层 嵌入/提取、抗攻击、模型热部署 Python/Java推理引擎、模型仓库、算子容器
数据层 水印元数据存储、算法模型、操作审计 MySQL、MongoDB、Redis、OSS

分层不是目的,解耦才是。我给你看一个我在实际项目中遇到的问题。

我曾经踩过一个坑: 在接入层直接调用了算法层的推理接口,跳过业务层。当时觉得"省一个RPC调用,快一点"。后来算法模型升级,接口参数变了,接入层跟着改,业务层状态没同步,导致线上水印数据对不上。记住:接入层只管"门面"的事,别替业务层做决策。

你想想看,如果业务层能集中收口所有策略逻辑,算法层只用专注算力,数据层只用管存储,每一层改东西时心理负担会小很多。

4.2 微服务拆分原则:按边界切,别按功能切

水印平台要不要拆微服务?我的答案是:拆,但要找准边界。我见过有人按功能拆——"搞个打水印服务、搞个验水印服务、搞个日志服务"。结果呢?验水印时得查模板,打水印时又得调日志——服务之间互相调来调去,延迟翻倍。说白了,按功能拆是"伪微服务"。

我个人习惯用三条原则来判断拆分粒度:

  • 独立变更原则: 两个功能如果经常一起改,就不该拆成两个服务。
  • 独立扩展原则: 两个功能如果有不同的吞吐需求,就应该拆开。
  • 数据主权原则: 一个服务应该拥有自己完整的数据边界,而不是共享数据库。

举个例子:水印任务执行和模板管理。任务执行需要低延迟、高并发,模板管理则相对低频。这两个自然拆开,独立部署、独立扩缩容。模板改了,任务执行不用重启。

避坑指南: 拆微服务前,先想好"如果这个服务挂了,影响面有多大"。我曾经把一个日志收集服务拆出来,结果它挂了之后整个业务链路都断了。嗯,因为业务代码里强依赖了日志服务返回的状态码。这不是微服务的锅,是服务间的耦合太隐蔽。

4.3 领域驱动设计在水印场景的落地

讲DDD,我不喜欢上来就讲"实体、值对象、聚合根"。对我来说,DDD的核心就一句话:用业务语言来构建代码模型。水印场景里,我们可以这么干。

4.3.1 核心领域概念

我把水印平台的领域模型做了一张图,你看完就懂了——

水印平台领域模型核心结构 聚合根:水印任务 (WatermarkTask) 实体:水印模板 实体:算法镜像 实体:执行节点 值对象:水印位置 值对象:嵌入强度 值对象:算法参数 值对象:用户标识 领域服务:水印嵌入引擎 (WatermarkEmbedService) 仓储:水印任务仓储 仓储:水印模板仓储 仓储:执行记录仓储 领域事件:水印嵌入完成 Event 领域事件:水印检测失败 Event 聚合根承载业务不变性,仓储负责持久化抽象, 领域事件驱动跨服务最终一致性。

为什么要这么建模?

举个例子。水印位置为什么是值对象?因为"左上角"、"右下角"没有唯一标识,只看坐标值。两个坐标相同的值对象就是相等的。水印任务为什么是聚合根?因为任务的启动、暂停、取消都依赖它的状态,你得经过它才能操作子实体。

我个人习惯: 做DDD建模时,先和业务方一起画"事件风暴"(Event Storming)。在水印这个领域,核心事件其实不多,比如"用户上传素材"、"水印嵌入完成"、"检测到篡改"、"审计追溯查询"等。每个事件引出一个集合——"谁干的?怎么干的?参数是什么?"。把这些集合原子化,再反过来设计领域模型,顺很多。

另外,领域事件在水印平台里特别好用。比如用户一直在问:"水印嵌入后,我什么时候能拿到图片?"以前的做法是轮询数据库,负载很高。后来我改成事件驱动——算法层嵌入完成后,抛一个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在水印场景的核心是把业务含义显式化,用领域事件解耦跨服务通信。

架构没有银弹。我分享的这些,都是我在真实项目里摔过跟头后总结出来的。你应用时一定要结合自己的场景。比如你只管做大图量水印,可能算法层比你想象的重要;如果你做的是电商小图,可能接入层的流量控制更关键。嗯,先看你的业务长什么样,再决定架构怎么搭。