多人同时导出 Excel 干崩服务器!新来的阿里大佬给出的解决方案太优雅了! ...

打印 上一主题 下一主题

主题 844|帖子 844|积分 2532

来源:juejin.cn/post/7259249904777838629
前言

业务诉求:思量到数据库数据日渐增多,导出会有全量数据的导出,多人同时导出可以会对服务性能造成影响,导出涉及到mysql查询的io操作,还涉及文件输入、输出流的io操作,所以对服务器的性能会影响的比较大;结合以上原因,对导出操作进行列队;
刚开始拿到这个需求,第一时间想到就是必要维护一个FIFO先进先出的队列,给定队列一个固定size,在队列内里的人进行列队进行数据导出,导出完成后立马出队列,下一个列队的人进行操作;还思量到异步,大概还必要建个文件导出表,紧张记录文件的导出环境,文件的存放地址,用户根据文件列表环境下载导出文件。
业务关系定义

分别是用户、导出队列、导出实验方法:

  • ExportQueue: 维护一条定长队列,可以获取队列里前后列队的用户,提供查询,队列假如已经满了,别的的人必要进行等待
  • User信息: 列队实验导出方法对应用户;
  • Export类: 定义导出方法,异步实验,用户可以通过导出页面查看、下载,导出的文件;

详细代码实现

推荐一个开源免费的 Spring Boot 实战项目:
https://github.com/javastacks/spring-boot-best-practice
ExportQueue队列:
  1. package com.example.system.config;
  2. import com.example.system.api.domain.ExportUser;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.stereotype.Component;
  5. import java.util.LinkedList;
  6. @Slf4j
  7. @Component
  8. public class ExportQueue {
  9.     private final int MAX_CAPACITY = 10; // 队列最大容量
  10.     private LinkedList<ExportUser> queue; // 用户队列
  11.     public ExportQueue(LinkedList<ExportUser> queue) {
  12.         this.queue = new LinkedList<>();
  13.     }
  14.     /**
  15.      * 排队队列添加
  16.      * @param sysUser
  17.      */
  18.     public synchronized LinkedList<ExportUser> add(ExportUser sysUser) {
  19.         while (queue.size() >= MAX_CAPACITY) {
  20.             try {
  21.                 log.info("当前排队人已满,请等待");
  22.                 wait();
  23.             } catch (InterruptedException e) {
  24.                 e.getMessage();
  25.             }
  26.         }
  27.         queue.add(sysUser);
  28.         log.info("目前导出队列排队人数:" + queue.size());
  29.         notifyAll();
  30.         return queue;
  31.     }
  32.     /**
  33.      * 获取排队队列下一个人
  34.      * @return
  35.      */
  36.     public synchronized ExportUser getNextSysUser() {
  37.         while (queue.isEmpty()) {
  38.             try {
  39.                 wait();
  40.             } catch (InterruptedException e) {
  41.                 e.printStackTrace();
  42.             }
  43.         }
  44.         ExportUser sysUser = queue.remove();
  45.         notifyAll(); //唤醒
  46.         return sysUser;
  47.     }
  48. }
复制代码
AbstractExport导出类

引入EasyExcel百万级别的导出功能
  1. package com.example.system.config;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import cn.hutool.core.util.PageUtil;
  4. import com.alibaba.excel.EasyExcel;
  5. import com.alibaba.excel.ExcelWriter;
  6. import com.alibaba.excel.write.metadata.WriteSheet;
  7. import com.example.system.api.domain.ExportUser;
  8. import lombok.extern.slf4j.Slf4j;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.IOException;
  11. import java.net.URLEncoder;
  12. import java.util.List;
  13. @Slf4j
  14. public abstract class AbstractExport<T, K> {
  15.     public abstract void export(ExportUser sysUser) throws InterruptedException;
  16.     /**
  17.      * 导出
  18.      *
  19.      * @param response 输出流
  20.      * @param pageSize 每页大小
  21.      * @param t        导出条件
  22.      * @param k        Excel内容实体类
  23.      * @param fileName 文件名称
  24.      */
  25.     public void export(HttpServletResponse response, int pageSize, T t, Class<K> k, String fileName) throws Exception {
  26.         ExcelWriter writer = null;
  27.         try {
  28.             writer = getExcelWriter(response, fileName);
  29.             //查询导出总条数
  30.             int total = this.countExport(t);
  31.             //页数
  32.             int loopCount = PageUtil.totalPage(total, pageSize);
  33.             BeanUtil.setProperty(t, "pageSize", pageSize);
  34.             for (int i = 0; i < loopCount; i++) {
  35.                 //开始页
  36.                 BeanUtil.setProperty(t, "pageNum", PageUtil.getStart(i + 1, pageSize));
  37.                 //获取Excel导出信息
  38.                 List<K> kList = this.getExportDetail(t);
  39.                 WriteSheet writeSheet = EasyExcel.writerSheet(fileName).head(k).build();
  40.                 writer.write(kList, writeSheet);
  41.             }
  42.         } catch (Exception e) {
  43.             String msg = "导出" + fileName + "异常";
  44.             log.error(msg, e);
  45.             throw new Exception(msg + e);
  46.         } finally {
  47.             if (writer != null) {
  48.                 writer.finish();
  49.             }
  50.         }
  51.     }
  52.     public com.alibaba.excel.ExcelWriter getExcelWriter(HttpServletResponse response, String fileName) throws IOException {
  53.         response.setContentType("application/vnd.ms-excel");
  54.         response.setCharacterEncoding("utf-8");
  55.         // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
  56.         String fileNameUtf = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
  57.         response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileNameUtf + ".xlsx");
  58.         return EasyExcel.write(response.getOutputStream()).build();
  59.     }
  60.     /**
  61.      * (模版导出)
  62.      *
  63.      * @param t
  64.      * @param fileName
  65.      * @param response
  66.      */
  67.     public abstract void complexFillWithTable(T t, String fileName, HttpServletResponse response);
  68.     /**
  69.      * 查询导出总条数
  70.      *
  71.      * @param t
  72.      * @return
  73.      */
  74.     public abstract int countExport(T t);
  75.     /**
  76.      * 查询导出数据
  77.      *
  78.      * @param t
  79.      * @return
  80.      */
  81.     public abstract List<K> getExportDetail(T t);
  82. }
复制代码
ExportImpl导出实现方法
  1. package com.example.system.service.impl;
  2. import com.alibaba.excel.ExcelWriter;
  3. import com.example.system.api.domain.ExportUser;
  4. import com.example.system.config.AbstractExport;
  5. import com.example.system.config.ExportQueue;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Service;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.IOException;
  11. import java.util.LinkedList;
  12. import java.util.List;
  13. @Service
  14. @Slf4j
  15. public class ExportImpl extends AbstractExport {
  16.     @Autowired
  17.     private ExportQueue exportQueue;
  18.     @Override
  19.     public void export(ExportUser sysUser) throws InterruptedException {
  20.         //导出
  21.         log.info("导出文件方法执行~~~~~~~~~");
  22. //        export(response,pageSize,t,k,fileName);
  23.         LinkedList<ExportUser> queue = exportQueue.add(sysUser);
  24.         log.info("导出队列:" + queue);
  25.         //休眠时间稍微设置大点,模拟导出处理时间
  26.         Thread.sleep(20000);
  27.         //导出成功后移除当前导出用户
  28.         ExportUser nextSysUser = exportQueue.getNextSysUser();
  29.         log.info("移除后获取下一个排队的用户: " + nextSysUser.getUserName());
  30.     }
  31.     @Override
  32.     public void export(HttpServletResponse response, int pageSize, Object o, Class k, String fileName) throws Exception {
  33.         super.export(response, pageSize, o, k, fileName);
  34.     }
  35.     @Override
  36.     public ExcelWriter getExcelWriter(HttpServletResponse response, String fileName) throws IOException {
  37.         return super.getExcelWriter(response, fileName);
  38.     }
  39.     @Override
  40.     public void complexFillWithTable(Object o, String fileName, HttpServletResponse response) {
  41.     }
  42.     @Override
  43.     public int countExport(Object o) {
  44.         return 0;
  45.     }
  46.     @Override
  47.     public List getExportDetail(Object o) {
  48.         return null;
  49.     }
  50. }
复制代码
测试controller
  1. package com.example.system.controller;
  2. import com.example.system.api.domain.ExportUser;
  3. import com.example.system.api.domain.SysUser;
  4. import com.example.system.service.impl.ExportImpl;
  5. import lombok.SneakyThrows;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.web.bind.annotation.PostMapping;
  9. import org.springframework.web.bind.annotation.RequestBody;
  10. import org.springframework.web.bind.annotation.RequestMapping;
  11. import org.springframework.web.bind.annotation.RestController;
  12. @RestController
  13. @RequestMapping("/export")
  14. @Slf4j
  15. public class ExportController {
  16.     @Autowired
  17.     private ExportImpl export;
  18.     @PostMapping("/exportFile")
  19.     public void exportFile() {
  20.             new Thread(new Runnable() {
  21.                 @SneakyThrows
  22.                 @Override
  23.                 public void run() {
  24.                     Thread thread1 = Thread.currentThread();
  25.                     ExportUser sysUser =new ExportUser();
  26.                     sysUser.setUserName(thread1.getName());
  27.                     export.export(sysUser);
  28.                 }
  29.             }).start();
  30.         }
  31. }
复制代码
测试结果

通过哀求测试方法,限制了我们导出队列最大限制10次,队列场长度超过10次则无法进行继续提交;

第一次哀求和第二次哀求,间隔10秒,第一个用户导出完成后出列,下一个列队用户在队列首位,在进行导出哀求排在上一个用户后面;

总结

别的的还未实现,导出文件的表的计划、oss文件上传、用户导出文件下载,还有高并发的场景下会不会出现什么问题,这些都还没有太思量进去; 实现的方式应该挺多的,Redis的队列应该也是可以的,这里仅仅提供一个实现思绪。
更多文章推荐:
1.Spring Boot 3.x 教程,太全了!
2.2,000+ 道 Java口试题及答案整理(2024最新版)
3.免费获取 IDEA 激活码的 7 种方式(2024最新版)
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

灌篮少年

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表