从单机到分布式微服务,大文件校验上传的通用解决方案

news2024/11/14 19:35:12

一、先说结论

本文将结合我的工作实战经历,总结和提炼一种从单体架构到分布式微服务都适用的一种文件上传和校验的通用解决方案,形成一个完整的方法论。本文主要解决手段包括多线程设计模式分而治之MapReduce等,虽然文中使用的编程语言为Java,但解决问题和优化思路是互通的,适合有一定开发经验的开发者阅读,希望对大家有帮助。

二、引言

文件上传的场景应该都不陌生,不管是C端还是B端,都会有文件上传的场景。用户在平台页面点击上传文件,用户请求在最后会到达后端服务器,后端服务器会对上传的文件进行各种校验,比如文件名称校验、文件大小校验、文件内容校验等,其中业务逻辑最复杂、技术上有挑战性的当属文件内容校验了。为什么这么说呢?接着看。

三、背景

文件校验和上传,看似是一件很简单的工作,要做好,可能也并非一件容易得事情。我以一个电商后台系统为例,上传csv格式的sku信息文档将会面临下面几方面挑战:

  1. 上传sku数量多:上传文件中sku数量不定,从个位数到百万级不等;为了好的用户体验,需要在较短的时间内上传校验完成并返回结果;

  2. 业务逻辑复杂:文件上传校验需要校验每条内容,校验规则多且复杂,校验规则包括录入的sku格式是否符合,如不符合需要给出提示语1;校验上传的sku是否合法有效,如果需要给出相应的提示语2;校验该操作人是否有该sku管理权限,如果没有给出相应的提示语3……每个校验逻辑中可能还包含许多分支、循环逻辑……

  3. 外部依赖RPC多:上传校验过程中涉及多个外部依赖RPC的调用,比如sku的管理权限校验,需要调用用户中台RPC接口获取上传人的基本信息;校验sku是否是本次活动范围,需要调用直播中台RPC接口……

四、关键问题拆解和解决思路

  1. 上传数量多且要求体验友好,就要求要注意高性能方面的优化:对于业务服务器来说,如果是单机性能优化,需要考虑使用多线程技术来充分发挥服务器性能;如果是分布式的服务,在优化单机性能无法业务场景需要的时候,还可以考虑依靠中间件来协同不同服务器,发挥集群优势。

  2. 业务逻辑复杂,就要求写出来的代码有较高的可阅读性、可维护性,不要成为“大泥球”:除了在系统架构方面的优化之外,对于开发人员,可以考虑使用设计模式来提高代码质量。

  3. 外部RPC依赖多,网络数据IO操作,接口性能可能无法保证,就需要使用异步调用的方式来保证性能;

五、系统架构

假设有这么一个电商活动管理系统,从架构上来说,可以分为服务层、业务层、数据层和外部依赖,架构图如下:

  • 服务层:包括对外服务和外部调用;

  • 业务层:活动的生命周期,包括创建、查看、修改、关闭流程;

  • 数据层:数据存储,主要是数据库集群和缓存集群;

  • 外部依赖:外部依赖的RPC服务,包括商品RPC服务等;

在技术实现方面,该系统是前后端分离的系统,前后端通过域名进行交互。前端服务主要提供操作页面,用户可以在页面端进行各种操作,例如创建活动、查看活动、修改活动、关闭活动等;

后端采用的是微服务架构,按照功能拆分为提供HTTP接口的soa应用、提供MQ消费功能的MQ应用、提供RPC服务的RPC应用,存储使用的是MySQL和Redis集群,大概架构图如下:

六、Java多线程实践

6.1 使用Java多线程优化单机性能

分析上面的场景,明显是IO密集型的场景。IO 密集型指的是大部分时间都在执行 IO 操作,主要包括网络 IO 和磁盘 IO,以及与计算机连接的一些外围设备的访问。在上面场景中,校验过程中需要调用大量RPC接口,大部分时间调用都在等待网络IO,所以可以使用异步和多线程的设计方法来提升网络IO性能,从而优化整体性能。

关于Java多线程在这里不赘述了,直接看关键代码实现吧:

    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    @ResponseBody
    @RequestMapping(value = "uploadSku", method = RequestMethod.POST)
    public Result uploadSku(@RequestParam(value = "file", required = false) MultipartFile file) throws IOException {
        Result result = new Result();
        result.setSuccess(true);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream()));

        try {
            // 校验文件名称
            result = checkFileNameFormat(file);
            if (!result.isSuccess()) {
                return result;
            }

            // 校验文件内容格式并填充校验任务
            List<UploadResInfo> uploadResInfos = new ArrayList<>();
            List<SkuCheckTask> tasks = checkFileContentAndFillSkuCheckTask(result, bufferedReader, uploadResInfos);

            // 执行校验任务
            result = dealSkuSkuCheckTask(tasks, uploadResInfos);

        } catch (Exception e) {
            result.setSuccess(false);
            result.setErrorMessage("上传文件异常!");
        }
        return result;
    }
    
        /**
     * @param tasks
     * @param uploadResInfos
     * @return
     */
    private Result dealSkuSkuCheckTask(List<SkuCheckTask> tasks, List<UploadResInfo> uploadResInfos) throws Exception {
        Result result = new Result();
        result.setSuccess(true);
        List<Long> passedSkus = new ArrayList<>();
        if (!CollectionUtils.isEmpty(tasks)) {
            List<Future<Result>> futureList = executorService.invokeAll(tasks);
            for (Future<Result> tempResult : futureList) {
                if (tempResult.get().isSuccess()) {
                    Result tempRes = tempResult.get();
                    if (null != tempRes.getResult().get("uploadResInfos")) {
                        uploadResInfos.addAll((List<UploadResInfo>) tempRes.getResult().get("uploadResInfos"));
                    }
                    passedSkus.addAll((List<Long>) tempRes.getObject());
                }
            }
        }
        result.addDefaultModel("passedSkus", passedSkus);
        if (passedSkus.size() == 0) {
            result.setErrorMessage("上传都不通过");
        }
        return result;
    }

public class SkuCheckTask implements Callable<Result> {

    private List<Long> skuList;

    public SkuCheckTask(List<Long> skuList) {
        this.skuList = skuList;
    }

    @Override
    public Result call() throws Exception {
        Result result = new Result();
        result.setSuccess(true);
        List<Long> passedSkuList = new ArrayList<>();
        List<UploadResInfo> uploadResInfos = new ArrayList<>();

        for (int i = 0; i < skuList.size(); i++) {
            if (checkSku(skuList.get(i))) {
                passedSkuList.add(skuList.get(i));
            } else {
                UploadResInfo uploadResInfo = new UploadResInfo(skuList.get(i).toString(), false, "RPC校验失败");
                uploadResInfos.add(uploadResInfo);
            }
        }
        result.setObject(passedSkuList);
        result.addDefaultModel("uploadResInfos", uploadResInfos);
        return result;
    }

    /**
     * 校验sku,复杂校验逻辑
     *
     * @param sku
     * @return
     */
    private boolean checkSku(Long sku) {
        // 复杂校验逻辑,例如多个RPC调用等耗时操作
        System.out.println("校验sku:" + sku);
        return true;
    }
}

6.2 线程数的设置

我们知道,调整线程池中的线程数量的主要是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。

对于CPU密集型任务(比如加解密、压缩和解压、计算),最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。因为CPU密集型任务本来就会占用大量的CPU资源,CPU 的每个核心工作基本都是满负荷的,而如果设置了过多的线程,每个线程都要去争取CPU资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多反而会导致性能下降。

对于IO密集型任务(比如数据库读写、文件读写、网络通信等),这种任务并不会太消耗CPU资源,反而是在等待IO操作。线程数设置可以参考以下公式:

线程数 = CPU核心数 * (1 + 平均等待时间/平均工作时间)

在本程序中,使用了线程池:FixedThreadPool,并将线程数设置为10。这里的考虑是容器为16C32G的配置,除了上传任务,服务端还会处理其他的任务,还有其他的线程池,为了综合考虑,这里只是分配了10个线程数。当然,最佳实践是使用远程配置中心动态调整线程池线程数,实现动态线程池,在实践中进行调整和压测,最终找到合适的线程数配置。

七、责任链模式实践

对于上述这个校验逻辑,最常见的处理方式是使用 if…else…条件判断语句来处理,这样处理可能存在这样的问题:

  1. 代码复杂度高:该场景中的判定条件通常不是简单的判断,需要调用外部RPC接口查询数据,从结果中解析到需要的字段,才能进行逻辑判断。这样代码的嵌套层数就会很多,代码复杂度就会很高,不用太久,这段代码将发展成为“大泥球”。

  2. 代码耦合度高:如果业务需求新增校验逻辑,那么就要继续添加 if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。

那么面对上面这种场景,如何实现更优雅呢?。其实这里也很简单,就是把判定条件的部分放到处理类中,这就是责任链模式。如果满足条件 1,则由 Handler1 来处理,不满足则向下传递;如果满足条件 2,则由 Handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。部分代码如下:

Handler接口:

public interface SkuCheckHandler {
    BaseResult doHandler(UploadInfo uploadInfo);
}

SkuCheckHandler接口实现Handler1:

public class Handler1 implements SkuCheckHandler {
    @Override
    public BaseResult doHandler(UploadInfo uploadInfo) {
        // 调用用户中台校验权限
        return new BaseResult();
    }
}

遍历Handler进行校验,如果Handler校验不通过直接返回校验结果,校验通过则继续进入下一个Handler进行校验:

public class SkuCheckHandlerChain {

    private List<SkuCheckHandler> handlers = new ArrayList<>();

    public void addHandler(SkuCheckHandler skuCheckHandler) {
        this.handlers.add(skuCheckHandler);
    }

    public BaseResult handle(UploadInfo uploadInfo){
        BaseResult baseResult = new BaseResult();
        baseResult.setSuccess(true);
        for (SkuCheckHandler handler : handlers) {
            baseResult = handler.doHandler(uploadInfo);
            if (!baseResult.isSuccess()) {
                return baseResult;
            }
        }
        return baseResult;
    }

}

责任链设置和调用:

    private boolean checkSku(Long sku) {
        // 复杂校验逻辑,例如多个RPC调用等耗时操作
        System.out.println("校验sku:" + sku);
        // 后续校验都依赖商品信息,所以需要调商品RPC获取Sku信息-uploadInfo
        UploadInfo uploadInfo = new UploadInfo();
        SkuCheckHandlerChain handlerChain = new SkuCheckHandlerChain();
        handlerChain.addHandler(new Handler1());
        handlerChain.addHandler(new Handler2());
        BaseResult baseResult = handlerChain.handle(uploadInfo);
        return baseResult.isSuccess();
    }

八、分布式文件上传最佳实践

8.1 MapReduce简介

当使用了多线程技术,并优化了线程数,似乎单机性能已经达到了极限。但是如果此时仍然不能满足业务场景需要,那又该怎么优化呢?

有人可能会想到垂直扩容,升级更高配的机器来提升性能。这个办法当然是可行的,也是最简单粗暴的方式,唯一的缺点就是“费钱”,土豪请随意。一般来说,Google的方式可能更加值得借鉴,Google使用“3M胶带粘在一起的服务器”打败了成本更高的高配计算机。

在面对海量数据背景下,Google科学家杰夫·迪恩提出了MapReduce技术。MapReduce其实并不复杂,使用的正是分而治之(Divide and Conquer)的思想。打个不太恰当的比方就是,老板分作业,小兵完成作业,老板进行汇总

MapReduce其实也是自顶向下的递归。MapReduce先在最顶层将一个复杂的大任务分解成为成百上千个小任务;然后将每个小任务分配到一个服务器上去求解;最后再将每个服务器上面的结果综合起来,得到原来大任务的最终结果。第一个自顶向下分解的过程称为Map,第二个自底向上合并的过程称为Reduce

其核心原理其实可以看这张图,图片出自论文《MapReduce: Simplified Data Processing on Large Clusters》。

8.2 MapReduce在文件上传场景的应用

单机服务器性能无法满足,应该考虑合理利用多台机器,不同微服务之间相互协作,共同完成上传的任务。借鉴MapReduce核心思想,可以使用现有系统架构,实现大文件的分布式上传和校验。

一图胜前言,方案说明都在图片中了,详细请看:

九、踩坑和代码调试

9.1 踩坑1:MQ消费中使用LoginContext获取用户信息异常

其中有个踩坑点需要注意,在soa应用中常用的LoginContext获取用户信息;在MQ应用中,使用LoginContext将无法获取到用户信息,如果使用将会出现空指针异常;出现异常之后,MQ消费将会进行重试,重试也一直会发生异常,从而死循环,无法得到正确的结果。

9.2 代码调试-Idea远程Debug

在开发工作中,代码写完并不是万事大吉了。部署到服务器测试过程中,可能还会发现各种各样意料之外的错误。当服务器日志打印过多或者过少都影响问题排查的效率,以文件上传场景为例,如果不打印完整的出入参,出现问题没有日志可以用来排查问题;如果每个方法都打印完整的出入参日志,当上传文件中sku数量较多,可以想象下如果有100w条的sku信息,从这么多的日志中去排查问题无异于“大海捞针”。

那这个问题无解了吗?当然不是,远程Debug可以提升排查效率,同事妹子看见了都直呼YYDS。其实这个工具就是我们几乎人人都在用的Idea,Idea自带了远程调试工具。下面是我的使用经验,适用于部署在Tomcat容器工程代码:

9.2.1 环境配置

  1. 远程Tomcat配置

远程Tomcat添加启动参数并重启生效:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

  1. Idea配置

话不多说,图上都有:

  1. 启动调试

9.2.2 常见问题

  1. 为什么调试断点没生效?

本地和远程代码要相同,不一样则会出现无法进入断点的情况;如果代码一致还是无法进入,尝试重启,一般可以解决;

  1. 进入断点调试之后,服务器还可以处理其他请求吗?

服务器在断点处停住了,无法处理其他请求;

  1. 改了本地代码可以直接debug吗?

不可以,需要部署在远程服务器之后再次启动debug;

通用解决方案总结

通过上述过程之后,总结出一套通用的大文件上传和校验的解决方案。总结一下就是,如果现在技术架构还处在单机架构的阶段,可以考虑使用多线程技术优化单机性能;为了使代码优雅一点,可以考虑使用责任链模式;如果现在技术架构已经发展到分布式和微服务了,可以借鉴分而治之的思想,让多服务器协作工作,发挥多服务器的优势。

如果用三个词总结,那就是:多线程、责任链模式、分而治之和MapReduce

文章转载自:James_Shangguan

原文链接:https://www.cnblogs.com/sgh1023/p/18079575

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1527181.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在Visual Studio中调试 .NET源代码

前言 在我们日常开发过程中常常会使用到很多其他封装好的第三方类库&#xff08;NuGet依赖项&#xff09;或者是.NET框架中自带的库。如果可以设置断点并在NuGet依赖项或框架本身上使用调试器的所有功能&#xff0c;那么我们的源码调试体验和生产效率会得到大大的提升。今天我…

openKylin系统安装ssh服务结合内网穿透实现固定公网地址访问

文章目录 1. 安装SSH服务2. 本地SSH连接测试3. openKylin安装Cpolar4. 配置 SSH公网地址5. 公网远程SSH连接小结 6. 固定SSH公网地址7. SSH固定地址连接 openKylin是中国首个基于Linux 的桌面操作系统开发者平台&#xff0c;通过开放操作系统源代码的方式&#xff0c;打造具有自…

Selenium 自动化 —— Selenium IDE录制、回放、导出Java源码

Hello Selenium 示例 之前我们在专栏的第一篇文章中演示了使用使用Selenium进行百度搜索的Hello world示例。 代码不复杂非常简单&#xff1a; public static void main(String[] args) {WebDriver driver null;try {// 设置Chrome驱动的路径 // System.setPro…

matplotlib库简介及函数说明

目录 简介matplotlib.pyplot as plt 常用函数说明创建子图plt.subplots&#xff08;&#xff09;.plot&#xff08;&#xff09; 子图参数set_title&#xff08;&#xff09;axis2.legend()fig.autofmt_xdate() 简介 matplotlib 是一个用于创建二维图表和数据可视化的 Python …

【JavaWeb】Spring非阻塞通信 - Spring Reactive之WebFlux的使用

【JavaWeb】Spring非阻塞通信 - Spring Reactive之WebFlux的使用 文章目录 【JavaWeb】Spring非阻塞通信 - Spring Reactive之WebFlux的使用参考资料一、初识WebFlux1、什么是函数式编程1&#xff09;面向对象编程思维 VS 函数式编程思维&#xff08;封装、继承和多态描述事物间…

【视频异常检测】Delving into CLIP latent space for Video Anomaly Recognition 论文阅读

Delving into CLIP latent space for Video Anomaly Recognition 论文阅读 ABSTRACT1. Introduction2. Related Works3. Proposed approach3.1. Selector model3.2. Temporal Model3.3. Predictions Aggregation3.4. Training 4. Experiments4.1. Experiment Setup4.2. Evaluat…

Linux设置IP地址多种方法保姆级教程,外加修改主机名并通过生成密钥,组建机群。

[引入提问] 怎么设置linux服务器的名称&#xff1f; 方法1&#xff1a; nmtui—自动打开设置机器名称的操作界面---输入即可。重启hostnamed服务&#xff0c; systemctl restart system-hostnamed 方法2&#xff1a; 直接编辑 vim /etc/hostname &#xff0c;输入新的…

VR历史建筑漫游介绍|虚拟现实体验店加盟|VR设备购买

VR历史建筑漫游是一种利用虚拟现实技术&#xff0c;让用户可以身临其境地参观和探索历史建筑的体验。通过VR头显和相关设备&#xff0c;用户可以在虚拟环境中自由移动和互动&#xff0c;感受历史建筑的真实氛围和文化内涵。 在VR历史建筑漫游中&#xff0c;您可以选择不同的历史…

Vue+SpringBoot打造数据可视化的智慧河南大屏

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 数据模块 A4.2 数据模块 B4.3 数据模块 C4.4 数据模块 D4.5 数据模块 E 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的数据可视化的智慧河南大屏&#xff0c;包含了GDP、…

SpringCloud搭建微服务之Micrometer分布式链路追踪

1. 概述 由于Spring Cloud Sleuth最新版本只支持Spring Boot 2.7.x&#xff0c;核心项目已经迁移到Micrometer Traceing项目&#xff0c;Spring Boot 3.x版本要实现分布式链路追踪需要集成Micrometer。更多详情可以参阅Micrometer官网 本文将以Spring Boot 3.2.x和Spring Clo…

视频私有云,HDMI/AV多硬件设备终端接入,SFU/MCU视频会议交互方案。

在视频业务深入的过程中越来越多的硬件设备接入视频交互的视频会议中远程交互&#xff0c;有的是视频采集&#xff0c;有的是医疗影像等资料&#xff0c;都需要在终端承显&#xff0c;这就需要我们的设备终端能多设备&#xff0c;多协议接入&#xff0c;设备接入如下。 1&#…

Python Web开发记录 Day13:Django part7 Ajax入门与案例(任务管理)

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 1、Ajax入门①简介②工作原理③优点④缺点⑤使用…

Poly Kernel Inception Network在遥感检测中的应用

摘要 https://export.arxiv.org/pdf/2403.06258 遥感图像&#xff08;RSI&#xff09;中的目标检测经常面临一些日益严重的挑战&#xff0c;包括目标尺度的巨大变化和多样的上下文环境。先前的方法试图通过扩大骨干网络的空间感受野来解决这些挑战&#xff0c;要么通过大核卷积…

易基因:人类大脑的单细胞DNA甲基化和3D基因组结构|Science

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 高通通量表观基因组分析技术可用于阐明大脑中细胞复杂性的基因调控程序。5-甲基胞嘧啶 (5mCs)是哺乳动物基因组中最常见的修饰碱基&#xff0c;大多数5mCs发生在胞嘧啶-鸟嘌呤二核苷酸&a…

Spring Boot 实现程序的优雅退出

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 目录 前言 优雅停机是什么 SpringBoot如何实现优雅停机 合理杀死进程 法一&…

企业开展开源安全治理必要性及可行性详细分析

背景 开源软件安全威胁是近几年企业安全面临的主要威胁&#xff0c;也是企业应用安全方向讨论的热门话题&#xff0c;但是由于是新的需求新的方向&#xff0c;很多企业在观望&#xff0c;当前开展这项工作是否已经成熟&#xff0c;项目成功率如何&#xff1f; 当新鲜事物产生时…

【iOS】——Blocks

文章目录 前言一、Blocks概要1.什么是Blocks 二、Block模式1.block语法2.block类型变量3.截获自动变量值4._Block修饰符5.截获的自动变量 三、Blocks的实现1.Block的实质2.截获自动变量值3._Block说明符4.Block存储域 前言 一、Blocks概要 1.什么是Blocks Blocks是C语言的扩…

聚类分析 | Matlab实现基于NNMF+DBO+K-Medoids的数据聚类可视化

聚类分析 | Matlab实现基于NNMFDBOK-Medoids的数据聚类可视化 目录 聚类分析 | Matlab实现基于NNMFDBOK-Medoids的数据聚类可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 NNMFDBOK-Medoids聚类&#xff0c;蜣螂优化算法DBO优化K-Medoids 非负矩阵分解&#xff08…

mac清除dns缓存指令 mac清除缓存怎么清理

你是否曾经被要求清理dns缓存并刷新&#xff1f;清理dns缓存一般是由于修改了主机文件&#xff0c;或者想排除一些网络上的故障。在Mac上清除dns缓存需要使用命令行来实现。在本文中&#xff0c;软妹子将向大家介绍mac清除dns缓存指令&#xff0c;并展示mac清除缓存怎么清理。 …

Stable Diffusion WebUI 生成参数:宽度/高度/生成批次/每批数量/提示词相关性/随机种子

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 大家好&#xff0c;我是水滴~~ 本文将继续了解 Stable Diffusion WebUI 的生成参数&#xff0c;主要内容有&#xff1a;宽度、高度、生成批次、每批数量、提示词相关性、随机种子。希望能对你…