背景

写这篇文章主要是介绍一下我做数据仓库ETL同步的过程中遇到的一些有意思的内容和提升程序运行效率的过程。

关系型数据库:

  项目初期:游戏的运营数据比较轻量,相关的运营数据是通过Java后台程序聚合查询关系型数据库MySQL完全可以应付,系统通过定时任务每日统计相关数据,等待运营人员查询即可。

  项目中后期:随着开服数量增多,玩家数量越来越多,数据库的数据量越来越大,运营后台查询效率越来越低。对于普通的关系型来说,如MySQL,当单表存储记录数超过500万条后,数据库查询性能将变得极为缓慢,而往往我们都不会只做单表查询,还有多表join。这里假如有100个游戏服,每个服有20张表,而每个表有500W数据,那么:

  总数据量 = 100 * 20 * 500W = 10亿  按当时的库表结构,换算成磁盘空间,约为100G左右

我的天呐,现在没有单机的内存能同一时间载入100G的数据

https://www.zhihu.com/question/19719997

  所以,考虑到这一点,Hive被提出来解决难题!

数据仓库

Hive适合做海量数据的数据仓库工具, 因为数据仓库中的数据有这两个特点:最全的历史数据(海量)、相对稳定的;所谓相对稳定,指的是数据仓库不同于业务系统数据库,数据经常会被更新,数据一旦进入数据仓库,很少会被更新和删除,只会被大量查询。而Hive,也是具备这两个特点

二、项目架构设计

在这里先说下初期项目架构的探索,因为数据流向,其实最终就是从MYSQL--------->Hive中,我使用的是Jdbc方式。为什么不使用下列工具呢?

  • Sqoop, 因为该游戏每个服有将近80张表,然后又有很多服,以后还会更多,而每个服的库表数据结构其实是完全一样的,只是IP地址不一样,使用Sqoop的话,将会需要维护越来越多的脚本,再者Sqoop没法处理原始数据中一些带有Hive表定义的行列分隔符
  • DataX 阿里开源的数据同步中间件,没做过详细研究

1、全局缓存队列

使用生产者消费者模型,中间使用内存,数据落地成txt

首先生产者通过Jdbc获取源数据内容,放入固定大小的缓存队列,同时消费者不断的从缓存读取数据,根据不同的数据类型分别读取出来,并逐条写入相应的txt文件。

速度每秒约8000条。

这样做表面上看起来非常美好,流水式的处理,来一条处理一下,可是发现消费的速度远远赶不上生产的速度,生产出来的数据会堆积在缓存队列里面,假如队列不固定长度的话,这时候还会大量消耗内存,所以为了提升写入的速度,决定采用下一种方案

2、每一张表一个缓存队列及writer接口

每张表各自起一个生产者消费者模型,消费者启动时初始化相应的writer接口,架构设计如下:

table1的生产者通过Jdbc获取源数据内容,放入table自带的固定大小的缓存队列,同时table1相应的消费者不断的从缓存读取数据,根据不同的数据类型分别读取出来,并逐条写入相应的txt文件。

速度每秒约2W条。

这样生产者线程可以并发的进行,通过控制生产者线程的数量,可以大大提高处理的效率, 项目关键代码如下:

1)线程池

/***
*
*
* @描述 任务线程池
*/
public class DumpExecuteService { private static ExecutorService dumpServerWorkerService; // 游戏服任务
private static ExecutorService dumpTableWorkerService; // 表数据任务
private static ExecutorService dumpReaderWorkerService; // 读取数据任务
private static ExecutorService dumpWriterWorkerService; // 写数据结果任务 /***
* 初始化任务线程池
* @param concurrencyDBCount 并发数量
*/
public synchronized static void startup(int concurrencyDBCount) { if (dumpServerWorkerService != null)
return; if (concurrencyDBCount > )
concurrencyDBCount = ; // 最多支持两个数据库任务并发执行 if (concurrencyDBCount < )
concurrencyDBCount = ; dumpServerWorkerService = Executors.newFixedThreadPool(concurrencyDBCount, new NamedThreadFactory(
"DumpExecuteService.dumpServerWorkerService" + System.currentTimeMillis()));
dumpTableWorkerService = Executors.newFixedThreadPool(, new NamedThreadFactory("DumpExecuteService.dumpTableWorkerService"
+ System.currentTimeMillis()));
dumpWriterWorkerService = Executors.newFixedThreadPool(, new NamedThreadFactory("DumpExecuteService.dumpWriterWorkerService"
+ System.currentTimeMillis()));
dumpReaderWorkerService = Executors.newFixedThreadPool(, new NamedThreadFactory("DumpExecuteService.dumpReaderWorkerService"
+ System.currentTimeMillis()));
} public static Future<Integer> submitDumpServerWorker(DumpServerWorkerLogic worker) {
return dumpServerWorkerService.submit(worker);
} public static Future<Integer> submitDumpWriteWorker(DumpWriteWorkerLogic worker) {
return dumpWriterWorkerService.submit(worker);
} public static Future<Integer> submitDumpReadWorker(DumpReadWorkerLogic worker) {
return dumpReaderWorkerService.submit(worker);
} public static Future<Integer> submitDumpTableWorker(DumpTableWorkerLogic worker) {
return dumpTableWorkerService.submit(worker);
} /***
* 关闭线程池
*/
public synchronized static void shutdown() { //执行线程池关闭...
}
}

说明:该类定义4个线程池,分别用于执行不同的任务

2)游戏服任务线程池

/**
* 1) 获取 游戏服log库数据库连接
2) 依次处理单张表
*/
public class DumpServerWorkerLogic extends AbstractLogic implements Callable<Integer> {
private static Logger logger = LoggerFactory.getLogger(DumpServerWorkerLogic.class); private final ServerPO server;// 数据库 private final String startDate;// 开始时间 private SourceType sourceType;// 数据来源类型 private Map<String, Integer> resultDBMap;// 表记录计数 private GameType gameType; public DumpServerWorkerLogic(ServerPO server, String startDate, SourceType sourceType, Map<String, Integer> resultDBMap,
GameType gameType) {
CheckUtil.checkNotNull("DumpServerWorkerLogic.server", server);
CheckUtil.checkNotNull("DumpServerWorkerLogic.startDate", startDate);
CheckUtil.checkNotNull("DumpServerWorkerLogic.sourceType", sourceType);
CheckUtil.checkNotNull("DumpServerWorkerLogic.resultDBMap", resultDBMap);
CheckUtil.checkNotNull("DumpServerWorkerLogic.gameType", gameType); this.server = server;
this.startDate = startDate;
this.sourceType = sourceType;
this.resultDBMap = resultDBMap;
this.gameType = gameType;
} @Override
public Integer call() { // 获取连接, 并取得该库的所有表
Connection conn = null;
try {
conn = JdbcUtils.getDbConnection(server);
}
catch (Exception e) {
throw new GameRuntimeException(e.getMessage(), e);
}
List<String> tableNames = null;
DumpDbInfoBO dumpDbInfoBO = DumpConfig.getDumpDbInfoBO(); int totalRecordCount = ;
try {
switch (this.sourceType) {
case GAME_LOG:
tableNames = JdbcUtils.getAllTableNames(conn);
break;
case INFOCENTER:
tableNames = dumpDbInfoBO.getIncludeInfoTables();
tableNames.add("pay_action");
break;
case EVENT_LOG:
tableNames = new ArrayList<String>();
Date date = DateTimeUtil.string2Date(startDate, "yyyy-MM-dd");
String sdate = DateTimeUtil.date2String(date, "yyyyMMdd");
String smonth = DateTimeUtil.date2String(date, "yyyyMM");
tableNames.add("log_device_startup" + "_" + smonth);
tableNames.add("log_device" + "_" + sdate);
break;
} // 遍历table
for (String tableName : tableNames) {
// 过滤
if (dumpDbInfoBO.getExcludeTables().contains(tableName))
continue;
DumpTableWorkerLogic tableTask = new DumpTableWorkerLogic(conn, server, tableName, startDate, resultDBMap,
gameType, sourceType);
Future<Integer> tableFuture = DumpExecuteService.submitDumpTableWorker(tableTask);
int count = tableFuture.get();
totalRecordCount += count;
logger.info(String.format("DumpServerWorkerLogic %s-%s.%s be done", startDate, server.getLogDbName(), tableName));
}
return totalRecordCount;
} catch (Exception e) {
throw new GameRuntimeException(e, "DumpTableWorkerLogic fail. server={%s}, errorMsg={%s} ",server.getId(), e.getMessage());
} finally {
JdbcUtils.closeConnection(conn);
} } }

3)表处理任务,一个表一个

/***
*
*
* @描述 创建一个表查询结果写任务 (一个表一个)
*/
public class DumpTableWorkerLogic implements Callable<Integer> {
private static Logger logger = LoggerFactory.getLogger(DumpTableWorkerLogic.class); private final String tableName;
private final Connection conn; private ServerPO server; private String startDate; private Map<String, Integer> resultDBMap;// 表记录计数 private GameType gameType; private SourceType sourceType;// 数据来源类型 public DumpTableWorkerLogic(Connection conn, ServerPO server, String tableName, String startDate, Map<String, Integer> resultDBMap,
GameType gameType, SourceType sourceType) {
CheckUtil.checkNotNull("DumpTableWorkerLogic.conn", conn);
CheckUtil.checkNotNull("DumpTableWorkerLogic.tableName", tableName);
CheckUtil.checkNotNull("DumpTableWorkerLogic.server", server);
CheckUtil.checkNotNull("DumpTableWorkerLogic.startDate", startDate);
CheckUtil.checkNotNull("DumpTableWorkerLogic.resultDBMap", resultDBMap);
CheckUtil.checkNotNull("DumpTableWorkerLogic.gameType", gameType);
CheckUtil.checkNotNull("DumpServerWorkerLogic.sourceType", sourceType); this.conn = conn;
this.tableName = tableName;
this.server = server;
this.startDate = startDate;
this.resultDBMap = resultDBMap;
this.gameType = gameType;
this.sourceType = sourceType; logger.info("DumpTableWorkerLogic[{}] Reg", tableName);
} @Override
public Integer call() {
logger.info("DumpTableWorkerLogic[{}] Start", tableName); // 写检查结果任务
DumpWriteWorkerLogic writerWorker = new DumpWriteWorkerLogic(server, tableName, startDate, resultDBMap, gameType,
sourceType);
Future<Integer> writeFuture = DumpExecuteService.submitDumpWriteWorker(writerWorker);
logger.info("DumpTableWorkerLogic[{}] writer={}", tableName); // 数据查询任务
DumpReadWorkerLogic readerWorker = new DumpReadWorkerLogic(conn, tableName, writerWorker, startDate);
DumpExecuteService.submitDumpReadWorker(readerWorker);
logger.info("DumpTableWorkerLogic[{}] reader={}", tableName); try {
int writeCount = writeFuture.get();
logger.info("DumpTableWorkerLogic[{}] ---" + startDate + "---" + server.getId() + "---" + tableName + "---导出数据条数---"
+ writeCount);
return writeCount;
} catch (Exception e) {
throw new GameRuntimeException(e, "DumpTableWorkerLogic fail. tableName={%s}, errorMsg={%s} ",tableName, e.getMessage());
}
} }

4)单表读取任务线程

/***
* mysql读取数据任务
*
*/
public class DumpReadWorkerLogic implements Callable<Integer> { private static Logger logger = LoggerFactory.getLogger(DumpReadWorkerLogic.class); private String tableName; private final Connection conn; private DumpWriteWorkerLogic writerWorker; // 写结果数据任务 private String startDate;// 开始导出日期 private static final int LIMIT = ;// 限制sql一次读出条数 public DumpReadWorkerLogic(Connection conn, String tableName, DumpWriteWorkerLogic writerWorker, String startDate) {
CheckUtil.checkNotNull("MysqlDataReadWorker.conn", conn);
CheckUtil.checkNotNull("MysqlDataReadWorker.tableName", tableName);
CheckUtil.checkNotNull("MysqlDataReadWorker.startDate", startDate); this.conn = conn;
this.tableName = tableName;
this.writerWorker = writerWorker;
this.startDate = startDate; logger.info("DumpReadWorkerLogic Reg. tableName={}", this.tableName);
} @Override
public Integer call() {
try {
List<Map<String, Object>> result = JdbcUtils.queryForList(conn, "show full fields from " + tableName); int index = ;
String querySql = ""; int totalCount = ;
while (true) {
int offset = index * LIMIT;
querySql = DumpLogic.getTableQuerySql(result, tableName, true, startDate) + " limit " + offset + "," + LIMIT;
int row = DumpLogic.query(conn, querySql, writerWorker);
totalCount += row;
logger.info("tableName=" + tableName + ", offset=" + offset + ", index=" + index + ", row=" + row + ", limit=" + LIMIT);
if (row < LIMIT)
break;
index++;
}
writerWorker.prepareClose();
logger.info(startDate + "---" + tableName + "---Read.End");
return totalCount;
}
catch (Exception e) {
throw new GameRuntimeException(e, "MysqlDataReadWorker fail. tableName={%s}, errorMsg={%s} ",tableName, e.getMessage());
}
} }

5)单表写入任务线程

/***
*
*
* @描述 mysql数据导出任务
*/
public class DumpWriteWorkerLogic implements Callable<Integer> { private static final Logger logger = LoggerFactory.getLogger(DumpWriteWorkerLogic.class);
private String tableName;// 表名 private AtomicBoolean alive; // 线程是否活着 private BufferedWriter writer; private ArrayBlockingQueue<String> queue; // 消息队列 private ServerPO server;// 服务器 private String startDate;// 开始时间 private Map<String, Integer> resultDBMap;// 当天某服某表数量记录 private GameType gameType; private SourceType sourceType;// 数据来源类型 public DumpWriteWorkerLogic(ServerPO server, String tableName, String startDate, Map<String, Integer> resultDBMap, GameType gameType,
SourceType sourceType) {
CheckUtil.checkNotNull("DumpWriteWorkerLogic.tableName", tableName);
CheckUtil.checkNotNull("DumpWriteWorkerLogic.server", server);
CheckUtil.checkNotNull("DumpWriteWorkerLogic.startDate", startDate);
CheckUtil.checkNotNull("DumpWriteWorkerLogic.resultDBMap", resultDBMap);
CheckUtil.checkNotNull("DumpWriteWorkerLogic.gameType", gameType);
CheckUtil.checkNotNull("DumpWriteWorkerLogic.sourceType", sourceType); this.tableName = tableName;
this.server = server;
this.startDate = startDate;
this.queue = new ArrayBlockingQueue<>();
this.alive = new AtomicBoolean(true);
this.gameType = gameType;
this.sourceType = sourceType;
this.writer = createWriter();
this.resultDBMap = resultDBMap; logger.info("DumpWriteWorkerLogic Reg. tableName={}", this.tableName);
} /***
* 创建writer, 若文件不存在,会新建文件
*
* @param serverId
* @return
*/
private BufferedWriter createWriter() {
try {
File toFile = FileUtils.getFilenameOfDumpTable(sourceType, tableName, startDate, gameType, ".txt");
if (!toFile.exists()) {
FileUtils.createFile(sourceType, tableName, startDate, gameType);
}
return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(toFile, true), Charsets.UTF_8), * * );
} catch (Exception e) {
throw new GameRuntimeException(e, "DumpWriteWorkerLogic createWriter fail. server={%s}, errorMsg={%s} ",server.getId(), e.getMessage());
}
} /***
* 写入文件
*
* @param line
* 一条记录
*/
private void writeToFile(String line) {
try {
this.writer.write(line + "\n");
} catch (Exception e) {
throw new GameRuntimeException(e, "DumpWriteWorkerLogic writeToFile fail. errorMsg={%s} ", e.getMessage());
}
} /**
* 记录数据到消息队列; 如果消息队列满了, 会阻塞直到可以put为止
*
* @param result
*/
public void putToWriterQueue(String line) { CheckUtil.checkNotNull("DumpWriteWorkerLogic putToWriterQueue", line); try {
queue.put(line);
} catch (InterruptedException e) {
throw new GameRuntimeException(e, "DumpWriteWorkerLogic putToWriterQueue fail. errorMsg={%s} ", e.getMessage());
}
} /**
* 准备关闭 (通知一下"需要处理的用户数据都处理完毕了"; task 写完数据, 就可以完毕了)
*/
public void prepareClose() {
alive.set(false);
} @Override
public Integer call() {
logger.info("DumpWriteWorkerLogic Start. tableName={}", this.tableName);
try {
int totalCount = ;
while (alive.get() || !queue.isEmpty()) {
List<String> dataList = new ArrayList<String>();
queue.drainTo(dataList);
int count = processDataList(dataList);
totalCount += count;
}
logger.info("DumpWriteWorkerLogic ---" + startDate + "---" + tableName + "---Writer.End");
return totalCount;
} catch (Exception exp) {
throw new GameRuntimeException(exp, "DumpWriteWorkerLogic call() fail. errorMsg={%s} ", exp.getMessage());
} finally {
FileUtil.close(this.writer);
}
} /***
* 处理数据:写入本地文件及map
*
* @param dataList
* 数据集合
* @return
*/
private int processDataList(List<String> dataList) {
int totalCount = ; // 所有记录
String key = server.getId() + "#" + tableName + "#" + sourceType.getIndex();
if (dataList != null && dataList.size() > ) { for (String line : dataList) { // 按行写入文件
writeToFile(line); // 记录到result_data_record_count
if (resultDBMap.get(key) != null) {
resultDBMap.put(key, resultDBMap.get(key) + );
}
else {
resultDBMap.put(key, );
} totalCount++;
}
} return totalCount;
} }

内存优化

1、使用Jdbc方式获取数据,如果这个数据表比较大,那么获取数据的速度特别慢;

2、这个进程还会占用非常大的内存,并且GC不掉。分析原因,Jdbc获取数据的时候,会一次将所有数据放入到内存,如果同步的数据表非常大,那么甚至会将内存撑爆。

那么优化的方法是让Jdbc不是一次全部将数据拿到内存,而是分页获取,每次最大limit数设置为50000,请参考read线程。

经过这种架构优化后,5000W数据大约花费40min可完成导出

说明:

因为本文只是记录项目的设计过程,详细的代码后面会开源。

数据仓库:Mysql大量数据快速导出的更多相关文章

  1. MySQL多线程数据导入导出工具Mydumper

    http://afei2.sinaapp.com/?p=456 今天在线上使用mysqldump将数据表从一个库导入到另外一个库,结果速度特别慢,印象中有个多线程的数据导入导出工具Mydumper,于 ...

  2. Oracle大规模数据快速导出文本文件

    哈喽,前几久,和大家分享过如何把文本数据快速导入数据库(点击即可打开),今天再和大家分享一个小技能,将Oracle数据库中的数据按照指定分割符.指定字段导出至文本文件.首先来张图,看看导出的数据是什么 ...

  3. Mysql 的数据导入导出

    一. mysqldump工具基本用法,不适用于大数据备份   1. 备份所有数据库: mysqldump -u root -p --all-databases > all_database_sq ...

  4. 使用MySQL的SELECT INTO OUTFILE ,Load data file,Mysql 大量数据快速导入导出

    使用MySQL的SELECT INTO OUTFILE .Load data file LOAD DATA INFILE语句从一个文本文件中以很高的速度读入一个表中.当用户一前一后地使用SELECT ...

  5. mysql 命令行快速导出数据,导入数据

    如果数据有20几万以上的时候,下面的方法很实用 导出数据 1.into outfile select * from table into outfile 'C:/a.sql'; 2.mysqldump ...

  6. 大量数据快速导出的解决方案-Kettle

    1.开发背景 在web项目中,经常会需要查询数据导出excel,以前比较常见的就是用poi.使用poi的时候也有两种方式,一种就是直接将集合一次性导出为excel,还有一种是分批次追加的方式适合数据量 ...

  7. mysql的数据导入导出

    1.Navicat for Mysql XML导出导入格式支持二进制数据:虽然同步数据人眼看不出区别,但是java尝试读取数据时,报datetime字段取出的值为“0000-00-00 00:00:0 ...

  8. MySQL之数据导入导出

    日常开发中,经常会涉及到对于数据库中数据的导入与导出操作,格式也有很多: TXT,CSV,XLS,SQL等格式,所以,在此总结一下,省的总是百度查询. 一 导出 1) 常用的方式就是使用现成的工具例如 ...

  9. mysql导入数据和导出数据

    导入数据: 首页进入mysql命令行界面: use 数据库名: source d:/data/test.sql; 如果是windows系统必须使用d:/,如果使用d:\会报语法错误. 那么如何导出(备 ...

随机推荐

  1. Spring cloud Eureka错误锦集(二)

    最近学习spring cloud,在测试Eureka作为服务注册中心的时候碰到了问题,错误提示如下: "D:\Program\Java\JDK1.8\bin\java" -XX:T ...

  2. 【开源GPS追踪】 之 服务器硬伤

    前面就说过了,目前GPS 追踪的原理都是通过GPRS将数据发送到一个服务器上,如果回看数据就从服务器上去数据,服务器在整个系统中具有举足轻重的地位. 如果服务器坏了,整个系统几千台设备可能也就无法工作 ...

  3. [USACO18DEC]Cowpatibility

    Description: Farmer John 的 \(N\) 头奶牛(\(2\le N\le 5\times 10^4\))各自列举了她们最喜欢的五种冰激凌口味的清单.为使这个清单更加精炼,每种可 ...

  4. 基于socketserver模块并发套接字

    1.基于tcp协议 服务端: import socketserverclass MyHandler(socketserver .BaseRequestHandler ): def handle(sel ...

  5. LINUX文件及目录管理命令基础(2)

    Linux文件类型 文件作为Linux操作系统中最常见的对象,在系统中被分为了多种类型 如下: - 普通文件 d 目录 l 链接 b 块设备 c 字符设备 p 管道设备 s 套接字文件 Linux目录 ...

  6. BZOJ4964 : 加长的咒语

    把$($看作$-1$,$)$看作$1$,设$a$为前缀和,则相当于找两个位置$x,y$使得$a[x]=a[y]$,且$a[x]$是$[x,y]$的区间最大值. 求出询问区间的最大值$o$,然后找到$o ...

  7. CSharp遗传算法求解背包问题

    断断续续写了四天,感觉背包问题是最适合了解遗传算法的问题模型 using System; using System.Collections.Generic; using System.Linq; us ...

  8. 可以直接用的“ html转字符串string”方法

    //html转字符串 -(NSString *)filterHTMLString:(NSString *)html { NSScanner * scanner = [NSScanner scanner ...

  9. idea快捷键列表

    Ctrl+Shift + Enter,语句完成 “!”,否定完成,输入表达式时按 “!”键 Ctrl+E,最近的文件 Ctrl+Shift+E,最近更改的文件 Shift+Click,可以关闭文件 C ...

  10. helm-chart4,流程控制和变量

    控制结构(模板说法中称为"动作")提供了控制模板生成流程的能力.Helm的模板语言提供了以下控制结构: if/ else用于创建条件块 with 指定范围 range,它提供了一个 ...