在(一)中直接写了代码, 能力有限 , 只能对(一)中的代码和遇到的坑做一些简单的阐释
1. 之所以没有使用aop是因为读写分离不是在一开始就在在我们的规划里面 , 方法名没有按照一定的规则写,所以不好拦截,最后选择了mybatis的插件拦截. 关于插件拦截的学习可以参考以下博客 :
https://www.jianshu.com/p/14bf6a4ca7ef
2. 最开始的配置中并没有使用到自定义DataSourceTransactionManager, 然后发现 , 不带有事务的增删改查 , 完全按照读走从库 , 增删改走主库的形式进行, 没有问题. 但是一旦涉及到事务 ,对于主从的选择就会出现问题 , 按照之前的设计, 带有事务的情况应该完全走主库才对. 可是实际情况遇到事务完全随机的来.
进行调试发现, 一个请求中 , 在调用事务方法之前 , 进行过查询(走过从库的), 在事务开启之后依然走的是从库; 而在事务开启之前没有调用查询(走从库的)使用的是默认的数据源(主库)
进一步查明原因:
在走过mybatis的插件之后, 会调用DataSourceUtils.class的getConnection方法, 部分代码如下 ,主要功能是获取链接的时候 , 根据上线文中存储的数据源的key(在插件拦截中已经选好了应该使用的数据源的key放在上线文中), 拿到相应的数据源; 而当事务存在并且事务中已经存在ConnectionHolder的情况下(开启事务的时候也会走一遍getConnection, 这时候会拿到上下文中已经存在的key去获取数据源, 并存在ConnectionHolder中), 就不会重新进行这些操作, 直接使用ConnectionHolder中的链接. 所以就算插件拦截中重新设置了上下文中数据源的key, 也没有通过key去重新获取数据源这一步了. 所以事务的时候, 设置数据源就失效了.
org.springframework.jdbc.datasource.DataSourceUtils
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
} catch (SQLException var2) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", var2);
} catch (IllegalStateException var3) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + var3.getMessage());
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
//在没有事务的情况下,conHolder是null,会进行fetchConnection方法,这个方法也贴在了下面,会调用dataSource的getConnection方法.- 通过上下文中的key拿到对应的数据源.
//在有事务的情况下,如果之前存在数据源(conHolder不是null)不会重新动态获取数据源了
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
}
private static Connection fetchConnection(DataSource dataSource) throws SQLException {
//这里的dataSource就是我们继承的AbstractRoutingDataSource类,并且重写了determineCurrentLookupKey方法的DataSourceSelector类
Connection con = dataSource.getConnection();
if (con == null) {
throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
} else {
return con;
}
}
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//这里调用的是我们重写的方法,从上下文中获取key值
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
所以在开启事务的时候, 解决问题的关键点, 在于开启事务的时候, 为上下文中的数据源key值重新赋值. 我们在(一)中的步骤5就是在处理这种情况
package com.hlz.dao.config;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import javax.sql.DataSource;
public class HlzDataSourceTransactionManager extends DataSourceTransactionManager {
public HlzDataSourceTransactionManager(DataSource dataSource) {
super(dataSource);
}
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DynamicDataSourceHolder.setDataSourceType(DynamicDataSourceHolder.DB_MASTER);
super.doBegin(transaction, definition);
}
}
3. 我们做的主库和从库, 使用的方式是: 主从复制binlog复制的方式, 像我们做的这种读写分离, 还是会有一些小小的问题, 设想一个场景, [在没有事务的情况下, 先插入, 再查询], 插入时候会走主库, 而查询走的是从库. 从插入到查询程序走的时间很短, 快到从库还没来得及从主库复制数据, 所以在写这些场景的时候要格外注意, 加上事务, 或者用其他的方式实现默认走主库.