一、多数据源的典型使用场景
在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景:
1️⃣业务复杂(数据量大)
数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目,各用各的数据库,涉及数据共享…
2️⃣读写分离
为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。
很多数据库拥主从架构。也就是,一台主数据库服务器,是对外提供增删改业务的生产服务器;另一(多)台从数据库服务器,主要进行读的操作。ꞏ
可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …),但是有一些规模较小的公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离。
这里的架构与上图类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力。
二、多数据源实现原理及实现方法
原理:
对于大多数的java应用,都使用了spring框架,spring-jdbc模块提供AbstractRoutingDataSource,其内部可以包含了多个DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构图如下所示:
应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(ds1、ds2)选择一个,来访问对应的数据库。
(1)当执行数据库持久化操作,只要集成了Spring就一定会通过DataSourceUtils获取Connection
(2)通过Spring注入的DataSource获取Connection即可执行数据库操作。所以思路就是:只需配置一个实现了DataSource的Bean, 然后根据业务动态提供Connection即可
(3)其实Spring已经提供一个DataSource实现类用于动态切换数据源——AbstractRoutingDataSource
(4)分析AbstractRoutingDataSource即可实现动态数据源切换。
1️⃣通过AbstractRoutingDataSource实现动态数据源
通过这个类可以实现动态数据源切换。如下是这个类的成员变量:
private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private Map<Object, DataSource> resolvedDataSources;
targetDataSources保存了key和数据库连接的映射关系
defaultTargetDataSource标识默认的连接
resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系
而AbstractRoutingDataSource实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,通常用作数据初始化。(resolvedDataSources就是在这里赋值)
@Override public void afterPropertiesSet() { ... this.resolvedDataSources = new HashMap<Object, DataSource(this.targetDataSources.size());//初始化resolvedDataSources //循环targetDataSources,并添加到resolvedDataSources中 for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); this.resolvedDataSources.put(lookupKey, dataSource); } ... }
所以,我们只需创建AbstractRoutingDataSource实现类DynamicDataSource然后初始化targetDataSources和key为数据源标识(可以是字符串、枚举、都行,因为标识是Object)、defaultTargetDataSource即可
后续当调用AbstractRoutingDataSource.getConnection 会接着调用提供的模板方法:determineTargetDataSource
通过determineTargetDataSource该方法返回的数据库标识从resolvedDataSources中拿到对应的数据源
所以,我们只需DynamicDataSource中实现determineTargetDataSource为其提供一个数据库标识
总结,在整个代码中我们只需做4件大事:
(1)定义AbstractRoutingDataSource实现类DynamicDataSource
(2)初始化时为targetDataSources设置不同数据源的DataSource和标识、及defaultTargetDataSource
(3)在determineTargetDataSource中提供对应的数据源标识即可
(4)切换数据源标识即可
什么到这还不会? 附上代码:
🍀(1)配置多数据源和 AbstractRoutingDataSource的自定义实现类:DynamicDataSource
配置多数据:
application.yml:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource datasource1: url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false username: root password: 123456 initial-size: 1 min-idle: 1 max-active: 20 test-on-borrow: true driver-class-name: com.mysql.cj.jdbc.Driver datasource2: url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false username: root password: 123456 initial-size: 1 min-idle: 1 max-active: 20 test-on-borrow: true driver-class-name: com.mysql.cj.jdbc.Driver
DynamicDataSourceConfig.java:
@Configuration public class DynamicDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.datasource1") public DataSource firstDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.datasource2") public DataSource secondDataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(5); targetDataSources.put(DataSourceNames.FIRST, firstDataSource); targetDataSources.put(DataSourceNames.SECOND, secondDataSource); return new DynamicDataSource(firstDataSource, targetDataSources); } }
DynamicDataSource.java:
public class DynamicDataSource extends AbstractRoutingDataSource { /** * ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。 * 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好 * * @param defaultTargetDataSource 默认数据源 * @param targetDataSources 目标数据源 */ public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { return getDataSource(); } public static void setDataSource(String dataSource) { CONTEXT_HOLDER.set(dataSource); } public static String getDataSource() { return CONTEXT_HOLDER.get(); } public static void clearDataSource() { CONTEXT_HOLDER.remove(); } }
2️⃣多数据源切换方式
多数据源切换方式需要根据我们的具体需求进行选择:
🍀(1)AOP+自定义注解
用于不同业务的数据源: 一般利用AOP,结合自定义注解动态切换数据源
- (1)自定义注解
@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface WR { String value() default "W"; }
(2)切面类
@Component @Aspect public class DynamicDataSourceAspect { // 前置通知 @Before("within(com.tuling.dynamic.datasource.service.impl.*) && @annotation(wr)") public void before(JoinPoint joinPoint, WR wr){ System.out.println(wr.value()); } }
(3)使用注解
@Service public class FrendImplService implements FrendService { @Autowired FrendMapper frendMapper; @Override @WR("R") // 库2 public List<Frend> list() { return frendMapper.list(); } @Override @WR("W") // 库1 public void save(Frend frend) { frendMapper.save(frend); } }
🍀(2)MyBatis插件
用于读写分离的数据源:如果是MyBatis可以结合插件实现读写分离动态切换数据源
@Intercepts( {@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class})}) public class DynamicDataSourcePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] objects = invocation.getArgs(); MappedStatement ms = (MappedStatement) objects[0]; // 读方法 if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { DynamicDataSource.name.set("R"); } else { // 写方法 DynamicDataSource.name.set("W"); } // 修改当前线程要选择的数据源的key return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) { } }
3️⃣Spring集成多个MyBatis框架实现多数据源
WDataSourceConfig.java:
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.w", sqlSessionFactoryRef = "wSqlSessionFactory") public class WDataSourceConfig { @Bean @Primary public SqlSessionFactory wSqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource1) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource1); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/w/*.xml")); /*主库设置sql控制台打印*/ org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setLogImpl(StdOutImpl.class); sessionFactory.setConfiguration(configuration); return sessionFactory.getObject(); } }
RDataSourceConfig.java:
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.r", sqlSessionFactoryRef = "rSqlSessionFactory") public class RMyBatisConfig { @Bean public SqlSessionFactory rSqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource2) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource2); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/r/*.xml")); /*从库设置sql控制台打印*/ org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setLogImpl(StdOutImpl.class); sessionFactory.setConfiguration(configuration); return sessionFactory.getObject(); } }