在处理大量表数据的时候大家经常会碰到这样的场景:
假如存在一个表且行数为30万行,目前需要把这30万条数据查询出来处理后生成一份新的报表,显然把整个表读在内存中再处理是不现实的,因为30万条数据会占用大量的内存,这时候思路会进一步通过分页查询,处理一批数据就释放资源。可是因为这是单线程查询,为何把30万条数据先分成若干份,然后再使用多线程并行处理,思路是有了,现在通过HIbernate和Java8 Stream API如何实现的问题了,下面展示例子
NavigableMap<Month, Optional<List<FgOutItem>>> result = new ConcurrentSkipListMap<>((i1, i2) -> i1.compareTo(i2));
IntStream.range(1, 13).parallel().forEach(month -> {
List<FgOutItem> fgOutItems = HibernateUtils.doInNewSession(new CurrentSessionCallback<List<FgOutItem>>() {
@Override
public List<FgOutItem> work(Session currentSession) {
FgOutItemQueryBuilder fgOutItemQueryBuilder = new FgOutItem.FgOutItemQueryBuilder();
fgOutItemQueryBuilder.queryComment("获取货明细每个月份的数据");
LocalDate start = LocalDate.of(currentDate.getYear(), month, 1);
fgOutItemQueryBuilder.fObDate(TimeDateUtils.toDate(start), TimeDateUtils.lastDayOfMonth(start));
List<FgOutItem> fgOutItems = fgOutItemQueryBuilder.list();
return fgOutItems;
}
});
result.put(Month.of(month), Optional.ofNullable(fgOutItems));
});
上面的思想是这样,在一张出货表中把数据按月份分成12个月,这里调用Stream#parallel()方法是使用原生的Stream API 来创建和启动流程(结合Java Stream就为从线程管理中解脱)Java Stream parallel默认是使用Java 环境中的CommonPool作为线程池而使用的线程数可以依据需求设置(方法请在网上找),那线程管理问题算是解决了,现在面临新的问题就是Hibernate Session 线程安全的问题(Session对象是非线程安全的对象),假如了解Spring整合Hibernate逻辑你会知道Spring中有一个org.springframework.orm.hibernate5.SpringSessionContext ,这个类简单来说是保证在每一个线程中都绑定唯一个Hibernate Session实例(不明白可以提评论),通过这种方式达到Hibernate线程安全的目的,如果在Spring受控的线程中SpringSessionContext会帮我们做这件事程,但是现在并行处理的线程是CommonPool中获取的而是Spring管理的线程,所以接下来要做的事情就是在Stream循环结构体中把Session实例绑定到当前线程中,请看以下代码:
@FunctionalInterface
public interface CurrentSessionCallback<R> {
R work(Session currentSession);
}
定义一个回调接口,接口传递当前绑定后的Session实例,而HibernateUtils#doInNewSession处理管理Hibernate Session实例的逻辑
public static <R> R doInNewSession(CurrentSessionCallback<R> callback) {
SessionFactory sessionFactory = AppContext.getService(SessionFactory.class);
boolean hasResource = TransactionSynchronizationManager.hasResource(sessionFactory);
if (!hasResource) {
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(sessionFactory.openSession()));
invokeCount.incrementAndGet();
}
try {
SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
R result = callback.work(sessionHolder.getSession());
if (!Hibernate.isInitialized(result)) {
Hibernate.initialize(result);
}
return result;
} catch (Exception e) {
logger.error("并行执行Hibernate Session错误", e);
throw new RuntimeException(e.getMessage());
} finally {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {//有可能是主经程也会被invoker
SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
sessionHolder.getSession().close();
invokeCount.decrementAndGet();
}
}
}
**总结:**通过上述方法就可以很轻易地做到把一个大表分成若干份再并行处理,最后当然需要reduce的操作(把各个线程处理完的结果再合并输出),其实这种设计思想和MapReduce是一样,同样也是分而治之的方式,只是MapReduce把任务拆分后分给节点而这里分配给线程,这样操作大大简化管理线程的逻辑,我个人是觉得很方便,现在只要遇到这种需要大数据的报表的时候就会采用这种多线程并行处理方式。