简单!
只要分好@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的源码~~
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
重点分析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也会查不到数据,除非事务级别是“读未提交”。