视频系统 DDD 实战:巴西 IPTV 项目的领域拆分
- 约 8 分钟读完 - 全文 3175 字视频系统 DDD 实战:巴西 IPTV 项目的领域拆分
2021 年到 2025 年,我负责面向巴西市场的 IPTV 视频点播系统。这是一个用户量过百万、业务持续演进的系统。在几次大的产品迭代中,我们引入了领域驱动设计(DDD)来控制系统复杂度。这篇文章会完整复盘当时的领域拆分过程。
一、项目背景
系统核心功能:
- 视频上传:支持 tus / FTP / 网盘 / m3u8 等多种来源
- 视频转码:h264 / h265 多码率输出
- DRM 加密:永新 / 自研加密方案
- 元数据管理:同步 IMDb / TMDB / PP 等外部数据源
- 播放列表:专题、分类、推荐
- 播放鉴权:用户权限、设备管理、防盗链
服务用户:200 万+,峰值日活 40 万。
二、为什么需要 DDD?
2.1 早期的问题
系统最初是一个单体应用,模块按技术分层组织:
controller/
service/
dao/
entity/
这种结构在业务简单时没问题,但随着功能增加,开始出现:
- 一个需求改动涉及多个「层」,改哪里不确定
- 业务逻辑散落在 Service 中,相似功能重复实现
- 转码、鉴权、元数据互相耦合,改一个模块影响多个模块
- 新成员上手慢,需要全局理解才敢改代码
2.2 DDD 带来的改变
DDD 不是银弹,但它帮我们做了两件事:
- 按业务边界组织代码,而不是按技术分层
- 显式定义领域概念,让业务逻辑有明确的归属
三、领域划分
3.1 核心域识别
我们先识别出系统的核心业务域:
flowchart TD
content["内容域 (Content)<br/>媒资管理、视频文件、转码任务、DRM 加密"]
metadata["元数据域 (Metadata)<br/>影片信息、演员、分类、标签、外部数据源同步"]
playback["播放域 (Playback)<br/>播放鉴权、播放地址生成、防盗链、码率适配"]
user["用户域 (User)<br/>用户账户、订阅、设备、播放历史"]
recommendation["推荐域 (Recommendation)<br/>播放列表、专题、个性化推荐"]
billing["计费域 (Billing)<br/>套餐、订单、支付、用量统计"]
content --- metadata --- playback --- user --- recommendation --- billing
3.2 域间关系
flowchart LR
playback["播放域"] -- 依赖 --> content["内容域"]
playback -- 依赖 --> user["用户域"]
recommendation["推荐域"] -- 依赖 --> metadata["元数据域"]
recommendation -- "依赖(获取热门数据)" --> playback
billing["计费域"] -- 依赖 --> user
metadata -- 依赖 --> external["外部数据源"]
依赖方向尽量保持单向,避免循环依赖。
四、内容域详细设计
内容域是最复杂的域之一,我们以它为例展开。
4.1 聚合根设计
flowchart TD
root["MediaAsset(媒资聚合根)"]
file["MediaFile(视频文件)"]
raw["原始文件"]
transcoded["转码后文件(多码率)"]
drmfile["DRM 加密文件"]
job["TranscodingJob(转码任务)"]
license["DrmLicense(DRM 许可证信息)"]
root --> file
file --> raw
file --> transcoded
file --> drmfile
root --> job
root --> license
MediaAsset 是聚合根,外部系统只能通过它访问内部实体。
4.2 关键领域事件
内容域会产生大量领域事件,其他域可以订阅:
| 事件 | 含义 | 订阅方 |
|---|---|---|
MediaAssetUploaded |
媒资上传完成 | 转码服务 |
TranscodingCompleted |
转码完成 | 内容域自身、播放域 |
DrmEncryptionCompleted |
DRM 加密完成 | 播放域 |
MetadataSynced |
元数据同步完成 | 推荐域 |
MediaAssetPublished |
媒资发布上线 | 推荐域、搜索服务 |
4.3 状态流转
媒资从上传到发布的状态机:
flowchart TD
UPLOADED --> PENDING_TRANSCODE --> TRANSCODING --> PENDING_DRM --> DRM_ENCRYPTED --> PENDING_METADATA --> METADATA_READY --> FINAL["PUBLISHED / REJECTED"]
每个状态变更都通过领域事件触发下一步操作。
五、分层架构
每个领域内部采用 DDD 经典四层架构:
flowchart TD
ui["用户接口层<br/>REST Controller / DTO"]
app["应用层<br/>Application Service / 用例编排"]
domain["领域层<br/>Entity / Value Object / Domain Service / Repository Interface / Domain Event"]
infra["基础设施层<br/>Repository Impl / MQ Client / External API Client / DB / Cache"]
ui --- app --- domain --- infra
5.1 领域层示例
public class MediaAsset {
private MediaAssetId id;
private String title;
private MediaStatus status;
private List<MediaFile> files;
private List<TranscodingJob> transcodingJobs;
public void upload(MediaFile file) {
this.files.add(file);
this.status = MediaStatus.UPLOADED;
registerEvent(new MediaAssetUploadedEvent(this.id));
}
public void completeTranscoding(TranscodingJob job) {
this.transcodingJobs.add(job);
this.status = MediaStatus.TRANSCODING_COMPLETED;
registerEvent(new TranscodingCompletedEvent(this.id, job.getOutputFiles()));
}
}5.2 防腐层设计
与外部系统(IMDb / TMDB / PP)对接时,使用防腐层(Anti-Corruption Layer):
public interface MetadataSourceClient {
ExternalMetadata fetch(String externalId);
}
// IMDb 适配器
public class ImdbClientAdapter implements MetadataSourceClient {
private final ImdbApiClient imdbClient;
@Override
public ExternalMetadata fetch(String externalId) {
ImdbResponse response = imdbClient.fetch(externalId);
return MetadataConverter.fromImdb(response);
}
}这样外部 API 的数据结构变化不会直接影响到我们内部的元数据模型。
六、微服务拆分过程
6.1 第一次拆分:按技术边界
早期我们按技术栈拆:
- upload-service
- transcoding-service
- metadata-service
- playback-service
结果导致一个业务操作要跨多个服务,分布式事务复杂。
6.2 第二次拆分:按领域边界
调整后按 DDD 领域拆分:
- content-service(内容域)
- metadata-service(元数据域)
- playback-service(播放域)
- user-service(用户域)
- recommendation-service(推荐域)
- billing-service(计费域)
每个服务有清晰的业务边界和聚合根。
6.3 服务间通信
| 场景 | 通信方式 | 说明 |
|---|---|---|
| 领域事件传播 | RocketMQ | 异步、最终一致 |
| 同步查询 | HTTP / gRPC | 强一致查询 |
| 配置/字典数据 | Redis 缓存 | 高频读取 |
七、领域事件实现
7.1 事件定义
public class TranscodingCompletedEvent extends DomainEvent {
private final MediaAssetId mediaAssetId;
private final List<FileUrl> outputFiles;
private final Instant completedAt;
}7.2 事件发布
领域层只负责注册事件,应用层统一发布:
@Transactional
public void handleTranscodingCallback(TranscodingJobId jobId, List<FileUrl> outputs) {
MediaAsset asset = mediaAssetRepository.findByJobId(jobId);
asset.completeTranscoding(new TranscodingJob(jobId, outputs));
mediaAssetRepository.save(asset);
eventPublisher.publish(asset.getDomainEvents());
asset.clearDomainEvents();
}7.3 事件消费
@Component
public class PlaybackEventListener {
@RocketMQMessageListener(topic = "content-events", selectorExpression = "TranscodingCompleted")
public class Listener implements RocketMQListener<TranscodingCompletedEvent> {
@Override
public void onMessage(TranscodingCompletedEvent event) {
playbackService.preparePlayUrls(event.getMediaAssetId(), event.getOutputFiles());
}
}
}八、几次关键演进
8.1 从单体到领域化单体
第一步不是拆微服务,而是先在单体内部按领域组织代码包:
com.ftmao.iptv.content
com.ftmao.iptv.metadata
com.ftmao.iptv.playback
这样可以在不引入分布式复杂度的前提下,先验证领域划分是否合理。
8.2 从同步调用到事件驱动
转码完成后,原本播放域通过接口轮询内容域查询状态。后来改为内容域发布 TranscodingCompleted 事件,播放域订阅。
收益:
- 内容域和播放域解耦
- 新增订阅方不需要改内容域
- 系统扩展性更好
8.3 引入 CQRS 处理播放查询
播放接口对性能要求很高,我们单独做了读模型:
- 写模型:MediaAsset 聚合根
- 读模型:Redis 中的播放信息缓存
当内容发布或更新时,通过事件同步更新读模型。
九、踩过的坑
9.1 领域划分过细
早期我们划分了 10 多个领域,导致服务数量爆炸,运维成本剧增。
教训:领域数量要匹配团队规模。小团队 4-6 个领域足够。
9.2 领域事件丢失
消息队列配置不当,出现过领域事件丢失,导致播放域看不到新发布的内容。
教训:关键领域事件必须持久化,支持重放和补偿。
9.3 贫血领域模型
很多「DDD」项目最后变成了 Transaction Script 套壳,实体只有 getter/setter。
教训:要把业务规则和状态变更放在领域对象里,而不是 Service 里。
9.4 过度设计聚合根
一开始想把所有内容相关的东西都放到 MediaAsset 聚合根里,结果聚合根过大,事务边界混乱。
教训:聚合根要小而聚焦,跨聚合用领域事件协作。
十、效果与反思
10.1 量化效果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 新需求平均改动文件数 | 40 个 | 20 个 |
| 线上因耦合导致的 Bug | 10 次/月 | 2 次/月 |
| 新成员独立上手时间 | 3 周 | 1 周 |
10.2 核心反思
- DDD 不是先拆分微服务,而是先拆清楚业务边界。微服务是结果,不是起点。
- 领域事件是域间协作的核心,但事件丢失和顺序问题必须重视。
- 不要追求完美的领域模型,先有一个可用的模型,再迭代优化。
- DDD 需要团队共识,不是架构师一个人画几张图就行的。
十一、总结
巴西 IPTV 项目让我对 DDD 有了真实的体感。它不是教科书上的概念,而是解决复杂业务系统演化问题的工程方法。
对于视频系统这类业务复杂、流程长、多团队协作的系统,DDD 能带来的最大价值是:
- 让每个业务概念都有清晰的归属
- 让系统变更的影响范围可控
- 让新成员能更快理解业务
如果你也在做 DDD 实践,欢迎在评论区交流领域划分和事件驱动的经验。