实践-DW框架中的hibernate多线程读写问题

一、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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jbn1sHM0-1657175890758)(D:\04专项任务\yanjiao\md-pic\image-20211221175919656.png)]

事件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iPaxVMzy-1657175890763)(D:\04专项任务\yanjiao\md-pic\image-20211221193140447.png)]

@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里要校验
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值