3种方式实现多数据源控制/切换、实现读写分离;演示借助AbstractRoutingDataSource实现多数据源的动态切换代码【享学Spring】

2019-09-03 16:05:24 浏览数 (1)

前言

什么时候一个Java工程里需要同时控制(连接)多个数据源呢?我认为主要有如下两种情况:

  1. 业务需要。比如项目里要实现两个DB的双写/数据迁移,或者微服务边界划分不清使得一个工程直连了多个DB
  2. 读写分离。但凡稍微大型一点的网站,为了提升DB的吞吐量和性能以及高可用性,数据库一般都会采用集群部署(1个Master N个Slave模式)。

作为技术宅的我们应该知道,不管是什么业务原因导致我们同一个工程内需要控制多个数据源,我们心里应该明确:在技术实施层面上都是一样的。 下面为了方便分析,以典型的读写分离为例作为讲解~

读写分离:主库master可读可写,从库slave是readOnly的且可以有多个。

为何数据库需要读写分离?

随着网站的业务不断扩展,数据不断增加,用户越来越多,数据库的压力也就越来越大,采用传统的方式,比如:提升数据库机器配置或者SQL的优化基本已达不到要求,这个时候可以横向扩展:采用读写分离的策略来改变现状。

我们现在应用都以分布式形式部署,所以99%情况下的性能瓶颈都发生在DB身上而非应用本身(因为应用都会负载均衡),这也是我们大力反对连表查询反对写复杂的SQL语句的最根本原因。

采用读写分离技术的目标:有效减轻Master库的压力,又可以把用户查询数据的请求分发到不同的Slave库(多个Slave之间也可实现负载均衡),从而保证整个系统的健壮性

实现多数据源管理的3种方式

从单一数据源到多数据源是有一个演进过程的:

单数据源的场景,一般的Web项目工程这样配置进行处理,就已经比较能够满足我们的业务需求(此处不考虑请求压力大、需要读写分离的情况)

多数据源多SessionFactory这样的场景。这其实就是在Dao层以编程的方式实现的对多数据源的控制。 到这里业务层面已经有多数据源的需求了,如上图我把它定位 多数据源**静态**切换说白了:就是定义两个数据源,想用哪个用哪个呗~

说明:因为我们控制是多数据源DataSource,而并不用关心到底是用哪种方式去使用,比如源生JDBCMyBatisHibernate等上层的使用方法上都是雷同的。

本文以JdbcTemplate操作数据源为例,场景以读写分离为例(其它case仿照着实施就ok了)

环境准备

准备的环境本着:旨在为了说明问题,一切从简的原则搭建。

jdbc.properties属性配置文件如下:

代码语言:javascript复制
## 主库master配置
datasource.drivername=com.mysql.jdbc.Driver
datasource.username=root
datasource.password=root
datasource.url=jdbc:mysql://localhost:3306/jedi

## 从库slave配置
datasource.slave.drivername=com.mysql.jdbc.Driver
datasource.slave.username=root
datasource.slave.password=root
datasource.slave.url=jdbc:mysql://localhost:3306/jedi_slave

此处两个库jedijedi_slave分别模拟两个数据源。两个库内容一模一样,内部都只有一张表user表,表内只有一条记录:

方式一:硬编码(静态切换)

最原始的方式也就是这种方式,静态控制多数据源。在代码层面直接控制(也就是在在编写代码时:就指定好要去操作哪个DB)。这种方式我把它叫做“静态切换”。给出简单示例如下:

JdbcConfig.java配置文件如下:

代码语言:javascript复制
@EnableTransactionManagement
@Configuration
@PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {

    @Value("${datasource.username}")
    private String userName;
    @Value("${datasource.password}")
    private String password;
    @Value("${datasource.url}")
    private String url;

    // 从库配置
    @Value("${datasource.slave.username}")
    private String slaveUserName;
    @Value("${datasource.slave.password}")
    private String slavePassword;
    @Value("${datasource.slave.url}")
    private String slaveUrl;


    ////////////////=====配置好两个数据源:
    @Primary
    @Bean
    public DataSource masterDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(userName);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }

    @Bean
    public DataSource slaveDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(slaveUserName);
        dataSource.setPassword(slavePassword);
        dataSource.setURL(slaveUrl);
        return dataSource;
    }

    // 手动配置好两个JdbcTemplate  分别用于操作
    @Primary
    @Bean
    public JdbcTemplate masterJdbcTemplate() {
        return new JdbcTemplate(masterDataSource());
    }

    @Bean
    public JdbcTemplate slaveJdbcTemplate() {
        return new JdbcTemplate(slaveDataSource());
    }

    // 事务管理器自己具体配置(如果是真的主从模式,从库可以不用事务管理器  只需要配置主库的即可)
    @Primary
    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(masterDataSource());
        dataSourceTransactionManager.setEnforceReadOnly(true); // 让事务管理器进行只读事务层面上的优化  建议开启
        return dataSourceTransactionManager;
    }

    // 指定注解使用的事务管理器
    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return transactionManager();
    }
}

如上配置,我们向容器内准备了两个JdbcTemplate,分别绑定着不同两个数据源。这样在代码中,我们想用哪个数据源的的链接connection执行sql时,使用对应的JdbcTemplate即可。它的优点就是配置简单、好理解,使用上也非常灵活

就是因为它过于灵活了,所以变得无法控制。这种方式有个最大的缺点:代码侵入性极强,它完全是用硬编码的方式实现的控制,不便于管理(比如我想统一在代码层面上做load balance就几乎不能实现了)

为了解决上面的缺点,下面就是要Spring的相关抽象隆重登场了:

Spring抽象的AbstractDataSource

上面方式其实单单只是借用了Spring的Bean来管理多数据源,严格意义上讲这和Spring对多数据源的抽象没毛关系。 下面介绍借助Spring对多数据源的抽象支持,来优雅的处理多数据源问题

首先看看AbstractDataSource本身:

代码语言:javascript复制
// @since 07.05.2003   它是个抽象类:实现了DataSource一些无关紧要的方法… 
public abstract class AbstractDataSource implements DataSource {
	... // 实现接口javax.sql.DataSource一些无关紧要的方法
}

它的继承图谱:

两个主要分支:和DriverManager/Driver有关的AbstractDriverBasedDataSource和数据源路由有关的AbstractRoutingDataSource

AbstractDriverBasedDataSource

正如类名所代表,这个抽象类的子类都是基于Driver/DriverManager来获取Connection对象的。

代码语言:javascript复制
// @since 2.5.5   此抽象类出现得较晚  但是木有关系
public abstract class AbstractDriverBasedDataSource extends AbstractDataSource {

	// 需要准备url、username、password等参数  属于更偏底层的处理方式~~~
	@Nullable
	private String url;
	@Nullable
	private String username;
	@Nullable
	private String password;
	@Nullable
	private String catalog;
	@Nullable
	private String schema;
	@Nullable
	private Properties connectionProperties;

	... // 省略所有的get/set方法

	// 此处getConnectionFromDriver方法是它的特色~~~
	@Override
	public Connection getConnection() throws SQLException {
		return getConnectionFromDriver(getUsername(), getPassword());
	}
	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return getConnectionFromDriver(username, password);
	}
	// 从Driver里获取到链接~~~~
	protected Connection getConnectionFromDriver(@Nullable String username, @Nullable String password) throws SQLException {
		Properties mergedProps = new Properties();
		Properties connProps = getConnectionProperties();
		// 可以看到  就连username  password其实都是可以放在Properties属性文件里的
		if (connProps != null) {
			mergedProps.putAll(connProps);
		}
		if (username != null) {
			mergedProps.setProperty("user", username);
		}
		if (password != null) {
			mergedProps.setProperty("password", password);
		}

		// 根据Properties属性文件 从Driver里获取链接   抽象方法,由子类去实现
		Connection con = getConnectionFromDriver(mergedProps);
		if (this.catalog != null) {
			con.setCatalog(this.catalog);
		}
		if (this.schema != null) {
			con.setSchema(this.schema);
		}
		return con;
	}

	// 待子类实现
	protected abstract Connection getConnectionFromDriver(Properties props) throws SQLException;
}

它没有实现太多内容,主要对userName、password等属性和Properties进行合并处理,最后提供抽象法方法getConnectionFromDriver交由子类去实现。

DriverManagerDataSource

java.sql.DriverManager,通过它来获取到链接。

代码语言:javascript复制
public class DriverManagerDataSource extends AbstractDriverBasedDataSource {
	...
	// 指定驱动
	// 参考:java.sql.DriverManager#registerDriver(java.sql.Driver)
	public void setDriverClassName(String driverClassName) {
		...
		Class.forName(driverClassNameToUse, true, ClassUtils.getDefaultClassLoader());
	}

	// 复写父类方法:从Driver里拿到链接
	// 本类使用的是从事务管理器里获取链接:DriverManager.getConnection(url, props);
	@Override
	protected Connection getConnectionFromDriver(Properties props) throws SQLException {
		String url = getUrl();
		return getConnectionFromDriverManager(url, props);
	}
	protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException {
		return DriverManager.getConnection(url, props);
	}
}

关于DriverManager这个驱动管理器类,可能大部分新一代的程序员小伙伴感觉非常陌生了,因为这种底层API我们现在几乎都见不到了。 最初我们获取链接都是直接从DriverManager里来的,形如:

代码语言:javascript复制
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        String username = "root";
        String password = "root";
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jedi", username, password);

        System.out.println(connection);
    }

这几句代码也就是JDBC非常经典(著名)的获取链接的**四大步骤**,由于现在直接这么使用的情况实在太少了太少了,所以很多小伙伴不熟悉也是情有可原(但我相信只要一提起都还是有点映象的吧)

SingleConnectionDataSource

它是DriverManagerDataSource的子类,并且还实现了SmartDataSource。它的特点是:能够保证每次调用getConnection(),获取都是相同的Connection所以它不能使用在多线程,是线程不安全的~~~

它的Connection是调用父类的,所以也是从DriverManager里来的

SimpleDriverDataSource

java.sql.Driver强相关。它直接继承自AbstractDriverBasedDataSource。它表示一个简单的数据源,每次获取Connection时,会重新建立一个Connection。通过Driver来获取Connection对象。 获取代码如下:

代码语言:javascript复制
Connection connection = driver.connect(url, props);

以上实现类基于AbstractDriverBasedDataSource的实现方式,**当然也能做为管理多数据源的方案。**这就是我想说的方式二,具体详细代码省略。

注意,注意,注意:在性能要求不高的情况,可以试试直接使用它们玩玩。否则请务必使用连接池技术提升性能~ 我一般只会把此种方式放在测试上~~




上面已经介绍了管理多数据源的两种方式,但都有弊端,真正使用起来也稍显麻烦。 接下来介绍的这种方式是使用最广泛也是本文的主菜~~~

方式三:AbstractRoutingDataSource动态切换数据源

在基于三层的后端架构中,操作数据库的是Dao层。比如现在我们一般使用SSM框架。若我们像上面两种方式操作多数据源,首先最大的缺点就是代码入侵性强、便管理。

此处我们还有一个方法,也就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。比如我们希望对于读写分离或者其他的数据同步的业务场景。

如上图,使用AbstractRoutingDataSource的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource。这样的编写方式比较好,至于其中的实现原理是下面会分析的重点

AbstractRoutingDataSource 是个抽象类,具体实现需要调用者书写实现类的。

Spring 2.0.1引入了一个AbstractRoutingDataSource ,我相信这值得关注。

代码语言:javascript复制
// @since 2.0.1   注意起始版本(该版本2006见发布)
// 并且实现了InitializingBean 接口
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
	
	// 根据key 缓存下来其对应的DataSource~~~~
	@Nullable
	private Map<Object, Object> targetDataSources;
	@Nullable
	private Object defaultTargetDataSource; // 默认数据源
	// 如果找不到当前查找键的特定数据源,请指定是否对默认数据源应用宽限回退。
	private boolean lenientFallback = true;
	
	// DataSourceLookup为一个函数式接口  只有一个方法DataSource getDataSource(String dataSourceName)
	// 此处使用的默认实现为JNDI
	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
	@Nullable
	private Map<Object, DataSource> resolvedDataSources;
	@Nullable
	private DataSource resolvedDefaultDataSource;
	... // 省略所有的set方法


	@Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());

		// 遍历设置进来的目标数据源们~~~~
		this.targetDataSources.forEach((key, value) -> {
			Object lookupKey = resolveSpecifiedLookupKey(key);
			DataSource dataSource = resolveSpecifiedDataSource(value);
			// 把已经解决好的缓存起来(注意key和value和上有可能就是不同的了)
			// 注意:key可以是个Object  而不一定只能是String类型
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

	// 子类IsolationLevelDataSourceRouter有复写此方法
	// 绝大多数情况下,我们会直接使用DataSource
	protected Object resolveSpecifiedLookupKey(Object lookupKey) {
		return lookupKey;
	}
	// 此处兼容String类型,若是string就使用dataSourceLookup去查找(默认是JNDI)
	protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
		if (dataSource instanceof DataSource) {
			return (DataSource) dataSource;
		} else if (dataSource instanceof String) {
			return this.dataSourceLookup.getDataSource((String) dataSource);
		} else {
			throw new IllegalArgumentException(
					"Illegal data source value - only [javax.sql.DataSource] and String supported: "   dataSource);
		}
	}

	/////////////////////链接数据库
	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}
	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return determineTargetDataSource().getConnection(username, password);
	}
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);

		// 若根据key没有找到dataSource,并且lenientFallback=true或者lookupKey == null  那就回滚到使用默认的数据源
		// 备注:此处可以看出key=null和this.lenientFallback =true有一样的效果
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key ["   lookupKey   "]");
		}
		return dataSource;
	}
	
	// 子类必须实现的抽象方法:提供key即可~~~~
	@Nullable
	protected abstract Object determineCurrentLookupKey();



	@Override
	@SuppressWarnings("unchecked")
	public <T> T unwrap(Class<T> iface) throws SQLException {
		if (iface.isInstance(this)) {
			return (T) this;
		}
		return determineTargetDataSource().unwrap(iface);
	}
	@Override
	public boolean isWrapperFor(Class<?> iface) throws SQLException {
		return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
	}
}

具体选择哪个数据源是由determineCurrentLookupKey()方法的返回值决定的,该方法需要我们继承AbstractRoutingDataSource来重写。

通过上面源码展示,我们也可以看出AbstractRoutingDataSource切换数据源的源码不多,并且非常简单,相信建立在源码的基础上再去应用,会让你感觉到简直不要太easy。


使用AbstractRoutingDataSource动态切换数据源示例代码

这种功能属于技术模块,完全可以独立于业务模块之外开发出一个类似中间件的组件形式存在。下面结合我具体的使用案例,给大家贡献参考如下参考代码:


1、定义一个常量,表示所有的DataSource的key。(建议用这样的全局常量维护key,当然这不是必须的)

代码语言:javascript复制
public abstract class DynamicDataSourceId {

    public static final String MASTER = "master";
    public static final String SLAVE1 = "slave1";
    public static final String SLAVE2 = "slave2";
    //... 可以继续无线扩展


    // 保存着有效的(调用者设置进来的)所有的DATA_SOURCE_IDS
    public static final List<String> DATA_SOURCE_IDS = new ArrayList();

    public static boolean containsDataSourceId(final String dataSourceId) {
        return dataSourceId != null && !dataSourceId.trim().isEmpty() ? DATA_SOURCE_IDS.contains(dataSourceId) : false;
    }
}

2、定义一个Holder,可以把数据源名称和当前线程绑定,提升易用性(当然也不是必须的)

代码语言:javascript复制
public abstract class DynamicDataSourceContextHolder {

    //每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 注意:使用静态方法setDataSourceId设置当前线程需要使用的数据源id(和当前线程绑定)
     */
    public static void setDataSourceId(final String dataSourceId) {
        CONTEXT_HOLDER.set(dataSourceId);
    }

    /**
     * 获取当前线程使用的数据源id
     */
    public static String getDataSourceId() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空当前线程使用的数据源id
     */
    public static void clearDataSourceId() {
        CONTEXT_HOLDER.remove();
    }
}

3、实现抽象类AbstractRoutingDataSource定义自己的动态数据源DataSource类

代码语言:javascript复制
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() { //所有的请求都会走此处,所以没有切换的时候,不要输出日志吧
        String dataSourceId = DynamicDataSourceContextHolder.getDataSourceId();
        if (dataSourceId != null) { //有指定切换数据源切换的时候,才给输出日志 并且也只给输出成debug级别的 否则日志太多了
            log.debug("线程[{}],此时切换到的数据源为:{}", Thread.currentThread().getId(), dataSourceId);
        }
        return dataSourceId;
    }

}

就这样三步,我们带有动态切换能力的数据源类DynamicDataSource就完成了。

接下来就可以这么直接使用在项目里了: Java配置文件JdbcConfig.java如下:

代码语言:javascript复制
@EnableTransactionManagement
@Configuration
@PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {

    @Value("${datasource.username}")
    private String userName;
    @Value("${datasource.password}")
    private String password;
    @Value("${datasource.url}")
    private String url;

    // 从库配置
    @Value("${datasource.slave.username}")
    private String slaveUserName;
    @Value("${datasource.slave.password}")
    private String slavePassword;
    @Value("${datasource.slave.url}")
    private String slaveUrl;


    ////////////////=====配置好两个数据源:
    @Bean
    public DataSource masterDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(userName);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }

    @Bean
    public DataSource slaveDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(slaveUserName);
        dataSource.setPassword(slavePassword);
        dataSource.setURL(slaveUrl);
        return dataSource;
    }

    // 定义动态数据源
    @Primary
    @Bean
    public DataSource dataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        // 初始化值必须设置进去  且给一个默认值
        dataSource.setTargetDataSources(new HashMap<Object, Object>() {{
            put(DynamicDataSourceId.MASTER, masterDataSource());
            put(DynamicDataSourceId.SLAVE1, slaveDataSource());

            //顺手注册上去,方便后续的判断
            DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.MASTER);
            DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.SLAVE1);
        }});

        dataSource.setDefaultTargetDataSource(masterDataSource());
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource());
        dataSourceTransactionManager.setEnforceReadOnly(true); // 让事务管理器进行只读事务层面上的优化  建议开启
        return dataSourceTransactionManager;
    }

    // 指定注解使用的事务管理器
    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return transactionManager();
    }
}

可以和上面配置对比,这里并不需要什么都配置两份了,而是都只需要配置一份即可,其余的交给动态去切换吧。

单元测试如下:

代码语言:javascript复制
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JdbcConfig.class})
public class TestSpringBean {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private DataSource dataSource;

    @Test
    public void test1() throws SQLException {
        System.out.println(jdbcTemplate.getDataSource() == dataSource); //true
        System.out.println(DataSourceUtils.getConnection(jdbcTemplate.getDataSource())); //com.mysql.jdbc.JDBC4Connection@17503f6b
        
        DynamicDataSourceContextHolder.setDataSourceId(DynamicDataSourceId.SLAVE1); 
        
        System.out.println(jdbcTemplate.getDataSource() == dataSource); //true
        System.out.println(DataSourceUtils.getConnection(jdbcTemplate.getDataSource())); //com.mysql.jdbc.JDBC4Connection@20bd8be5


        // 完成操作后  最好把数据源再set回去  否则可能会对该线程后续再使用JdbcTemplate的时候造成影响
        //DynamicDataSourceContextHolder.setDataSourceId(DynamicDataSourceId.MASTER);
    }
}

从上结果,此处有几个细节需要注意:

  1. 数据源DataSource永远没变(用的是我们配置的DynamicDataSource
  2. DynamicDataSourceContextHolder.setDataSourceId(DynamicDataSourceId.SLAVE1);JdbcTemplate绑定的数据源肯定是不会变的。只是内部去获取链接的时候,从所属数据源变化了
  3. 用完之后记得还原现场、清理线程(切换数据源源用完之后记得切回来)

附:第一次链接所属的数据源截图:

切换后为:

理解了原理之后。其实我们的masterDataSource以及slaveDataSource是完全没有比较放到Spring容器内的,减轻Spring容器容器的负担嘛。使用下面配置效果一样(这样做更能体现对多数据源的理解,逼格更高~~~):

代码语言:javascript复制
@EnableTransactionManagement
@Configuration
@PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {
	
	...
	
    // 定义动态数据源
    @Bean
    public DataSource dataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        final DataSource masterDataSource = masterDataSource();
        final DataSource slaveDataSource = slaveDataSource();
        // 初始化值必须设置进去  且给一个默认值
        dataSource.setTargetDataSources(new HashMap<Object, Object>() {{
            put(DynamicDataSourceId.MASTER, masterDataSource);
            put(DynamicDataSourceId.SLAVE1, slaveDataSource);

            //顺手注册上去,方便后续的判断
            DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.MASTER);
            DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.SLAVE1);
        }});

        dataSource.setDefaultTargetDataSource(masterDataSource);
        return dataSource;
    }

    private DataSource masterDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(userName);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }
    private DataSource slaveDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(slaveUserName);
        dataSource.setPassword(slavePassword);
        dataSource.setURL(slaveUrl);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
    ...
}

上面讲述了Spring动态切换数据源的核心原理逻辑以及使用方式,但是我们可以看到它使用上还是有一点点代码侵入性的。其实绝大多数情况下我们都希望切换数据源在方法级别即可,并不需要这么细粒度的控制。因此下面继续介绍更加优雅的操作方式(自定义注解 AOP)

使用AOP 自定义注解方式优雅的实现数据源动态切换

为了实现更优雅的动态数据源的切换,我们可以使用Spring AOP 自定义注解的方式实现对方法级别的数据源切换。

因为注解最低只能定义在方法上(而非代码块上),所以此种方式最细粒度为方法级别,99.99%情况下都够用了

自定义切换数据源的注解:(此处我定义的注解表示:可以用在方法上和类上)

代码语言:javascript复制
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSourceSwitch {
    String dataSourceId() default DynamicDataSourceId.MASTER;
}

书写切面:

代码语言:javascript复制
@Slf4j
@Order(1)
@Aspect
@Component // 切面必须交给容器管理
public class DynamicDataSourceHandlerAspect {

    @Pointcut("@annotation(com.fsx.dynamic.DynamicDataSourceSwitch)")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        DynamicDataSourceSwitch annotationClass = method.getAnnotation(DynamicDataSourceSwitch.class);//获取方法上的注解
        if (annotationClass == null) {
            annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicDataSourceSwitch.class);//获取类上面的注解
            if (annotationClass == null) return;
        }

        //获取注解上的数据源的值的信息(这里最好还判断一下dataSourceId是否是有效的  若是无效的就用warn提醒  此处我就不处理了)
        String dataSourceId = annotationClass.dataSourceId();
	
		// 此处:切换数据源~~~
        DynamicDataSourceContextHolder.setDataSourceId(dataSourceId);
        log.info("AOP动态切换数据源,className"   joinPoint.getTarget().getClass().getName()   "methodName"   method.getName()   ";dataSourceId:"   dataSourceId == "" ? "默认数据源" : dataSourceId);
    }

    // 清理掉当前设置的数据源,让默认的数据源不受影响
    @After("pointcut()")
    public void after(JoinPoint point) {
        DynamicDataSourceContextHolder.clearDataSourceId();
    }
}

这样我们只需要在方法上标注一个注解,指定要切换到的数据源key就搞定了,非常的优雅,没有代码侵入性。

代码语言:javascript复制
    @Transactional
    @DynamicDataSourceSwitch
    @Override
    public Object hello(Integer id) {
    	...
    }

在实际开发中我也是推荐使用此方式,若存在极其特殊的场景,你也可以结合编程的方式进行更细粒度的控制。

请确保标注此注解的Bean是交给Spring容器管理的~ 另外一个常识:你的@Aspect切面只能切入同容器内的Bean,而不能切入子容器内的Bean。(这点在常规SpringMVC开发中可能存在Controller标注注解无效的情况吗,但在SpringBoot开发中无此顾虑,因为SpringBoot面向开发者只定义了一个容器)




总结

本文介绍了多种实现同一个工程内对多个数据源管理,但很显然,它的最佳实践是有一个: Spring2.0.1引入了AbstractRoutingDataSource,它并不是在1.0里几有的抽象,可见它也是时代的产物。 该类充当了DataSource路由中介。 能有在运行时, 根据某种key值来动态切换到真正的DataSource上, 同时对于不支持事务隔离级别的JTA事务来说, Spring还提供了另外一个类IsolationLevelDataSourceRouter来处理这个问题。(具体在JTA事务里再会详解) 另外,上面讲述的这些API都在spring-jdbc.jar里。

最后也留一个小悬念:多数据源切换是成功了,但牵涉到事务呢?单数据源事务是ok的,但如果多数据源需要同时使用一个事务呢?

0 人点赞