Java 百万数据秒级导出到Excel中

出自:

腾讯课堂 700多分钟干货实战Java多线程高并发高性能实战全集 , 我学习完了之后, 我给 老师在课上说的话做了个笔记,以及视频的内容,还有代码敲了一遍,然后添加了一些注释,把执行结果也整理了一下, 做了个笔记

1.案例背景

某个MySQL服务器的user表有100万条数据,请运用多线程并发编程等相关基础知识,将这100万条数据秒级(8秒内)导出到若干Excel文件.

实际需求如果产品经理说要弄到一个Excel文件的话,你可以跟产品经理要提建议的,因为你100万条数据往一个Excel里面保存的话那么你的文件会很大,而且看数据也不方便,也会很卡.

如果你把一百万条数据导出到多个Excel里面,一个Excel里面有两万条数据,这样看起来也稍微方便点.

2.百万数据串行从数据库导出到Excel中会产生什么问题?

数据量比较大,串行的话耗时比较长,百万级别数据放到一个List集合里面可能会出现内存溢出的问题.就是list数据结构里面还没装到100W条数据之后就内存溢出了.

所以就需要并行分批次处理数据, 比如说一次拉取2W条数据处理,处理完了快速的释放掉内存,这样防止内存溢出,

3.如何秒级将百万数据并发写到Excel的文件里面

如果是串行的去做的话,肯定是很慢的,就得批量并行去做,就需要多线程了,每个线程处理若干笔数据,比如说每个线程处理2万条数据.这样处理起来就快很多了.

4.如何从架构角度优化性能,如何解决整体性能瓶颈

img

答案是:功能分层

上面是三个流程,只有每一次性能都很好,整体性能才能好,如果有一层性能不好的话,那么整体性能也不会太好.

比如说即使数据库和Java应用的性能好,但是你配置的数据库连接池配置的不到位,那么整体的性能就不好.

如果你数据库性能好,数据库连接池配置的到位,但是你Java应用性能不好,那么整体的性能也不会太好.

所以数据库层 数据库连接池 Java应用,这几层的性能都要好.

MySQL的SQL语句要有索引,数据库连接池再调优一下,Java应用这里代码再用多线程处理一下,就很快了.

如果哪一层慢的话,就每一层代码上下打个日志,看日志的时间,来确定哪里比较慢.

5.MySQL数据库核心参数优化,Druid相关参数调优

MySQL 的 my.cfg参数调优:

innodb_thread_concurrency 参数调整

druid相关参数调优:

# 配置连接池的参数
initialSize=50
# 连接池的最大数据库连接数。设为0表示无限制。
maxActive=200
# 最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
maxWait=600000
# 连接池中的最小空闲连接数,Druid会定时扫描连接池的连接,如果空闲的连接数大于该值,则关闭多余的连接,反之则创建更多的连接以满足最小连接数要求。
minIdle=5

活跃数越高,性能就越高

6.代码使用方式

执行sql脚本

在D盘创建一个名字叫 excel 的文件夹.

执行ExcelExporter类即可

代码

sql

CREATE TABLE `test`.`user` (
  `id` INT NOT NULL,
  `name` VARCHAR(45) NULL,
  `createdTime` timestamp NULL,
  `updatedTime` timestamp NULL,
  PRIMARY KEY (`id`))
COMMENT = '用户测试表';

ALTER TABLE `test`.`user`
ADD INDEX `index` (`id` ASC);

ExcelExporter

package com.yrxy.thread.case2;

import com.yrxy.thread.common.PageHelper;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExcelExporter {

   public static void main(String[] args) {
   	createExcelFile();
   }

   public static void createExcelFile() {
   	ExecutorService pool = Executors.newFixedThreadPool(100);

   	//获取总条数
   	int count = UserHandler.queryCount();

       // 获取总页数
   	final int totalPageCount = PageHelper.getTotalPageCount(count);
   	String tableName = "user";

   	final long start = System.currentTimeMillis();
   	for (int currentPageNum = 0; currentPageNum < totalPageCount; currentPageNum++) {

   		String pageSql = PageHelper.getPageSql(tableName, currentPageNum);

   		final List userList = UserHandler.queryUserList(pageSql);
   		final int finalCurrentPageNum = currentPageNum;
   		Runnable run = () -> {
   			ExcelUtil.CreateExcel(finalCurrentPageNum, userList);
   			if (finalCurrentPageNum == (totalPageCount - 1)) {
   				System.out.println("  export data to excel, it  has spent " + (System.currentTimeMillis() - start) + "  ms");
   			}
   		};
   		pool.execute(run);

   	}
   }


}

ExcelUtil

package com.yrxy.thread.case2;

import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.HorizontalAlignment;

import java.io.FileOutputStream;
import java.util.List;

public class ExcelUtil {
   /**
    *
    * @param currentPageNum 当前页数 -1
    * @param userList  数据
    */
   public static void CreateExcel(int currentPageNum, List userList)  {
       // 第一步,创建一个webbook,对应一个Excel文件
       HSSFWorkbook wb = new HSSFWorkbook();
       // 第二步,在webbook中添加一个sheet,对应Excel文件中的sheet
       HSSFSheet sheet = wb.createSheet("用户信息");
       // 第三步,在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制short
       HSSFRow row = sheet.createRow((int) 0);
       // 第四步,创建单元格,并设置值表头 设置表头居中
       HSSFCellStyle style = wb.createCellStyle();
       style.setAlignment(HorizontalAlignment.CENTER); // 创建一个居中格式

       HSSFCell cell = row.createCell((short) 0);
       cell.setCellValue("id");
       cell.setCellStyle(style);
       cell = row.createCell((short) 1);
       cell.setCellValue("姓名");
       cell.setCellStyle(style);
       cell = row.createCell((short) 2);

       // 第五步,写入实体数据 实际应用中这些数据从数据库得到,

       for (int i = 0; i < userList.size(); i++)
       {
           row = sheet.createRow((int) i + 1);
           User stu = (User) userList.get(i);
           // 第四步,创建单元格,并设置值
           row.createCell((short) 0).setCellValue(stu.getId());
           row.createCell((short) 1).setCellValue(stu.getName());

       }
       // 第六步,把文件存到指定位置
       try
       {
           //为了测试方便,写死了路径,建议大家可以手工建D://excel路径,不然会报错
           FileOutputStream fout = new FileOutputStream("D://excel/user-"+(currentPageNum+1)+".xls");
           wb.write(fout);
           fout.close();
       }
       catch (Exception e)
       {
           e.printStackTrace();
       }


   }


}

User

 package com.yrxy.thread.case2;

import java.sql.Timestamp;

public class User {
    private int id;
    private String name;
    private Timestamp createdTime;
    private Timestamp updatedTime;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Timestamp getCreatedTime() {
        return createdTime;
    }

    public void setCreatedTime(Timestamp createdTime) {
        this.createdTime = createdTime;
    }

    public Timestamp getUpdatedTime() {
        return updatedTime;
    }

    public void setUpdatedTime(Timestamp updatedTime) {
        this.updatedTime = updatedTime;
    }
}

UserHandler

package com.yrxy.thread.case2;

import com.yrxy.thread.common.DataSourceUtils;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class UserHandler {

//     public final static long pageSize=10000;

	public static void main(String[] args) {
		System.out.append("count is  : " + queryCount());
	}

	/**
	 * 封装查询操作
	 *
	 * @param pageSql
	 * @return
	 */
	public static List queryUserList(String pageSql) {
		List userList = new ArrayList();
		Connection conn = DataSourceUtils.getConnection();
		ResultSet rst = null;
		User user;

		try {
			PreparedStatement pst = conn.prepareStatement(pageSql);
			rst = pst.executeQuery();
			while (rst.next()) {
				user = new User();
				user.setId((Integer) rst.getObject("id"));
				user.setName((String) rst.getObject("name"));
				user.setCreatedTime((Timestamp) rst.getObject("createdTime"));
				user.setUpdatedTime((Timestamp) rst.getObject("updatedTime"));
				userList.add(user);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}

		return userList;
	}

	/**获取总条数
	 */
	public static int queryCount() {
		String countSql = "SELECT count(*) as count  from user";
		ResultSet rst = null;
		Long count = null;
		Connection conn = null;
		try {
			conn = DataSourceUtils.getConnection();
			PreparedStatement pst = conn.prepareStatement(countSql);
			rst = pst.executeQuery();
			while (rst.next()) {
				count = (Long) rst.getObject("count");
			}

		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				rst.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
			try {
				conn.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		return count.intValue();
	}

}

PageHelper

package com.yrxy.thread.common;

/**
 * Filename: PageHelper.java
 * 
 * @function:分页对象,用作数据的批量处理
 */

public class PageHelper {

//    public static final int pageSize = 100000;
      public static final int pageSize = 10000;

    private int currentPageNum;
    private int lastPageNum;
    private int totalRecordCount;

    public static void main(String[] args) {
        String tableName="user";
        int index=1;
        long currentPageNum=0;
        String pageSql=PageHelper.getPageSql( tableName,  currentPageNum);
        System.out.println("  pageSql  is  : "+pageSql);

    }

    /**
     *获取总页数
     * @param totalRecordCount 总条数
     * @return 总页数
     */
    public static int getTotalPageCount(int totalRecordCount) {
        if (totalRecordCount == 0) {
            return 0;
        }

        int lastPageCount = totalRecordCount % pageSize;
        int totalPageCount;
        if (lastPageCount > 0) {
            // 如果 余数大于零的话
            totalPageCount = totalRecordCount / pageSize + 1;
        } else {
            //如果余数为零的话
            totalPageCount = totalRecordCount / pageSize;
        }
        return totalPageCount;
    }


    /**
     *拼接查询sql,根据id偏移量进行查询的
     * @param currentPageNum 当前页数-1
     * @return sql
     */
    public static String getPageSql(String tableName,long currentPageNum){

        return "select * from "+ tableName +
                " where id>=" +(1+(currentPageNum * pageSize)) +
                " and id<="+(currentPageNum+1) * pageSize;
    }


    /**
     * 
     * @param tableName
     * @param updateTimeLabel
     * @param lastMaxUpdateTime
     * @param interval
     * @param currentPageNum
     * @param lastPageNum
     * @return
     */
    public String getPageSql(String tableName, String updateTimeLabel, String lastMaxUpdateTime, String interval, int currentPageNum, int lastPageNum) {
        String pageSql;
        if (lastMaxUpdateTime.indexOf(Constants.sysdate) != -1) {
            pageSql = "select * from(select rownum AS rowno, a.* from " + tableName + " a where " + updateTimeLabel + " >=" + lastMaxUpdateTime + "-" + interval + " and rownum <=" + currentPageNum * pageSize + " order by "+updateTimeLabel+ " ) b where b.rowno >" + lastPageNum * pageSize;

        } else {
            pageSql = "select * from(select rownum AS rowno, a.* from " + tableName + " a where " + updateTimeLabel + ">=to_date('" + lastMaxUpdateTime + "','yyyy-MM-dd HH24:mi:ss')" + "-" + interval + " and rownum <=" + currentPageNum * pageSize + " order by "+updateTimeLabel+  ") b where b.rowno >" + lastPageNum * pageSize;

        }
        return pageSql;
    }

    /**
     * 
     * @param basicSql
     * @return
     */
    public String getTotalRecordsCountSql(String basicSql) {
        String totalRecordsCountSql = "select count(*) from " + "(" + basicSql + ")";
        return totalRecordsCountSql;
    }

    /**
     * 
     * @param tableName
     * @param updateTimeKey
     * @param updateTimeLable
     * @param lastMaxUpdateTime
     * @param interval
     * @return
     */
    public String buildDynamicSyncSql(String tableName, String updateTimeKey, String updateTimeLable, String lastMaxUpdateTime, String interval) {
        String sql;
        if (lastMaxUpdateTime.indexOf(Constants.sysdate) != -1) {
            sql = "SELECT  * FROM  " + tableName + "   where " + updateTimeLable + ">=" + lastMaxUpdateTime + "-" + interval;
        } else {
            sql = "SELECT  * FROM  " + tableName + "   where " + updateTimeLable + ">=to_date('" + lastMaxUpdateTime + "','yyyy-MM-dd HH24:mi:ss')" + "-" + interval;
        }

        return sql;
    }

    public int getCurrentPageNum() {
        return currentPageNum;
    }

    public void setCurrentPageNum(int currentPageNum) {
        this.currentPageNum = currentPageNum;
    }

    public int getLastPageNum() {
        return lastPageNum;
    }

    public void setLastPageNum(int lastPageNum) {
        this.lastPageNum = lastPageNum;
    }

    public int getTotalRecordCount() {
        return totalRecordCount;
    }

    public void setTotalRecordCount(int totalRecordCount) {
        this.totalRecordCount = totalRecordCount;
    }

    public static int getPagesize() {
        return pageSize;
    }



}

DataSourceUtils

package com.yrxy.thread.common;

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class DataSourceUtils {

    public static void main(String[] args){
      Connection conn=  DataSourceUtils.getConnection();
      System.out.println("conn is  :  "+conn);
    }

    //创建一个成员变量
    private static DataSource ds;

    /**
     * 加载的代码写在静态代码块中
     */
    static {
        try {
            Properties info = new Properties();
            //加载类路径下,即src目录下的druid.properties这个文件
            info.load(DataSourceUtils.class.getResourceAsStream("/druid.properties"));

            //读取属性文件创建连接池
            ds = DruidDataSourceFactory.createDataSource(info);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 得到数据源
     */
    public static DataSource getDataSource() {
        return ds;
    }

    /**
     * 得到连接对象
     */
    public static Connection getConnection() {
        try {
            return ds.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 释放资源
     */
    public static void close(Connection conn, Statement stmt, ResultSet rs) {
        if (rs!=null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (stmt!=null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn!=null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }


    public static void close(Connection conn, Statement stmt) {
        close(conn, stmt, null);
    }


}

druid.properties

initialSize=50
maxActive=200
maxWait=600000
minIdle=5

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://zjj101:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=false
username=root
password=root

代码Git地址

https://gitee.com/zjj19941/mutil-thread.git

看case2

  • 7
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值