PAYATHON 2026

数千 TB 图像应存储在 FileSystem 还是 SQL server DB,是否分库?

技术老齐

图像放独立存储,SQL Server 保存关联信息

交易图像已有数十 TB,且可能增长到数千 TB 时,建议把文件放在对象存储或可扩展的文件存储中。SQL Server 只保存交易关系、对象标识、文件大小、校验值和处理状态。

这些元数据通常可以留在现有业务数据库中,通过 TransactionId 关联交易。查看交易详情时,应用先查询图像记录,通过鉴权后再读取文件,或生成短期有效的访问链接。

如果必须把图像二进制内容存入 SQL Server,是否单独建库要看备份恢复要求和资源负载。同一实例内分库便于独立运维,但无法保证性能隔离。

为什么优先选择外部存储

交易详情页通常只需要按交易编号查找几张图像。由关系数据库查找关联记录、文件存储传输图像,适合这种访问方式。

直接把图像写入普通的 varbinary(max) 列,会增加数据库的数据量和事务日志写入量,也会加重备份、恢复的负担。图像不断积累后,数据库运维就需要处理大量与交易查询无关的二进制内容。

数据库中存有大图像,并不必然让其他表的每次查询都变慢。实际影响主要取决于:

  • 图像读写是否与交易请求竞争磁盘吞吐、CPU、内存或网络带宽。
  • 查询是否意外返回图像列,例如使用 SELECT *。
  • 大批量上传、删除和备份是否与业务高峰重叠。
  • 存储设备、索引和连接管理能否承受实际并发。

对象存储适合通过对象标识上传和读取文件。如果现有应用依赖文件路径、共享目录或文件接口,可以考虑文件存储。容量达到数千 TB 后,需要评估分布式存储的容量与故障恢复能力,单台 Web 服务器的本地磁盘很难满足这类需求。

如果必须存进 SQL Server,是否应该分库

方案适用条件需要承担的影响
与交易放在同一个数据库图像规模可控,且需要在同一个数据库事务中提交交易与图像图像增长会增加该数据库的备份、恢复和容量管理负担
放在同一实例的独立数据库图像需要独立的备份计划、权限或保留周期仍会共享实例资源;底层存储也可能共享
放在独立实例或服务器图像负载已经影响业务,且需要分配独立资源部署、监控和跨系统一致性处理更复杂
文件放外部存储,元数据放业务库图像持续增长,主要按交易查询和下载需要处理文件与数据库之间的一致性

单独建立图像数据库能解决部分运维问题,吞吐瓶颈仍需单独处理。两个数据库如果位于同一实例、同一组磁盘上,图像写入仍可能影响交易响应。

分库还会影响关联约束。SQL Server 不支持跨数据库的声明式外键,交易表与图像记录分处不同数据库时,需要由应用或其他机制维护关联关系。

如果希望保留数据库事务语义,同时通过文件流访问大对象,可以评估 SQL Server 的 FILESTREAM。它把 varbinary(max) 数据保存在文件系统中,由 SQL Server 管理事务关系。采用前应核对目标版本、操作系统和部署方式是否支持,并做好容量、备份和恢复规划。

推荐的表结构

图像元数据可以与交易表放在同一个数据库。以下示例假设已有 dbo.Transactions 表,其主键 TransactionId 的类型为 bigint:

CREATE TABLE dbo.TransactionImage
(
    ImageId bigint IDENTITY(1,1) NOT NULL
        CONSTRAINT PK_TransactionImage PRIMARY KEY,

    TransactionId bigint NOT NULL,
    ObjectKey varchar(512) NOT NULL,
    ContentType varchar(100) NOT NULL,
    ByteLength bigint NOT NULL,
    Sha256 binary(32) NULL,

    UploadState varchar(16) NOT NULL
        CONSTRAINT DF_TransactionImage_UploadState
        DEFAULT ('Pending'),

    CreatedAt datetime2(3) NOT NULL
        CONSTRAINT DF_TransactionImage_CreatedAt
        DEFAULT (SYSUTCDATETIME()),

    CONSTRAINT FK_TransactionImage_Transaction
        FOREIGN KEY (TransactionId)
        REFERENCES dbo.Transactions (TransactionId),

    CONSTRAINT UQ_TransactionImage_ObjectKey
        UNIQUE (ObjectKey),

    CONSTRAINT CK_TransactionImage_ByteLength
        CHECK (ByteLength >= 0),

    CONSTRAINT CK_TransactionImage_UploadState
        CHECK (UploadState IN ('Pending', 'Ready', 'Failed'))
);

CREATE INDEX IX_TransactionImage_TransactionId_UploadState
    ON dbo.TransactionImage (TransactionId, UploadState);

这里假设对象标识使用 ASCII 字符,长度不超过 512 字节。如果接入多个存储位置,还应增加存储位置字段,并根据实际范围设计唯一约束。

ObjectKey 保存服务端生成的稳定标识,例如:

transactions/2026/11/02/7d83d2e5c7914c3b9e88f0f4a69b2d10.jpg

带过期时间的签名 URL 不应作为永久地址保存。存储位置和访问域名可能调整,应用应根据对象标识生成访问地址。

查询交易图像时,只返回所需字段。@TransactionId 应由数据库驱动作为参数传入:

SELECT
    ImageId,
    ObjectKey,
    ContentType,
    ByteLength
FROM dbo.TransactionImage
WHERE TransactionId = @TransactionId
  AND UploadState = 'Ready'
ORDER BY ImageId;

应用应在数据库查询结束后再读取图像内容,避免传输大文件时一直占用数据库连接。

上传和删除如何保持一致

普通 SQL 事务无法自动回滚外部文件上传,因此需要明确失败后的处理和重试流程。

上传可以按以下步骤实现:

  1. 服务端完成鉴权,生成唯一的对象标识,并在数据库中建立 Pending 记录。
  2. 上传图像,确认存储服务返回成功,按需要核对文件大小和校验值。
  3. 将记录更新为 Ready,交易详情页只展示该状态的图像。
  4. 后台任务定期检查长期停留在 Pending 的记录,确认文件状态后重试、补记成功或清理。

客户端超时时,文件可能已经保存成功,不能仅凭超时就认定上传失败。重试时应复用同一个上传标识,并采用存储服务支持的条件写入等机制,避免生成重复文件或意外覆盖已有文件。

删除也需要重试机制。可以先在数据库中记录删除任务,由后台删除文件,确认成功后再清理数据库。删除任务必须能够安全地重复执行。

当数据达到数十 TB 或数千 TB 时怎么处理

上线前应完成容量规划,至少估算原图、缩略图和保留版本的容量增长:

每日新增容量 = 每日新增图像数 × 平均图像大小

实际所需空间还要计入副本、备份、历史版本和容量余量。这些开销取决于存储方案,无法统一按某个固定倍数估算。

扩容和拆分应根据实际瓶颈决定:

  • 交易详情查询变慢时,先检查按 TransactionId 查询的索引、执行计划和返回量。
  • 文件传输占满带宽时,评估缩略图、缓存及独立的文件分发能力。
  • 备份或恢复时间超过业务目标时,调整数据保留、归档和存储布局。
  • 元数据本身已经很大时,再根据查询与清理方式评估表分区或分库。

表分区有助于管理和清理大表,对查询性能的改善则取决于具体查询。分库也会增加路由、关联查询和一致性处理成本,不宜只凭容量达到多少 TB 就决定。

备份需要同时覆盖数据库和图像存储。数据库恢复到某个时间点后,必须能找到该时间点记录引用的文件。使用不可变对象标识、保留必要的历史版本,并让文件删除策略覆盖数据库的可恢复时间范围,有助于减少恢复后缺图的情况。上线前应实际演练恢复,确认耗时符合业务要求。

访问图像时的注意事项

交易图像通常需要鉴权,每次读取都应验证用户是否有权查看对应交易。随机文件名只能降低猜测概率,不能代替权限检查。使用临时签名 URL 时,应限制有效期,避免链接通过日志泄露。

上传时,不要直接信任文件名、扩展名或客户端声明的 Content-Type。应限制文件大小和图像尺寸,验证文件内容,并使用服务端生成的存储标识。使用文件存储时,还需要防止路径穿越,避免直接把用户输入作为磁盘路径。

备注:内容仅供参考。