3. 对象存储核心领域模型:Bucket 与 Object
在对象存储领域,Bucket(存储桶)与Object(对象)是构成整个系统的两大核心实体。理解它们的边界、属性及生命周期,是进行领域驱动设计(DDD)与数据库建模的基础。本章将严格遵循 DDD 的战术建模方法,定义这两个聚合根,并设计其状态机。
3.1 核心实体定义与边界
从 DDD 视角看,Bucket 是一个聚合根(Aggregate Root),它作为对象的命名空间和权限边界;Object 则是另一个聚合根,其生命周期完全依赖于 Bucket,但拥有独立的标识和版本控制。
| 实体 | DDD 角色 | 唯一标识 | 边界职责 |
|---|---|---|---|
| Bucket | 聚合根 | Bucket Name(全局唯一) | 命名空间隔离、权限策略、地域锁定、版本控制开关 |
| Object | 聚合根 | Bucket + Object Key + VersionId | 数据内容存储、元数据管理、分片上传、生命周期流转 |
设计要点:Bucket 与 Object 之间是一对多的强关联关系,但为了支持高并发与水平扩展,Object 不应持有 Bucket 的领域引用,仅通过 Bucket Name 进行逻辑关联。
3.2 属性设计
3.2.1 Bucket 属性
- BucketName:全局唯一字符串,遵循 DNS 命名规范(小写字母、数字、短横线)。
- Region:所属地域,决定了数据物理存储位置。
- StorageClass:默认存储类型(标准、低频、归档)。
- VersioningStatus:版本控制状态(Enabled / Suspended / Unversioned)。
- ACL / Policy:访问控制列表与桶策略 JSON。
- CreationDate:创建时间戳。
3.2.2 Object 属性
- ObjectKey:在 Bucket 内唯一的对象键,支持路径分隔符
/。 - VersionId:版本标识,由 UUID 或时间戳生成。
- ContentLength:对象大小(字节)。
- ContentType:MIME 类型。
- ETag:MD5 或 SHA256 校验值。
- LastModified:最后修改时间。
- StorageClass:当前存储类型,可覆盖 Bucket 默认值。
- Metadata:用户自定义元数据(KV 对)。
PostgreSQL 映射建议:Bucket 表使用
VARCHAR(63) 作为主键;Object 表使用 (bucket_name, object_key, version_id) 复合主键,并利用 JSONB 存储用户元数据,以支持灵活的查询与索引。
3.3 生命周期状态机
对象存储的核心价值之一在于自动化的生命周期管理。Object 的状态流转直接决定了存储成本与访问性能。下图展示了 Object 从创建到最终删除的完整状态机。
3.4 状态流转规则
生命周期状态机遵循以下核心规则,这些规则将直接映射到 PostgreSQL 的触发器或定时任务中:
- 自动降冷:对象在标准存储中超过 30 天无读取,自动转为低频存储;超过 90 天转为归档存储。
- 手动删除:任何状态的对象均可被手动删除,进入“已删除(标记)”状态。
- 过期清理:处于“已删除”状态超过 7 天的对象,由后台任务彻底清除物理数据。
- 版本控制:开启版本控制后,删除操作仅新增删除标记,历史版本保留,直到显式清理。
避坑指南:在 PostgreSQL 中实现状态机时,不要使用
ENUM 直接存储状态码,建议使用 VARCHAR(20) 并配合 CHECK 约束,以便未来扩展新的存储类型(如深度归档)。同时,为 last_accessed_at 字段建立索引,以支持高效的生命周期扫描查询。
3.5 领域事件与集成
在 DDD 实践中,状态变更应触发领域事件,以便其他限界上下文(如计费、审计、通知)做出响应。以下是关键事件列表:
| 事件名称 | 触发条件 | 事件负载(关键字段) |
|---|---|---|
ObjectUploaded |
对象上传完成 | BucketName, ObjectKey, VersionId, ContentLength |
ObjectStorageClassChanged |
生命周期自动降冷 | BucketName, ObjectKey, OldClass, NewClass |
ObjectDeleted |
手动删除或版本标记 | BucketName, ObjectKey, VersionId, IsDeleteMarker |
ObjectExpired |
彻底清理物理数据 | BucketName, ObjectKey, VersionId, ExpiredAt |
这些事件可以通过 PostgreSQL 的 LISTEN/NOTIFY 机制或 Outbox 模式发布到消息队列,实现最终一致性。
-- 示例:Object 状态变更的 Outbox 表结构
CREATE TABLE object_event_outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id VARCHAR(255) NOT NULL, -- BucketName/ObjectKey/VersionId
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
published_at TIMESTAMPTZ
);
CREATE INDEX idx_outbox_unpublished ON object_event_outbox (created_at)
WHERE published_at IS NULL;