Java数据库分表与多线程查询结果汇总
今天接到一个需求:要对一个物理分表的逻辑表进行查询统计。而数据库用的是公司自己研发的产品,考虑的到公司产品的特点以及业务的需求,该逻辑表是按年月进行分表的,而非分区。我们来看一下,在按时间段进行查询统计的时候,会有哪些问题:
- 需要对多个表查询,且表个数不确定
- 时间跨度越大,查询等等表个数越多,对应查询时间也会越长
如何解决?一起来看一下
分表与分区
目的
既然谈到数据的分表与分区,那我们来简单了解一下。先说一下分表与分区的目的。我们日常开发中都会经常遇到百万或千万级的数据大表,这些表数据量大,数据增速快,不用太久就会造成在查询或修改数据库数据的时候造成性能低下的问题,联合查询的时候,情况可能更糟。一次有必要对原来的表进行改造设计。这时候数据库分区和分表技术就应运而生了
区别
-
分表
分表是将一个大表按照一定的规则分解成多张子表,而各个子表存储空间彼此独立。
-
分区
分区也是按照一定的规则进行数据划分,对各部分数据各自存储,但在处理逻辑上,散列存放的数据还是属于同一张大表。
依赖于数据库实现,对程序屏蔽,减轻程序员编程压力
分表逻辑下的多线程查询与数据汇总
回到文首提到的情况,当前的情况是分表,分表的划分依据是根据年月划分,一个月一张表。意味着当我们要统计跨多个隔离单位的数据进行统计时,要自己去实现的对分散在多个表中数据的查询汇总处理。
通常表名会带有划分依据的信息,比如按年月划分,表名格式一般为
TABLE_NAME_YYYYMM
确定数据表
当前的需求是对一段时间内的数据进行统计,时间单位精确到月份。一次当我们根据服务入参拿到开始月份和结束月份后,要先得到所有涉及的月份。我们可以计算出将所有月份并保存在一个List中,方便我们查询各个表时进行表名的拼接。代码实现如下
/**
* 获取时间段内所有月份集合
* @param beginMonth 开始年月
* @param endMonth 结束年月
**/
private List<String> getMonths(String beginMonth,String endMonth){
List<String> result = new ArrayList<>();
Date beginDate = DateUtils.getDate("yyyyMM",beginMonth);
Date endDate = DateUtils.getDate("yyyyMM",endMonth);
if (beginDate.after(endDate)) {
throw new BusiException("时间入参非法");
}
result.add(beginMonth);
Calendar cal = Calendar.getInstance();
Date originalDate = beginDate;
while (endDate.after(originalDate)) {
cal.setTime(originalDate);
cal.add(Calendar.MONTH, 1);
originalDate = cal.getTime();
result.add(DateUtils.getFormatDate(originalDate).substring(0,6));
}
return result;
}
确认线程个数
拿到所有月份后,进行分多线程处理的操作,增加单位时间内查询表的个数,以此缩短查询时间,通常我们都利用线程池来进行多线程操作。这里会涉及线程池大小的考虑问题,可以参考以下博文:计算线程池最佳线程数。我们姑且用CPU复杂型公式进行计算
int cpuNums = Runtime.getRuntime().availableProcessors() + 1;
均匀分配数据
确定好线程的大小,我们还要考虑一个问题,那就是我们如何为一个线程均匀地分配数据的处理量,在当前的需求下,就是如何均匀地为每个线程分配对应处理的月份,可以参考以下代码:
/**
* 平衡分组算法 - 已知分配份数
* @param sourceList 数据源
* @param groupNum 被非配份数
**/
public static <T> List<T>[] spiltDataList(List<T> sourceList,int groupNum){
List<T> [] group = new List[groupNum];
/* 初始化数组 */
for (int i = 0 ; i < groupNum ; i++) {
group[i] = new ArrayList<>();
}
int sourceSize = sourceList.size();
int batchNum = sourceSize % groupNum == 0 ? sourceSize / groupNum : sourceSize / groupNum + 1;
for (int i = 1; i <= batchNum ; i++){
if (i == batchNum){
int finalBatchNum = sourceSize - (i - 1) * groupNum;
for (int j = 0 ; j < finalBatchNum ; j++){
group[j].add(sourceList.get((i - 1) * groupNum + j));
}
}else {
for (int j = 0 ; j < groupNum ; j++){
group[j].add(sourceList.get((i - 1) * groupNum + j));
}
}
}
return group;
}
多线程实现
要对所有子线程进行汇总,就必须使用Callable和Future的方式来实现多线程,我们就可以拿到每个子线程的查询返回,进而汇总分析处理。关于多线程实现方式,可以参考Java多线程事务管理中对多线程实现方式的介绍
以下为核心代码实现
/**
* @param qryType 查询类型
* @param qryValue 查询值
* @param payType 扣费类型
* @param beginMonth 开始年月
* @param endMonth 结束年月
**/
public List<CollInfoQueryBo> collInfoQueryByThread(
String qryType,Long qryValue,String payType,String beginMonth,String endMonth)
throws InterruptedException,ExecutionException{
List<CollInfoQueryBo> collInfoQueryBos = new ArrayList<>();
List<String> months = getMonths(beginMonth,endMonth);
int cpuNums = Runtime.getRuntime().availableProcessors() + 1;
int totalNum = months.size();
int threadNum;
if (totalNum < cpuNums){
threadNum = totalNum;
}else {
threadNum = cpuNums;
}
/* 分线程处理 */
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum);
CountDownLatch endLock = new CountDownLatch(threadNum);
BlockingQueue<Future<List<CollInfoQueryBo>>> queue = new LinkedBlockingQueue<>();
List<String>[] stringList = spiltDataList(months,threadNum);
for (List<String> monthList : stringList) {
Future<List<CollInfoQueryBo>> future = fixedThreadPool.submit(new Callable<List<CollInfoQueryBo>>() {
@Override
public List<CollInfoQueryBo> call() throws Exception {
List<CollInfoQueryBo> collInfoQueryBoList = getAllMonthResult(monthList,qryType,qryValue);
endLock.countDown();
return collInfoQueryBoList;
}
});
queue.add(future);
}
endLock.await();
/* 汇总结果 */
for(Future<List<CollInfoQueryBo>> future : queue) {
List<CollInfoQueryBo> currentThreadList = future.get();
collInfoQueryBos.addAll(currentThreadList);
}
fixedThreadPool.shutdown(); //关闭线程池
return collInfoQueryBos;
}