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

一、先说结论

本文将结合我的工作实战经历,总结和提炼一种从单体架构到分布式微服务都适用的一种文件上传和校验的通用解决方案,形成一个完整的方法论。本文主要解决手段包括多线程设计模式分而治之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;}@Overridepublic 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 {@Overridepublic 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信息-uploadInfoUploadInfo 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.hqwc.cn/news/549207.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

五、初识Django

初识Django 五、初识Django1.安装django2.创建项目2.1第一种方式&#xff1a;在终端2.2第二种方式&#xff1a;Pycharm 3.创建app4.快速上手4.1再写一个页面4.2templates模板4.3静态文件4.3.1static目录4.3.2引用静态文件 5.模板语法案例&#xff1a;伪联通新闻中心6.请求和相应…

Redis中的String编码转换底层原理及6.0新特性

String编码转换底层原理 String对象为什么把大于39字节或者44字节的字符串编码为raw&#xff0c;小于的时候编码为embstr? 在Redis3.2以前的版本中,SDS作为字符串类型中存储字符串内容的结构&#xff0c;源码如下&#xff1a; 3.2版本SDS结构 struct sdshdr {// 记录buf数…

【Unity每日一记】unity中的内置宏和条件编译(Unity内置脚本符号)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

项目性能优化—使用JMeter压测SpringBoot项目

我们的压力测试架构图如下&#xff1a; 配置JMeter 在JMeter的bin目录&#xff0c;双击jmeter.bat 新建一个测试计划&#xff0c;并右键添加线程组&#xff1a; 进行配置 一共会发生4万次请求。 ctrl s保存&#xff1b; 添加http请求&#xff1a; 配置http请求&#xff1a;…

openEuler-22.03-LTS-SP2更改阿里云yum安装源

openEuler-22.03-LTS-SP2更改阿里云yum安装源 将文件/etc/yum.repos.d/openEuler.repo&#xff0c;替换为以下内容 [OS] nameOS baseurlhttps://mirrors.aliyun.com/openeuler/openEuler-22.03-LTS-SP2/OS/$basearch/ enabled1 gpgcheck1 gpgkeyhttps://mirrors.aliyun.com/op…

为 java 开发者设计的性能测试框架,用于压测+测试报告生成

拓展阅读 junit5 系列教程 基于 junit5 实现 junitperf 源码分析 Auto generate mock data for java test.(便于 Java 测试自动生成对象信息) Junit performance rely on junit5 and jdk8.(java 性能测试框架。压测测试报告生成。) junitperf junitperf 是一款为 java 开…

蓝桥杯练习02随机数生成器

随机数生成器 介绍 实际工作中随机数的使用特别多&#xff0c;比如随机抽奖、随机翻牌。通过随机数还能实现很多有趣的效果&#xff0c;比如随机改变元素的位置或颜色。 本题需要在已提供的基础项目中使用JS知识封装一个函数&#xff0c;该函数可以根据需要&#xff0c;生成指…

排序问题—java实现

冒泡排序 算法思想&#xff1a; 每次比较相邻元素&#xff0c;若逆序则交换位置&#xff0c;每一趟比较n-1次&#xff0c;确定一个最大值。故需比较n趟&#xff0c;来确定n个数的位置。 外循环来表示比较的趟数&#xff0c;每一趟确定一个最大数的位置内循环来表示相邻数字两…

服务器病毒木马通用排查处理应急响应流程

目录 一、勒索病毒发作的特征 二、勒索病毒的应急响应 三、勒索病毒预防与事后加固 一、勒索病毒发作的特征 如果发现大量统一后缀的文件&#xff1b;发现勒索信在Linux/home、/usr等目录&#xff0c;在Windows 桌面或者是被加密文件的文件夹下。如果存在以上特…

mybatis-plus 的saveBatch性能分析

Mybatis-Plus 的批量保存saveBatch 性能分析 目录 Mybatis-Plus 的批量保存saveBatch 性能分析背景批量保存的使用方案循环插入使用PreparedStatement 预编译优点&#xff1a;缺点&#xff1a; Mybatis-Plus 的saveBatchMybatis-Plus实现真正的批量插入自定义sql注入器定义通用…

Linux进程通信补充——System V通信

三、System V进程通信 ​ System V是一个单独设计的内核模块&#xff1b; ​ 这套标准的设计不符合Linux下一切皆文件的思想&#xff0c;尽管隶属于文件部分&#xff0c;但是已经是一个独立的模块&#xff0c;并且shmid与文件描述符之间的兼容性做的并不好&#xff0c;网络通…

华为汽车业务迎关键节点,长安深蓝加入HI模式,车BU预计今年扭亏

‍编辑 |HiEV 一年之前&#xff0c;同样是在电动汽车百人会的论坛上&#xff0c;余承东在外界对于华为和AITO的质疑声中&#xff0c;第一次公开阐释了华为选择走智选车模式的逻辑。 一年之后&#xff0c;伴随问界M7改款、问界M9上市&#xff0c;华为智选车模式的面貌已经发生了…