需求:
获取聊天记录里产生的文件,下载并保存,存储到文件服务器(用oss表示)中
过程:
看起来很简单,直接干;
初步思路:
- 请求企业微信文件下载接口,先下载到本地
- 从本地读取文件,上传到oss
看起来并没什么问题,直接干!
踩坑1
既然是从企业微信接口下载的,那就先看看企业微信的开发文档吧
纳尼,这些参数是什么???
好吧,获取这些参数的方式请参考《企业微信会话存档-如何高性能存储海量聊天记录》,这里就不深究了:获取聊天记录,消息类型有很多,如果消息包含文件类型,那就会包含以上字段;
因为涉及其他业务,一条消息可能需要多次处理,所以处理消息的方式使用事件广播的方式,这里只需要监听消息事件就可以了。
通过API文档了解到,媒体消息是需要进行分片拉取的,每个分片大小最大为512kb;
实际的代码太长了,为方便理解使用伪代码表示,如下(文中所有的代码均为伪代码)
// 0. 记录文件信息表
inert(fileInfo);
// 1. 初始化sdk;验证秘钥并解密
sdk = init();
// 2. 创建文件
file = createFile();
byte[] b = new byte[0];
// 3. 拉取消息
indexBuf = "";
while(true) {
// 3.1 获取数据
result = getMediaData(indexBuf);
// 3.2 将数据保存
// 企微返回的就是byte数组
byte[] b1 = result.data;
b = b.length == 0 ? b1 : ArrayUtil.addAll(b, b1);
// 3.3 判断文件是否下载完成;1表示下载完成
if (1 == isFinish) {
break;
} else {
// 记录 indexBuf,即文件下载到哪里了,下次请求带上该参数
indexBuf = result.indexBuf;
}
}
// 4. 保存到文件
file.write(b);
// 5. 上传到oss
ossService.upload(file);
// 6. 删除文件
del(file);
写到这里,完成了,上线上线
踩坑2,3,4,5,6…
一个一个写太慢了,篇幅长而无用也是浪费读者时间,这里一并列举:
- 项目跑的时候,同事发了个win11的系统镜像;哎,系统怎么停了,服务器内存怎么满了?
- 一个一个下载文件,太慢了,改成多线程吧;使用线程池,最大开了8个线程,等待队列开了20个;项目重启,刚才的文件呢,怎么丢了一部分?
- oss的服务怎么突然停了,那我的项目怎么跑?
- 纳尼,不用oss了,换成s3服务器了?因为oss在升级,过段时间再换回来?内心…
- 怎么客户发送了个文件,消息记录有这条消息,但是文件没法下载呢,也没个提示?
- 大文件就算了,我就发了个不到1M的文件,就算有延迟,也不应该等了几个小时还没下载完吧?
- 为什么有的文件没有传到oss呢,没传到oss的会丢失吗?
- 文件太多了,能不能优化一下,节省一些带宽?
- 客户:为什么,为什么,为什么…能不能这样处理…想要个这样的效果…
修复ing
剖析原始代码的问题:
- 使用byte[] 数组接收数据,但是所有的数据都是存储在内存中的,文件过大会把内存占满
- 因为是请求的企业微信,外网通信,难免会因为网络等原因导致请求失败,如果中途请求失败,文件会丢失
- 文件下载完成之后,直接上传oss,若上传失败,文件又会丢失
逐个针对单个问题简单说一下解决思路(单个看起来有点儿乱,可直接看完整解决方案):
- byte[] 更换为 FileOutputStream;每次while循环里都直接写入并flush();while完成后关闭流;可解决大文件读取在内存中,导致内存不足的问题
- 等待队列改为0;文件记录存入redis;重启项目不会丢失数据
- 记录文件id和状态,使用定时任务去重试上传oss
- 抽象一个文件上传第三方服务器的接口,把使用第三方服务的类型放进配置中心,修改配置中心即可更换上传方式
- 将文件下载的状态也记录起来,同步到页面展示
- 有可能刚好几个线程全是下载的大文件,都很耗时;将大文件小文件分开下载
- 重复多次上传,一直失败,就永久保存,提供文件访问接口
- 使用md5校验文件内容是否一致,如果相同就不重复下载和上传
完整解决方案
经过不断的踩坑,不断的修复,终于得到了一个较为完善的解决方案;
处理流程中,文件有多种状态,为保证文件不丢失,将文件状态记录到表中;各状态分别是:
/**
* -1 初始化值;监听到消息就记录值
*/
Integer INIT = -1;
/**
* 0-微信下载中
*/
Integer DOWNLOADING = 0;
/**
* 1-微信下载失败
*/
Integer WECHAT_ERROR = 1;
/**
* 2-文件存储失败
*/
Integer SAVE_ERROR = 2;
/**
* 6-文件无需下载
*/
Integer NOT_NEED_DOWNLOAD = 6;
/**
* 7-文件存储成功
*/
Integer SUCCESS = 7;
/**
* 8-文件转存oss失败
*/
Integer OSS_ERROR = 8;
/**
* 9-文件转存oss成功
*/
Integer OSS_SUCCESS = 9;
程序的执行流程,纯文字看起来太迷糊了,来张图看吧
总结
总结就是上图