存储空间优化

前言

上一篇,我们实现了项目的部署,当部署完毕后,我们会发现云服务器的空间是有限的。本篇,我们将针对云服务器内存小、硬盘小的问题,对项目使用的存储空间进行优化,完成优化后,我们将继续开始管理员功能的开发。

内存优化

Keller 云笔记项目进行到现在,还没有到缓存和 Redis,Spring Boot、Vue.js 项目运行过程总占用的内存是比较固定的,暂时不用控制,需要注意的是 MySQL 使用的内存空间。

MySQL 索引

MySQL 占用内存的是表的索引:MySQL 在建表时如果不指定存储引擎,默认用的是 InnoDB,它的主键索引是聚集索引,其他索引是非聚集索引。

索引的数据结构使用的是 B+ 树,所有的数据均在 B+ 树的叶子节点中存储,示意图如下:

B+

  • 聚集索引:所有的数据都在 B+ 树的叶子节点存储,索引的结构即表的结构
  • 非聚集索引:所有的叶子节点存储的是指向聚集索引的指针

MySQL 设置的索引页大小为 16384,也就是 16KB,每当创建一个索引,都会将索引的根节点放入内存,也就是多了 16KB 的内存开销。

查询索引页大小:

SHOW  GLOBAL STATUS where Variable_name like 'Innodb_page_size';

查询结果:

1

查询数据表状态:

show table status from kyle_notes;

查询结果:

2

查询结果中 Index_length 字段表示该表索引大小,也就是索引占用内存的大小,结合数据表建表语句来看,索引建立越多的表,占用的内存越多。因此,在使用索引时要注意:

  • 仅在必要的字段上创建索引
    • 不必要的字段上创建索引,会浪费内存空间
  • 可以创建联合索引的情况下就不创建几个单独的索引
    • 使用联合索引比使用多个单独索引能节省内存空间
  • 不要在高重复字段上创建索引
    • 高重复字段上创建索引会降低数据表的查询效率,并且占用内存空间

单独来说,一个索引会占用 16KB 的内存,可能觉得不太多,如果在云服务器上,内存总大小可能也就 1GB,这一个个 16KB 也就显得弥足珍贵了。大家可以结合这些注意事项,来审视当前已经创建的表中,在创建索引时存在哪些问题。

磁盘空间优化

磁盘空间占用比较大的就是文件服务器,存储的是用户的头像和笔记中图片。拿笔者使用的阿里云服务器来说,磁盘空间共有 20GB,如果不断更换头像或者上传笔记中的图片,不出半天,也就将磁盘空间打爆了,因此需要对冗余的用户头像和笔记图片进行删除。

用户头像

对用户头像的优化的目标是:每个用户只会在文件服务器上存储一个头像,当上传新头像时会覆盖掉原有的头像。

为达到这个目标,对创建头像和更新头像的操作进行如优化:

  • 创建头像
    • 单独创建个文件夹存放所有用户的头像
    • 每个用户根据用户 ID 生成一个固定的头像文件名
    • 头像图片必须是固定格式的(如: PNG),如果用户上传的头像是其他格式,则将其转换为指定格式的图片
  • 更新头像
    • 先根据用户 ID 生成固定的文件名
    • 判断用户的头像文件是否已经存在
    • 如果存在,删除原头像,创建新头像
    • 如果不存在,创建头像

这些操作均在服务端做改动,不涉及到 Web 端的修改,修改如下。

在 FileUtils.java 中添加方法 getPortraitName,用来生成固定的头像名称:

        /**
     * 获取头像文件名
     * @param userId
     * @return 文件名,不带后缀
     */
    public static String getPortraitName(int userId){
        return Md5Utils.getMd5String(PublicConstant.PORTRAIT_PREFIX + userId) ;
    }

该方法会通过使用固定前缀加用户 ID 的方式生成统一的头像文件名,同时,使用 MD5 加密,隐藏文件名生成规则。

在 FileUtils.java 中添加方法 getPortraitPath,用来获取头像的保存路径:

/**
     * 获取头像保存路径
     * @param fileName 文件名,不带后缀
     * @return  完整的头像保存路径
     */
    public static String getPortraitPath(String fileName){
        StringBuilder builder = new StringBuilder();
        builder.append(PublicConstant.nginxPath)
                .append(PublicConstant.portraitPath)
                .append(PublicConstant.originImgPath);

        if(mkdirs(builder.toString())){
            builder.append(fileName)
                    .append(".")
                    .append(PublicConstant.PORTRAIT_TYPE);
            return builder.toString();
        }else {
            return null;
        }
    }

该方法会构建一个图片保存的完整路径,同时指定头像使用固定的后缀名(PublicConstant.PORTRAIT_TYPE)。

新建 ImageUtils.java 处理图片的存储,添加方法 savePortrait,用于保存用户头像:

/**
     * 保存用户头像
     * 用户头像保存为固定的文件名,设置新的头像时,会覆盖掉原来的头像
     * @param file
     * @param userId
     * @return
     * @throws IOException
     */
    public static String savePortrait(MultipartFile file, int userId) throws IOException{
        //解析文件后缀名
        String suffix = FileUtils.getSuffix(file.getOriginalFilename());
        String fileName = FileUtils.getPortraitName(userId);

        //保存原图
        File img = new File(FileUtils.getPortraitPath(fileName));
        if(img.exists()){
            img.delete();
        }
        //保存缩略图
        File thum = new File(FileUtils.getPortraitThumPath(fileName));
        if(thum.exists()){
            thum.delete();
        }
        //如果图片是 PNG 格式的,直接存储
        if(PublicConstant.PORTRAIT_TYPE.equals(suffix)){
            file.transferTo(img);
        }else{
            /*
             将其他格式的图片转换成 PNG 格式
              */
            Thumbnails.of(file.getInputStream()).scale(1).outputFormat(PublicConstant.PORTRAIT_TYPE).toFile(img);
        }
        //生成缩略图
        Thumbnails.of(img).size(PublicConstant.THUM_MAX_WIDTH,PublicConstant.THUM_MAX_HEIGHT).toFile(thum);
        return fileName;
    }

该方法确保用户在文件服务器中只保存一个头像,并且使用固定的文件名称和后缀名。

笔记中的图片

对笔记图片的优化目标是:用户删除笔记时,从文件服务器中删除笔记中的全部图片;用户在编辑器中删除图片时,同步删除文件服务器中的图片。

为了达到这个目标,对相关操作做如下优化:

  • 上传笔记中的图片
    • 保存图片时,按照每个用户一个文件夹保存
    • 图片名称加上笔记 ID 作为前缀,以便于确定所属的笔记
  • 删除笔记
    • 删除笔记时,遍历用户的文件夹,删除属于该笔记的所有图片
  • 删除笔记中的图片
    • 删除笔记中的图片时,根据图片名称,直接从文件服务器中删除掉该图片
    • 难点在于:如何知道用户在编辑器中删除了图片
      • mavon-editor 提供了删除图片的回调方法 imgDel,但只能在菜单栏上传图片按钮中触发,在编辑器中删除图片无法触发任何操作
      • WangEditor 没有提供删除图片相关的回调
      • 解决方法是在 Web 端出发保存笔记的操作时,通过比较修改前后的内容,判断出删除的图片

movan-editor 中可以出发 imgDel 回调的操作:

3

主要方法有:

在 FileUtils.java 中添加方法 getUserImgDirectory,用于获取用户文件目录。

    /**
     * 获取用户文件目录
     * @param userId
     * @return
     */
    private static String getUserImgDirectory(int userId){
        StringBuilder builder = new StringBuilder();
        builder.append(PublicConstant.nginxPath)
                .append(PublicConstant.imgPath)
                .append(userId)
                .append("/");
        if(mkdirs(builder.toString())){
            return builder.toString();
        }else{
            return null;
        }
    }

在 FileUtils.java 中添加方法 getImgPath,用于获取图片保存路径:

        /**
     * 获取图片保存路径
     * @param fileName 文件名
     * @return 完整的保存路径
     */
    public static String getImgPath(String fileName,int userId){
        String directory = getUserImgDirectory(userId);
        if(directory.isEmpty()){
            return null;
        }else {
            return directory + fileName;
        }
    }

在 ImgUtils.java 中添加方法 saveImg,用于保存图片:

/**
     * 保存图片
     * @param file  文件
     * @param noteId    笔记ID
     * @param userId    用户ID
     * @return
     * @throws IOException
     */
    public static String saveImg (MultipartFile file,int noteId,int userId) throws IOException {
        //解析文件后缀名
        String suffix = FileUtils.getSuffixWithSpilt(file.getOriginalFilename());

        String timeMask = DateUtils.getTimeMask();
        //构建图片名称
        String fileName = noteId +  "-" + timeMask + suffix;
        //保存图片
        File img = new File(FileUtils.getImgPath(fileName,userId));
        file.transferTo(img);
        return fileName;
    }

在 FileUtils.java 中添加方法 getUserImgs,用于获取用户的所有图片名称:

   /**
     * 获取用户存储的所有图片名称
     * @param userId
     * @return
     */
    private static String[] getUserImgs (int userId){
        File file = new File(getUserImgDirectory(userId));
        if(file.exists() && file.isDirectory()){
            return file.list();
        }else{
            return null;
        }
    }

在 FileUtils.java 中添加方法 deleteImgByNoteId,用于删除笔记中的所有图片:

       /**
     * 删除笔记中的图片
     * @param userId
     * @param noteId
     */
    public static void deleteImgByNoteId(int userId,int noteId){
        String noteIdStr = noteId + "";
        String[] imgs = getUserImgs(userId);
        if(imgs == null || imgs.length < 1){
            return;
        }
        for(String imgName :imgs){
            String[] name = imgName.split("-");
            if(name.length < 2){
                continue;
            }
            if(noteIdStr.equals(name[0])){
                deleteImg(imgName,userId);
            }
        }
    }
判断在编辑器中删除了图片

在 WangEditor 中和 mavon-editor 判断图片被删除的思路是一样的:

  1. 使用正则表达式匹配编辑器内容中的图片
  2. 分别取出修改前和修改后内容中包含的图片
  3. 如果有些图片在修改前有,修改后没有,说明图片被删除了

下面以判断在 mavon-editor 中删除图片的操作为例,说明实现过程。

data.js 中添加方法 getMarkDownImg,用以取出内容中包含的图片名称:

/**
     * 从 MarkDown 格式的内容中读取出图片名称(不带路径)
     * @param {Object} content
     */
    getMarkDownImg(content) {
        // 匹配 MarkDown 中的图片 匹配格式:![图片描述](图片地址)
        var pattern = /!\[?([^\)]*)\)?/g;
    // 获取匹配到的图片数组
        var arr = content.match(pattern);
        let imgs = []
    // 遍历图片数组,解析出图片的文件名,如:abc.jpg
        for (let index in arr) {
            let img = arr[index];
            img = img.substring(img.lastIndexOf("/") + 1, img.length - 1);
            imgs.push(img);
        }
    //返回纯图片名的数组
        return imgs;
    }

说明:

因为 Keller 云笔记项目中每个用户存储图片的目录是固定的,在判断图片时,取出图片文件名即可。如果放的是网络图片,要在这里做相应的处理(如:判断图片命名格式、图片访问的域名等),以区分网络图片和项目文件服务器中存储的图片。

data.js 中添加方法 getMarkDownDelImgs,用于判断出删除的图片:

/**
     * 获取到 MarkDown 编辑器中删除的图片
     * @param {Object} oldContent   修改前的内容
     * @param {Object} newContent   修改后的内容
     */
    getMarkDownDelImgs(oldContent, newContent) {
        let oldImgs = this.getMarkDownImg(oldContent);
        //如果修改前没有图片,不用判断
        if (oldImgs.length < 1) {
            return [];
        }
        let newImgs = this.getMarkDownImg(newContent);
        //如果修改后没有图片,说明图片全被删除
        if (newImgs.length < 1) {
            return oldImgs;
        }
        let del;
        let delImgs = [];
        for (let i in oldImgs) {
            del = false;
            for (let j in newImgs) {
                //如果修改前的图片,修改后仍存在,说明未删除
                if (oldImgs[i] === newImgs[j]) {
                    del = false;
                    break;
                } else {
                    del = true;
                }
            }
            if (del) {
                delImgs.push(oldImgs[i]);
            }
        }
    // 返回删除的图片
        return delImgs;
    }

Node.vue 的 handleSave 方法中添加删除图片的操作:

handleSave() {
                let newText;
                let newHtml;
                if (this.note.type == 0) {
                    newText = this.$refs.wangEditor.getText();
                    newHtml = this.$refs.wangEditor.getHtml();
                } else if (this.note.type == 1) {
                    newText = this.$refs.markDown.getText();
                    newHtml = this.$refs.markDown.getHtml();

                    //如果 MarkDown 笔记内容有变化,检查是否删除了图片,如果删除了图片,则请求服务器删除图片
                    if(this.note.text !== newText){
                        let delImgs = editorImgCheck.getMarkDownDelImgs(this.note.text,newText);
                        if(delImgs.length > 0){
                            for(let index in delImgs){
                // 调用删除图片的接口,删除文件服务器中的图片
                                this.handleDelImg(delImgs[index]);
                            }
                        }
                    }
                } else {
                    return;
                }
     ... ... 
}

源码地址

本篇完整的源码地址:

https://github.com/tianlanlandelan/KellerNotes

小结

本篇根据项目部署的实际情况,带领大家在力所能及的范围内控制项目消耗的内存、磁盘空间资源,避免因为大量冗余的数据浪费服务器存储空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值