对“springBoot+mybatis数据库读写分离”中两种方式的对比

方式一(配置两个SqlSessionFactory)

方式二(配置动态数据源——AbstractRoutingDataSource)

方式一优点为:

简单!

只要分好@Matser和@Slave的mapper,上层的代码跟未做读写分离前,没什么差别。当用manager层去掩盖掉底层的mapper差异,引用manager的调用方对读写分离无感。而“方式二 ”还需要在具体的方法上加上@TargetDataSource注解,比较繁琐,也容易出错(例如,如果方法里涉及数据库修改,而注解指定了从库,那就会报错)。

方式二优点为:

规避分布式事务!

举例子,如果在两种读写分离方式的demo代码上,增加一层service层TextService类,该类有个开启事务的方法,先用TextManager保存一行数据,再查询该行数据,会发生什么情况呢?

TextService类代码

package com.zidongxiangxi.practise.one.service;

import com.zidongxiangxi.practise.one.entity.Text;
import com.zidongxiangxi.practise.one.manager.TextManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TextService {
    @Autowired
    private TextManager textManager;

    @Transactional(rollbackFor = Exception.class)
    public Text saveAndGetText(String content) {
        Text text = new Text();
        text.setContent(content);
        textManager.saveText(text);
        return textManager.getById(text.getId());
    }
}

两种读写分离方式各跑一次,会发现“双SqlSessionFactory”的方式,返回的是null;“动态数据源”的方式能正常返回插入的数据。为什么会这样呢?这就需要去看看spring的事务实现源码和spring集成mybatis的源码~~


先了解一下spring事务的源码

demo例子中,采用的都是声明式事务,是通过TransactionInterceptor来实现。TransactionInterceptor类并没做什么事情,主要是调用它的父类TransactionAspectSupport的invokeWithinTransaction方法。所以我们先去看看TransactionAspectSupport.invokeWithinTrascation方法的代码。

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
			throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
			// 重点在这里,createTransactionIfNecessary方法的调用
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
			Object retVal = null;
			try {
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}
		// 这else分支暂时忽略,因为基本不该分支
		else {
			// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
			try {
				Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr,
						new TransactionCallback<Object>() {
							@Override
							public Object doInTransaction(TransactionStatus status) {
								TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
								try {
									return invocation.proceedWithInvocation();
								}
								catch (Throwable ex) {
									if (txAttr.rollbackOn(ex)) {
										if (ex instanceof RuntimeException) {
											throw (RuntimeException) ex;
										}
										else {
											throw new ThrowableHolderException(ex);
										}
									}
									else {
										// A normal return value: will lead to a commit.
										return new ThrowableHolder(ex);
									}
								}
								finally {
									cleanupTransactionInfo(txInfo);
								}
							}
						});

				if (result instanceof ThrowableHolder) {
					throw ((ThrowableHolder) result).getThrowable();
				}
				else {
					return result;
				}
			}
			catch (ThrowableHolderException ex) {
				throw ex.getCause();
			}
		}
	}

    protected TransactionInfo createTransactionIfNecessary(
			PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
		if (txAttr != null && txAttr.getName() == null) {
			txAttr = new DelegatingTransactionAttribute(txAttr) {
				@Override
				public String getName() {
					return joinpointIdentification;
				}
			};
		}

		TransactionStatus status = null;
		if (txAttr != null) {
			if (tm != null) {
				status = tm.getTransaction(txAttr);
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
							"] because no transaction manager has been configured");
				}
			}
		}
		return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
	}

重点分析createTransactionIfNecessary方法,它会判断是否存在事务,根据事务的传播属性。做出不同的处理,也是做了一层包装,核心是通过TransactionStatus来判断事务的属性。
在createTransactionIfNecessary方法中,通过持有的PlatformTransactionManager来获取TransactionStatus。
AbstractPlatformTransactionManager类实现了PlatformTransactionManager接口,实现了getTransaction方法

 public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
     //这里其实主要就是调用PlatformTransactionManager的getTransactionf方法来获取TransactionStatus来开启一个事务:
        Object transaction = doGetTransaction();
        boolean debugEnabled = logger.isDebugEnabled();
        if (definition == null) {
            definition = new DefaultTransactionDefinition();
        }
     //这个判断很重要,是否已经存在的一个transaction
        if (isExistingTransaction(transaction)) {
       //如果是存在的将进行一些处理,如新的事务传播是REQUIRED_NEWS,那就需要挂起原来的事务,重新开启一个事务
            // Existing transaction found -> check propagation behavior to find out how to behave.
            return handleExistingTransaction(definition, transaction, debugEnabled);
        }

        if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
            throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
        }

        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException(
                    "No existing transaction found for transaction marked with propagation 'mandatory'");
        }
     //如果是PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED这三种类型将开启一个新的事务
        else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            SuspendedResourcesHolder suspendedResources = suspend(null);
            if (debugEnabled) {
                logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
            }
            try {
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
          //开启新事物
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
            catch (RuntimeException ex) {
                resume(null, suspendedResources);
                throw ex;
            }
            catch (Error err) {
                resume(null, suspendedResources);
                throw err;
            }
        }
        else {
            // Create "empty" transaction: no actual transaction, but potentially synchronization.
            if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
                logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                        "isolation level will effectively be ignored: " + definition);
            }
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
        }
    }

getTransaction方法的重点在doGetTransaction、isExistingTransaction、handleExistingTransaction和doBegin方法的调用。
doGetTransaction尝试获取当前存在的事务信息;
isExistingTransaction根据doGetTransaction的结果判断当前是否存在事务(主要依据就是ConnectionHolder是否非空且有效);
当事务存在,则调用handleExistingTransaction方法,根据事务传播级别决定行为;
当存在事务不存在,且需要开启事务,就会用doBegin开启事务。

AbstractPlatformTransactionManager并没有给出doGetTransaction的具体实现,而是由子类实现。可以看下DataSourceTransactionManager类的实现代码,

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManager, InitializingBean {
    private DataSource dataSource;

    @Override
  //这段代码中主要是根据this.dataSource来获取ConnectionHolder,这个ConnectionHolder是放在TransactionSynchronizationManager的ThreadLocal中持有的,如果是第一次来获取,肯定得到是null。
    protected Object doGetTransaction() {
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
     //这一行代码中TransactionSynchronizationManager很重要,是对connection的核心获取、持有、删除等
        ConnectionHolder conHolder =
                (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);
     //这里不论获取到或者获取不到都将此设置newConnectionHolder为false
        txObject.setConnectionHolder(conHolder, false);
        return txObject;
    }

	@Override
    protected boolean isExistingTransaction(Object transaction) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
     //如果是第一次开启事务这里必然是false,因为ConnectionHolder是null。
        return (txObject.getConnectionHolder() != null && txObject.getConnectionHolder().isTransactionActive());
    }

	@Override
	protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;

		try {
			if (txObject.getConnectionHolder() == null ||
					txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
				Connection newCon = this.dataSource.getConnection();
				if (logger.isDebugEnabled()) {
					logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
				}
				// 在这里,new了一个ConnectionHolder对象,并且标记为“新建”(第二个入参,true)
				txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
			}

			txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
			con = txObject.getConnectionHolder().getConnection();

			Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
			txObject.setPreviousIsolationLevel(previousIsolationLevel);

			// 如果连接本身是设置自动提交的,现在开启了事务,需要把自动提交设置为false,并记录需要恢复自动提交。在事务提交之后,要把连接恢复为自动提交
			if (con.getAutoCommit()) {
				txObject.setMustRestoreAutoCommit(true);
				if (logger.isDebugEnabled()) {
					logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
				}
				con.setAutoCommit(false);
			}

			prepareTransactionalConnection(con, definition);
			txObject.getConnectionHolder().setTransactionActive(true);

			int timeout = determineTimeout(definition);
			if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
				txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
			}

			// 如果是新建的,需要把ConnectionHolder和当前的datasource关联起来
			if (txObject.isNewConnectionHolder()) {
				TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
			}
		}

		catch (Throwable ex) {
			if (txObject.isNewConnectionHolder()) {
				DataSourceUtils.releaseConnection(con, this.dataSource);
				txObject.setConnectionHolder(null, false);
			}
			throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
		}
	}

在doBegin方法中,新建完ConnectionHolder后,会把ConnectionHolder和当前的datasource绑定起来(线程范围),TransactionSynchronizationManager中有一个ThreadLocal的线程变量,用于记录当前线程里,datasource和ConnectionHolder的映射关系。

到这里,就可发现一个关键点!
doGetTrascation里的“TransactionSynchronizationManager.getResource(this.dataSource);”,用于判断事务是否存在。
doBegin里的“TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());”,开启事务会绑定ConnectionHolder。

看到这里,很容易有一种错觉,“双SqlSessionFactory”方式之所以执行TextService.saveAndGetText方法的返回值与期望不同,就是这点导致的:
TextManager.saveText方法时,datasource是主库,获取了主库的链接执行保存;TextManager.getById方法时,datasource是从库,“TransactionSynchronizationManager.getResource(this.dataSource);”方法返回的是null,导致重新开启事务,获取到了从库的连接。而这个时候,主库的事务还没有提交,从从库查询,必然查询不到刚刚插入的记录

思路是对的,但是并不是在开启事务的时候出错,而是在执行数据库操作的时候!!

我改一下“双SqlSessionFactory”方式的TextManager代码:

@Service
public class TextManager {
    @Autowired
    private TextMapper textMapper;

    @Autowired
    private TextSlaveMapper textSlaveMapper;

    @Transactional(rollbackFor = Exception.class)
    public int saveText(Text text) {
        textMapper.insertSelective(text);
        return text.getId();
    }

	// 增加了 @Transactional注解,支持开启事务
    @Transactional(rollbackFor = Exception.class, readOnly = true, propagation = Propagation.SUPPORTS)
    public Text getById(Integer id) {
        return textSlaveMapper.getById(id);
    }
}

增加一个单元测试:

public class TextServiceTest extends BaseTest {
    @Autowired
    private TextService textService;

    @Test
    public void testSaveAndGet() {
        Text text = textService.saveAndGetText("777");
        Assert.assertNotNull(text);
    }

运行单元测试,在DataSourceTransactionManager类的doGetTransaction打个断点:
打断点
会发现,一共进来doGetTransaction方法3次,除了第一次是空的,剩下两次获取ConnoctionHolder都不为空
因为在“双SqlSessionFactory”方式的代码里,只配置了一个事务管理器,事务管理器使用的是主库的dataSource,所以开启事务,都是主库的连接。

那为什么TextManager.getById还会获取不到TextManager.saveText保存的内容呢?
答案在spring集成mybatis的源码里。
有两个关键点,sqlSession的获取数据库连接的获取
SqlSessionUtils类的getSqlSession方法

 public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
	// 跟开启事务时的代码很像,只是变成用SqlSessionFactory对象作为key
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }
	// 如果没有已经存在的session,则需要新开一个,并于sessionFactory绑定
    session = sessionFactory.openSession(executorType);
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
    return session;
  }

从getSqlSession方法可以看出,同一个线程里,每个SqlSessionFactory只会打开一个session。“双SqlSessionFactory”的情况下,会各自打开一个session

获取数据库连接,是在BaseExecutor的getConnection方法

protected Connection getConnection(Log statementLog) throws SQLException {
	// 这里的transaction类型是SpringManagedTransaction
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

转去看看SpringManagedTransaction的源码

 @Override
  public Connection getConnection() throws SQLException {
  	// 连接为空,就打开连接,一个事务中只调用一次openConnection方法
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }
  
  private void openConnection() throws SQLException {
  	// 重点!!去看一下DataSourceUtils的源码,就能在mybatis里怎么获取数据库连接的
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }

关注DataSourceUtils的getConnection和doGetConnection方法

// 就只调用了doGetConnection方法,封装了sql异常
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
		try {
			return doGetConnection(dataSource);
		}
		catch (SQLException ex) {
			throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
		}
	}
// 在真正去获取连接
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
		Assert.notNull(dataSource, "No DataSource specified");
		// 看到了曙光!!!联想doBegin中调用TransactionSynchronizationManager.bindResource方法,说明这里就是尝试去拿事务开启时设置的连接!!!
		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
			conHolder.requested();
			if (!conHolder.hasConnection()) {
				logger.debug("Fetching resumed JDBC Connection from DataSource");
				conHolder.setConnection(dataSource.getConnection());
			}
			return conHolder.getConnection();
		}
		// Else we either got no holder or an empty thread-bound holder here.

		// 如果通过TransactionSynchronizationManager拿不到ConnectionHolder,就会直接从dataSource拿一个连接。这就是TextManager.getById返回null的原因!!!!
		logger.debug("Fetching JDBC Connection from DataSource");
		Connection con = dataSource.getConnection();

		if (TransactionSynchronizationManager.isSynchronizationActive()) {
			logger.debug("Registering transaction synchronization for JDBC Connection");
			// Use same Connection for further JDBC actions within the transaction.
			// Thread-bound object will get removed by synchronization at transaction completion.
			ConnectionHolder holderToUse = conHolder;
			if (holderToUse == null) {
				holderToUse = new ConnectionHolder(con);
			}
			else {
				holderToUse.setConnection(con);
			}
			holderToUse.requested();
			TransactionSynchronizationManager.registerSynchronization(
					new ConnectionSynchronization(holderToUse, dataSource));
			holderToUse.setSynchronizedWithTransaction(true);
			if (holderToUse != conHolder) {
				TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
			}
		}

		return con;
	}

结论:
当使用“双SqlSessionFactory”的方式来实现读写分离,在同一个事务里,主库的SqlSessionFactory和从库的SqlSessionFactory会各自产生一个SqlSession。执行sql语句的时候,也是用各自dataSource去ThreadLocal里拿数据库连接,如果ThreadLocal拿不到,就会直接从dataSource拿。
在TextService.saveAndGetText方法中,TextManager.saveText从ThreadLocal里拿到了主库的连接,用该连接执行了插入操作,但未提交;TextManager.getById则从ThreadLocal拿不到任何连接,直接用从库的dataSource获取新的连接,执行查询操作。这个时候,由于主库修改未提交,所以不会同步到从库,所以查询不到任何信息。即使主库和从库实际上指向的是同一个数据库实例,TextManager.getById也会查不到数据,除非事务级别是“读未提交”

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值