Spring-Hibernate分库事务实现

    忙活了一个星期,看了N多Spring、Hibernate源码,查了N多资料,走了N多弯路,总算实现了分布式事务。回头看看,实现这个功能对自己的提升确实是巨大的。

    进入正题,官网之前的选库方案是在DataSource层面进行选库的,继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource来进行的选库,用SpringAOP做切面,在进入DAO方法之前进行选库(比如某个参数或者注解等)。事务用的是org.springframework.orm.hibernate3.HibernateTransactionManager。在一次操作单个数据库的情况下是没有问题的,但是在一次操作多个数据库时,比如有A、B两个库里面有一个相同的表,在A插入一条数据a,在B插入一条数据b,事务便会出问题,在提交的时候会全部提交到A库去,究其原因,应该是在事务开启时就打开了session,而一般打开事务时是在service层,没有进入DAO层。也就是说在打开事务时根本没有进行选库操作,而是进入的默认的数据库,导致事务提交时提交错了数据库。

    为了解决这个问题,在网上浏览了一番后,找到一个CobarClient的阿里出的分布式数据库访问层,找到其中的事务管理类查看源码 com.alibaba.cobar.client.transaction.MultipleDataSourcesTransactionManager ,研究了一番后,按照自己的思路实现,发现根本没有进入事务控制,数据直接就到数据库了。发现事情没有这么简单,CobarClient是根据DataSource进行的事务开启和关闭操作,和官网环境不同,是用的Spring-Hibernate,事务管理也是Spring的 tx:annotation-driven 标签,而且CobarClient几年前就不更新了,还不知道用的是那个Spring-Hibernate版本,所以只能另寻解决办法。

    在经历过上次失败后,突然想到官网用的最原始的org.springframework.orm.hibernate3.HibernateTransactionManager这个事务管理器是有效果的,只是最后commit的时候数据放错库了而已。那我写个类继承这个,重写相关方法不就行了,然后我就根据CobarClient的思想进行改造,在doBegin方法中把配置的全部的数据库连接开启了事务(事实上这样做很蠢,如果有100个配置,不就有100个事务了。。。),doCommit和doRollback方法循环调用的父类方法按照FILO规则来,然后又出问题了。因为SessionFactory是同一个,Spring会报一个同步错误,大意就是说你的session开启了一个以上事务,便会报错。

    这是才意识到,选库和分布式事务是要相互配合的。

    于是进行了一次比较大的修改,抛弃org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource选库方案,在初始化时进行SessionFactory初始化(为此又研究了Spring动态注册,以及org.springframework.beans.factory.FactoryBean.getObject(),一路艰辛啊。。),在选库时由选择DataSource改为选择SessionFactory,DAO层在调用时根据线程参数(java.lang.ThreadLocal)中的选库结果调用SessionFactory。事务方面研究了org.springframework.transaction.support.AbstractPlatformTransactionManager的源码,重点在于getTransaction方法发现doGetTransaction返回的结果并不重要(返回类型是Object),在框架中并没有其他使用它的地方,只有在必须实现的方法doBegin、doCommit、doRollback,以及挂起doSuspend,恢复挂起doResume方法isExistingTransaction是否已开启事务方法,以及doCleanupAfterCompletion事务结束后的销毁方法中用到。而这些方法都需要我们来实现,那么就有了一个想法。

    在doGetTransaction方法中标识事务是否开启:

    private ThreadLocal<Object> isTransaction = new ThreadLocal<Object>();   
  
    @Override
    protected Object doGetTransaction() throws TransactionException {
        Object o = new Object();
        isTransaction.set(o);
        return o;
    }

    事实上这里返回的object没有任何用处。而isTransaction只是标识事务开启而已,如果get出来为空则是没有开启事务。

    在doBegin中将TransactionDefinition存起来(根据源码调用,这个东西从头到尾都是一个)

 private ThreadLocal<TransactionDefinition> localTransactionDefinition = new ThreadLocal<TransactionDefinition>();
  @Override
  protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
      //将TransactionDefinition保存起来
      try {
          localTransactionDefinition.set(ObjectUtil.<TransactionDefinition>deepClone(definition));
      } catch (IOException e) {
          e.printStackTrace();
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
  }

    重点在于选库的时候添加事务(插一句 Assert 这个用来判断参数挺不错的):

    /**
     * DataSourceAspect中选库时开启事务操作,如之前(线程中)就以开启事务,则不做操作
     * @param sessionFactory 选中的库
     * @return 是否成功开启事务
     * @throws Exception
     */
    public boolean chooseDataSource(SessionFactory sessionFactory) throws Exception {
        Assert.notNull(sessionFactory, "Empty SessionFactory!");
        //check sessionFactory is correct
        if (!checkSessionFactory(sessionFactory)) return false;
        //check transaction is start
        if (isTransaction.get() == null) return false;
        //check sessionFactory is started transaction
        if (isInTransaction(sessionFactory)) return false;
        List<HibernateTransactionObject> localObj = getLocalObj();
        HibernateTransactionObject txObject = getHibernateTransactionObject(sessionFactory);
        doBegin(txObject);
        localObj.add(txObject);
        setLocalObj(localObj);
        return true;
    }
  
    private HibernateTransactionObject getHibernateTransactionObject(SessionFactory sessionFactory) {
        Assert.notNull(sessionFactory, "Empty SessionFactory!");
        HibernateTransactionObject txObject = new HibernateTransactionObject();
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
        txObject.setSessionFactory(sessionFactory);
        SessionHolder sessionHolder =
                (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        if (sessionHolder != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Found thread-bound Session [" +
                        SessionFactoryUtils.toString(sessionHolder.getSession()) + "] for Hibernate transaction");
            }
            txObject.setSessionHolder(sessionHolder);
        }
        DataSource dataSource = SessionFactoryUtils.getDataSource(sessionFactory);
        if (dataSource != null) {
            txObject.setDataSource(dataSource);
            ConnectionHolder conHolder = (ConnectionHolder)
                    TransactionSynchronizationManager.getResource(dataSource);
            txObject.setConnectionHolder(conHolder);
        }
        return txObject;
    }
 
    /**
     * 开启一个事务
     * @param txObject
     * @throws Exception
     */
    private void doBegin(HibernateTransactionObject txObject) throws Exception {
        TransactionDefinition definition = localTransactionDefinition.get();
        if (definition == null) throw new Exception("Transaction is not begin!");
        if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            throw new IllegalTransactionStateException(
                    "Pre-bound JDBC Connection found! HibernateTransactionManager does not support " +
                            "running within DataSourceTransactionManager if told to manage the DataSource itself. " +
                            "It is recommended to use a single HibernateTransactionManager for all transactions " +
                            "on a single DataSource, no matter whether Hibernate or JDBC access.");
        }
        Session session = null;
        try {
            if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
                Session newSession = txObject.getSessionFactory().openSession();
                if (logger.isDebugEnabled()) {
                    logger.debug("Opened new Session [" + SessionFactoryUtils.toString(newSession) +
                            "] for Hibernate transaction");
                }
                txObject.setSession(newSession);
            }
            session = txObject.getSessionHolder().getSession();
            if (isSameConnectionForEntireSession(session)) {
                // We're allowed to change the transaction settings of the JDBC Connection.
                if (logger.isDebugEnabled()) {
                    logger.debug(
                            "Preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]");
                }
                Connection con = session.connection();
                Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
                txObject.setPreviousIsolationLevel(previousIsolationLevel);
            }
            else {
                // Not allowed to change the transaction settings of the JDBC Connection.
                if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
                    // We should set a specific isolation level but are not allowed to...
                    throw new InvalidIsolationLevelException(
                            "HibernateTransactionManager is not allowed to support custom isolation levels: " +
                                    "make sure that its 'prepareConnection' flag is on (the default) and that the " +
                                    "Hibernate connection release mode is set to 'on_close' (SpringTransactionFactory's default). " +
                                    "Make sure that your LocalSessionFactoryBean actually uses SpringTransactionFactory: Your " +
                                    "Hibernate properties should *not* include a 'hibernate.transaction.factory_class' property!");
                }
                if (logger.isDebugEnabled()) {
                    logger.debug(
                            "Not preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]");
                }
            }
            if (definition.isReadOnly() && txObject.isNewSession()) {
                // Just set to MANUAL in case of a new Session for this transaction.
                session.setFlushMode(FlushMode.MANUAL);
            }
            if (!definition.isReadOnly() && !txObject.isNewSession()) {
                // We need AUTO or COMMIT for a non-read-only transaction.
                FlushMode flushMode = session.getFlushMode();
                if (flushMode.lessThan(FlushMode.COMMIT)) {
                    session.setFlushMode(FlushMode.AUTO);
                    txObject.getSessionHolder().setPreviousFlushMode(flushMode);
                }
            }
            Transaction hibTx;
            // Register transaction timeout.
            int timeout = determineTimeout(definition);
            if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1+
                // Applies to all statements, also to inserts, updates and deletes!
                hibTx = session.getTransaction();
                hibTx.setTimeout(timeout);
                hibTx.begin();
            }
            else {
                // Open a plain Hibernate transaction without specified timeout.
                hibTx = session.beginTransaction();
            }
            // Add the Hibernate transaction to the session holder.
            txObject.getSessionHolder().setTransaction(hibTx);
            // Register the Hibernate Session's JDBC Connection for the DataSource, if set.
            if (txObject.getDataSource() != null) {
                Connection con = session.connection();
                ConnectionHolder conHolder = new ConnectionHolder(con);
                if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                    conHolder.setTimeoutInSeconds(timeout);
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Exposing Hibernate transaction as JDBC transaction [" + con + "]");
                }
                TransactionSynchronizationManager.bindResource(txObject.getDataSource(), conHolder);
                txObject.setConnectionHolder(conHolder);
            }
            // Bind the session holder to the thread.
            if (txObject.isNewSessionHolder()) {
                TransactionSynchronizationManager.bindResource(txObject.getSessionFactory(), txObject.getSessionHolder());
            }
            txObject.getSessionHolder().setSynchronizedWithTransaction(true);
        }
        catch (Exception ex) {
            if (txObject.isNewSession()) {
                try {
                    if (session.getTransaction().isActive()) {
                        session.getTransaction().rollback();
                    }
                }
                catch (Throwable ex2) {
                    logger.debug("Could not rollback Session after failed transaction begin", ex);
                }
                finally {
                    SessionFactoryUtils.closeSession(session);
                }
            }
            throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex);
        }
    }
 
    /**
     * Hibernate transaction object, representing a SessionHolder.
     * Used as transaction object by HibernateTransactionManager.
     */
    public class HibernateTransactionObject extends JdbcTransactionObjectSupport {
        private SessionHolder sessionHolder;
        private boolean newSessionHolder;
        private boolean newSession;
        private DataSource dataSource;
        private SessionFactory sessionFactory;
        public void setSession(Session session) {
            this.sessionHolder = new SessionHolder(session);
            this.newSessionHolder = true;
            this.newSession = true;
        }
        public void setExistingSession(Session session) {
            this.sessionHolder = new SessionHolder(session);
            this.newSessionHolder = true;
            this.newSession = false;
        }
        public void setSessionHolder(SessionHolder sessionHolder) {
            this.sessionHolder = sessionHolder;
            this.newSessionHolder = false;
            this.newSession = false;
        }
        public SessionHolder getSessionHolder() {
            return this.sessionHolder;
        }
        public boolean isNewSessionHolder() {
            return this.newSessionHolder;
        }
        public boolean isNewSession() {
            return this.newSession;
        }
        public DataSource getDataSource() {
            return dataSource;
        }
        public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
        public SessionFactory getSessionFactory() {
            return sessionFactory;
        }
        public void setSessionFactory(SessionFactory sessionFactory) {
            this.sessionFactory = sessionFactory;
        }
        public boolean hasSpringManagedTransaction() {
            return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null);
        }
        public boolean hasHibernateManagedTransaction() {
            return (this.sessionHolder != null && this.sessionHolder.getSession().getTransaction().isActive());
        }
        public void setRollbackOnly() {
            this.sessionHolder.setRollbackOnly();
            if (hasConnectionHolder()) {
                getConnectionHolder().setRollbackOnly();
            }
        }
        public boolean isRollbackOnly() {
            return this.sessionHolder.isRollbackOnly() ||
                    (hasConnectionHolder() && getConnectionHolder().isRollbackOnly());
        }
        @Override
        public void flush() {
            try {
                this.sessionHolder.getSession().flush();
            }
            catch (HibernateException ex) {
                throw SessionFactoryUtils.convertHibernateAccessException(ex);
            }
        }
    }


    好吧,除了chooseDataSource方法之外  基本copy的org.springframework.orm.hibernate3.HibernateTransactionManager。HibernateTransactionObject 中添加了SessionFactory和DataSource两个属性。

至于doCommit和doRollback基本就是按照FILO循环操作了

 

    完成代码后试验,发现成功了。成功提交回滚,不会提交到一个库里面了。事务挂起没有实验成功,因为并没有进挂起方法。原因暂时不明,需要继续查资料。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值