架构设计:文件服务的设计与实现

在面向架构编程一文中,我阐述了自己对架构和代码之间的关系的看法:「代码需要反映出架构」!
本文通过对文件服务核心功能的设计与实现,来验证这一观点 。设计过程融合了「用例驱动设计」和「领域驱动设计」!

本文及后续几篇文章会设计并开发几个实际的系统,同时尝试总结一套适用的架构设计与开发流程 。欢迎探讨!
功能文件服务器的核心功能就两个:「文件上传」和「文件下载」!其中上传可能需要支持断点续传、分片上传 。而下载可能需要进行下载保护,例如非指定客户端无法下载 。
除了这两个核心功能,一般都会有一个额外功能,就是「转换」!转换包括:
  • 图片规格转换:一张图片需要切分多个不同的尺寸
  • 添加水印:图片或视频需要添加水印
  • 格式转换:
  • 文件格式转换:office转pdf,pdf转word,pdf转图片,office转图片等
  • 视频格式转换:mp4转m3u8,码率转换等
除了上面的业务功能外,还包括如下非功能性约束:
  • 安全性:是否需要认证后才能上传或下载
  • 伸缩性:是否支持扩容,提高访问量
  • 可用性:作为基础服务,可用性不低于4个9
  • 可配置性:对于转换方式、上传下载方式等内容需要提供可配置能力
  • 扩展性:能方便的进行功能扩展,例如对转换方式的扩展
初步流程
  • 上传流程

架构设计:文件服务的设计与实现

文章插图
 
  • 下载流程

架构设计:文件服务的设计与实现

文章插图
 
初步模块划分根据功能,可划分如下功能模块:
  • 上传模块(核心模块):处理文件上传
  • 下载模块(核心模块):处理文件下载
  • 转换模块:处理文件类型转换
  • 配置模块:对文件服务进行配置
  • 安全模块:对文件服务进行安全保护
架构设计首先通过分层架构对模块进行一个大致的划分,按照领域设计的分层方式:
  • 应用层:配置模块,安全模块
  • 领域层:上传模块,下载模块,转换模块

架构设计:文件服务的设计与实现

文章插图
 
从上面的流程可以看到「上传模块」对「转换模块」有一定的依赖,像下面这样:
架构设计:文件服务的设计与实现

文章插图
 
但是,「上传模块」是核心模块,而「转换模块」是非核心模块 。核心模块的功能相对稳定,非核心模块的功能相对不稳定 。让稳定的模块去依赖不稳定的模块,会导致稳定的模块也不稳定,所以需要对依赖进行「倒置」 。
架构设计:文件服务的设计与实现

文章插图
 
「依赖倒置」解决了模块依赖问题 。但是转换是个很耗时的过程,例如用户上传视频,在不转换的情况下,只要上传完成就可以得到响应,但是如果转换的话,可能就需要双倍甚至三四倍的时间才能得到反馈,体验非常的不好 。且一般上传和观看的时效性并不需要即时性,所以转换应该是个异步的过程 。
异步执行的方式很多,比如基于事件,自定义线程等 。这里通过事件的方式来进行处理 。(领域事件可参考领域设计:领域事件)
架构设计:文件服务的设计与实现

文章插图
 
文件上传会创建UploadEvent,UploadListener监听UploadEvent事件,当监听到了UploadEvent,则执行转换 。
转换流程异步化后,如何告知客户端转换结果呢?有几种方案:
  • 上传完成后,文件服务返回一个token,后续业务系统通过token来获取转换后的URL 。此方案需要业务系统请求两次 。
  • 文件服务转换完成后入库,业务系统从数据库获取 。此方案也需要业务系统请求两次,且对不同的业务需要有不同的实现 。
  • 文件服务转换完成后回调业务系统 。此方案可能需要实现不同的业务回调接口 。
  • 文件服务器返回一个事先生成的URL,在文件转换完成时返回特定状态码,在转换完成后,返回文件 。对于某些场景无法事先生成URL,例如office转图片,一个文档会转成多张图片,转换前无法得知图片URL
目前主流做法是第一种,不过为保证文件服务器的适用性,需要能支持多种方案 。故对转换后的通知也基于事件进行处理,转换后创建对应事件,关注该事件的对象来做出对应的处理 。一个可能处理流程如下:


推荐阅读