一、DW中hibernate并发读写异常现象
使用hibernate进行多线程读写,需要绑定session到线程中,否则会出现
No session currently bound to execution context;
本文针对DW中如何使用hibernate并发操作数据库进行指导,上面报错的代码位置:ManagedSessionContext.java,是当无法从线程的本地变量中获取session时,抛出的异常。
public Session currentSession() {
Session current = existingSession(this.factory());
if (current == null) {
throw new HibernateException("No session currently bound to execution context");
} else {
this.validateExistingSession(current);
return current;
}
}
private static Session existingSession(SessionFactory factory) {
Map<SessionFactory, Session> sessionMap = sessionMap();
return sessionMap == null ? null : (Session)sessionMap.get(factory);
}
//最终从线程本地变量从取
private static final ThreadLocal<Map<SessionFactory, Session>> CONTEXT_TL = new ThreadLocal();
二、模拟串并行查询(Rest进入)
基于上述场景,我们从测试用例脚本出发,设计串并行查询的用例
//串行方式一
final SignalMapInfoServiceImpl signalService = SignalMapInfoServiceImpl.getInstance();
List<SignalMapInfo> result = new ArrayList<>();
IntStream.range(1, 21).forEach(p -> {
//注意些时getSigalMapInfoService无UnitOfWork修饰
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
});
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(-1);
result.add(sigalMapInfoService);
System.out.println("A==" + result.size());
//串行方式二
final SignalMapInfoServiceImpl signalService = new SignalMapInfoServiceImpl();
List<SignalMapInfo> result = new ArrayList<>();
IntStream.range(1, 21).forEach(p -> {
//注意些时getSigalMapInfoService无UnitOfWork修饰
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
});
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(-1);
result.add(sigalMapInfoService);
System.out.println("A1==" + result.size());
//并行方式一
final SignalMapInfoServiceImpl signalService = SignalMapInfoServiceImpl.getInstance();
List<SignalMapInfo> result = new ArrayList<>();
IntStream.range(1, 21).parallel().forEach(p -> {
//注意此时getSigalMapInfoService修改成无UnitOfWork修饰
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
});
System.out.println("B==" + result.size());
//并行方式二
final SignalMapInfoServiceImpl signalService = new SignalMapInfoServiceImpl();
List<SignalMapInfo> result = new ArrayList<>();
IntStream.range(1, 21).parallel().forEach(p -> {
//注意此时getSigalMapInfoService修改在无UnitOfWork修饰
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
});
System.out.println("C==" + result.size());
在如上的并发操作中都产生了No session currently bound to execution context异常。
三、手动管理session
如上的并发操作中都产生了No session currently bound to execution context,基于此尝试修改如下:
final SignalMapInfoServiceImpl signalService = SignalMapInfoServiceImpl.getInstance();
List<SignalMapInfo> result = new ArrayList<>();
//注意不能放在多线程内,提前传入sessionFactory在多线程里使用opesnSession
final SessionFactory sessionFactory = signalService.getSignalMapInfoDao().defaultSession().getSessionFactory();
IntStream.range(1, 21).parallel().forEach(p -> {
final Session session = sessionFactory.openSession();
//注意些时getSigalMapInfoService无UnitOfWork修饰
final SignalMapInfo sigalMapInfoService = signalService.getSignalMapInfoById(session, p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
session.close();
});
System.out.println("B1==" + result.size());
final SignalMapInfoServiceImpl signalService = new SignalMapInfoServiceImpl();
final SessionFactory sessionFactory = signalService.getSignalMapInfoDao().defaultSession().getSessionFactory();
List<SignalMapInfo> result = new ArrayList<>();
IntStream.range(1, 21).parallel().forEach(p -> {
final Session session = sessionFactory.openSession();
//注意些时getSigalMapInfoService无UnitOfWork修饰
final SignalMapInfo sigalMapInfoService = signalService.getSignalMapInfoById(session, p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
session.close();
});
System.out.println("C1==" + result.size());
这样并发报异常的问题解决了,但上述的方法存在明显的缺点:需要显示传入session,显示关闭session,针对session进行人工管理维护;在大规模使用时必须对业务有侵入,所有方法(getSignalMapInfoById)可以不用使用@UnitOfWork修饰;
所以这里有一个默认规则:
如果想自己管理session,则不需要使用DW的事务框架,即不需要UnitOfWorkAwareProxyFactory包装和@UnitOfWork修饰
但当然如果想强行使用DW的事务,仍然使用openSession理论功能上也无影响。
四、失败原理分析
分析下并发报错失败的原因:即使使用了Proxy进行包装,但压根不会进事务核心处理逻辑,原因方法上无UnitOfWork注解,会直接提前return(有判断逻辑)(即使已经注销SessionFactory后)
final SignalMapInfoServiceImpl signalService = SignalMapInfoServiceImpl.getInstance();
List<SignalMapInfo> result = new ArrayList<>();
ManagedSessionContext.unbind(signalService.getSignalMapInfoDao().defaultSession().getSessionFactory());
IntStream.range(1, 21).parallel().forEach(p -> {
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
});
System.out.println("C2==" + result.size());
事务控制逻辑UnitOfWorkAwareProxyFactory.java
proxy.setHandler((self, overridden, proceed, args) -> {
final UnitOfWork unitOfWork = overridden.getAnnotation(UnitOfWork.class);
final UnitOfWorkAspect unitOfWorkAspect = newAspect(sessionFactories);
try {
unitOfWorkAspect.beforeStart(unitOfWork);
Object result = proceed.invoke(self, args);
unitOfWorkAspect.afterEnd();
return result;
} catch (InvocationTargetException e) {
unitOfWorkAspect.onError();
throw e.getCause();
} catch (Exception e) {
unitOfWorkAspect.onError();
throw e;
} finally {
unitOfWorkAspect.onFinish();
}
});
Session existingSession = null;
if(ManagedSessionContext.hasBind(sessionFactory)) {
existingSession = sessionFactory.getCurrentSession();
}
if(existingSession != null) {
sessionCreated = false;
session = existingSession;
validateSession();
} else {
sessionCreated = true;
session = sessionFactory.openSession();
try {
configureSession();
ManagedSessionContext.bind(session);
beginTransaction(unitOfWork, session);
} catch (Throwable th) {
session.close();
session = null;
ManagedSessionContext.unbind(sessionFactory);
throw th;
}
}
但是我们看到:
public class ManagedSessionContext extends AbstractCurrentSessionContext {
private static final ThreadLocal<Map<SessionFactory,Session>> CONTEXT_TL = new ThreadLocal<Map<SessionFactory,Session>>();
所以想要并发多线程访问数据库
原则上要使用UnitOfWorkAwareProxyFactory包装+@UnitOfWork包装
即是在并发多线程中虽然使用了UnitOfWorkAwareProxyFactory包装,但方法未@UnitOfWork包装,所以进入代理逻辑后直接提前返回
五、使用规范
针对并发多线程的数据访问:
1、在线程内部使用UnitOfWorkAwareProxyFactory包装的代理Service对象,同时待操作方法要使用@UnitOfWork包装(缺少会失败)
//成功
final SignalMapInfoServiceImpl signalService = SignalMapInfoServiceImpl.getInstance();
List<SignalMapInfo> result = new ArrayList<>();
IntStream.range(1, 21).parallel().forEach(p -> {
final SignalMapInfo sigalMapInfoService = signalService.getSigalMapInfoService(p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
});
2、如不使用UnitOfWorkAwareProxyFactory包装的Service对象,多线程并发操作上,则需要自己使用SessionFactory openSession,并在任务执行完后关闭
final SignalMapInfoServiceImpl signalService = SignalMapInfoServiceImpl.getInstance();
List<SignalMapInfo> result = new ArrayList<>();
final SessionFactory sessionFactory = signalService.getSignalMapInfoDao().defaultSession().getSessionFactory();
IntStream.range(1, 21).parallel().forEach(p -> {
final Session session = sessionFactory.openSession();
final SignalMapInfo sigalMapInfoService = signalService.getSignalMapInfoById(session, p);
if (sigalMapInfoService != null) {
result.add(sigalMapInfoService);
}
session.close();
});
- Dao外的Service如有并发可能性,必须包装@UnitOfWork,即所有方法必须使用@UnitOfWork
- 性能优化特殊接口,可以自己管理session,进行并发控制(严格)需要线程外部sessionFactory.openSession()
六、思考题
那串行时,session是从哪来的呢?因为session的获取永远是从线程本地变量中获取的,难道在执行时,有什么逻辑会主动绑定一个session到当前线程中?从下图可以看到,DW框架中的UnitOfWorkApplicationListener,触发了ManagedSessionContext#bind 方法,绑定了session
事件:
@Provider
public class UnitOfWorkApplicationListener implements ApplicationEventListener {
可以发现DW框架针对所有Rest接口,会强制调用了UnitOfWorkEventListener::registerUnitOfWorkAnnotations,触发绑定线程中绑定session
而在Rest的接口上加了UnitOfWork的作用,先取接口,再取实现。有这注解,每次都会主动open一个Session并去bind,并打开事务,否则也提前return,这样在Rest线程中绑定了Session
综上想要在Rest接口中使用原生currentSession,则一定要使用UnitOfWork注释,这样才会在该线程中bind对应的session,不然在下面直接DAO操作数据会报错。
那又有一个疑问,那些不是从Rest触发的DAO操作,这个bind从哪来的呢?不会有session进行bind。这样就存在两种错误的使用场景:
- 从Rest进入,但方法没有UnitOfWork注释,这样currentsesion不会绑定,又不使用UnitOfWorkAwareProxyFactory,或者即使使用了UnitOfWorkAwareProxyFactory,但方法也没有UnitOfWork注释
- 不从Rest入口进入,又不使用UnitOfWorkAwareProxyFactory,或者即使使用了UnitOfWorkAwareProxyFactory,但方法也没有UnitOfWork注释
避免业务操作复杂性,可以针对所有包装DAO的Service,都要求使用UnitOfWorkAwareProxyFactory创建,同时Service中所有方法都使用UnitOfWork注释
在一些测试用例中,会出现java.lang.IllegalStateException: Existing session transaction state (false) does not match requested (true)报错,但通过下面的unbind则又正常,原因又是什么?
在主线程中使用unbind,这样在操作数据库时(UnitOfWorkAwareProxyFactory包装+UnitOfWork),进入后,优化判断是否绑定了sessio,由于已经解绑,则只能OpenSession
而如果没有unbind则使用cruuentSession进行操作,进入到代理逻辑中,会进入到validateSession中,代码如下:
protected void validateSession() {
if (unitOfWork == null || session == null) {
throw new NullPointerException("unitOfWork or session is null. This is a bug!");
}
if(unitOfWork.readOnly() != session.isDefaultReadOnly()) {
throw new IllegalStateException(String.format(
"Existing session readOnly state (%b) does not match requested state (%b)",
session.isDefaultReadOnly(),
unitOfWork.readOnly()
));
}
if(unitOfWork.cacheMode() != session.getCacheMode()) {
throw new IllegalStateException(String.format(
"Existing session cache mode (%s) does not match requested mode (%s)",
session.getCacheMode(),
unitOfWork.cacheMode()
));
}
if(unitOfWork.flushMode() != session.getHibernateFlushMode()) {
throw new IllegalStateException(String.format(
"Existing session flush mode (%s) does not match requested mode (%s)",
session.getHibernateFlushMode(),
unitOfWork.flushMode()
));
}
final Transaction txn = session.getTransaction();
if(unitOfWork.transactional() != (txn != null && txn.isActive())) {
throw new IllegalStateException(String.format(
"Existing session transaction state (%s) does not match requested (%b)",
txn == null ? "NULL" : Boolean.valueOf(txn.isActive()),
unitOfWork.transactional()
));
}
}
在最后一个判断抛出异常,原因是很明显调用方调用这个方法时,事务应该在开启状态,因为真正逻辑还未执行,但从当前的session中获取的Transaction的isActive确是false,两者不匹配。
那除了上面的解决这个默认的session,可否将currentsession的事务状态调整正确呢?
database.inTransaction(()->{
Response response = portalResManager.getLinkInfo(linkFilter, null);
Assert.assertEquals(200, response.getStatus());
List<LinkInfo> linkInfo = (List<LinkInfo>) response.getEntity();
Assert.assertNotNull(linkInfo);
});
在inTransaction方法中可以看到:
public <T> T inTransaction(Callable<T> call) {
Session session = this.sessionFactory.getCurrentSession();
Transaction transaction = session.beginTransaction();
try {
T result = call.call();
transaction.commit();
return result;
} catch (Exception var5) {
transaction.rollback();
if (var5 instanceof RuntimeException) {
throw (RuntimeException)var5;
} else {
throw new RuntimeException(var5);
}
}
}
将session手动开启了事务。
那在正常的业务逻辑中,是否也会存在上面的问题呢?
在currentSession中使用了(UnitOfWorkAwareProxyFactory包装+UnitOfWork),但不在多线程中。
答案不会:在REST自动bind的逻辑中,发现其已经帮我们打开了Transaction,等于上面的inTransaction
当然如果这个session是我自己Open的,然后人为绑定进context的自己是没有打开transaction,也是当然要报错的。
- 当前线程自动绑定了session,一定要开启事务了。因为UnitOfWorkAwareProxyFactory里要校验