实现

1、建库建表

首先,我们在本地新建三个数据库名分别为master,slave1,slave2,我们的目前就是写入操作都是在master,查询是 slave1,slave2
因此我们在上一篇也就是【SpringBoot2.0系列05】SpringBoot之整合Mybatis基础上进行改动,
我们在master slave1 slave2中都创建user表 其中初始化salve1库的user表数据为

image.png

初始化
slave2库的user

image.png

具体的数据库脚本如下

create table master.user
(id bigint auto_increment comment '主键'primary key,age int null comment '年龄',password varchar(32) null comment '密码',sex int null comment '性别',username varchar(32) null comment '用户名'
)
engine=MyISAM collate=utf8mb4_bin
;create table slave1.user
(id bigint auto_increment comment '主键'primary key,age int null comment '年龄',password varchar(32) null comment '密码',sex int null comment '性别',username varchar(32) null comment '用户名'
)
engine=MyISAM collate=utf8mb4_bin
;INSERT INTO slave1.user (id, age, password, sex, username) VALUES (2, 22, 'admin', 1, 'admin');create table slave2.user
(id bigint auto_increment comment '主键'primary key,age int null comment '年龄',password varchar(32) null comment '密码',sex int null comment '性别',username varchar(32) null comment '用户名'
)
engine=MyISAM collate=utf8mb4_bin
;
INSERT INTO slave2.user (id, age, password, sex, username) VALUES (3, 19, 'uuu', 2, 'user');
INSERT INTO slave2.user (id, age, password, sex, username) VALUES (4, 18, 'bbbb', 1, 'zzzz');

2、配置多数据源

经过上面初始化 我们的master.user是一张空表,我们等下的插入与更新操作就在这上面,那么我们的查询操作就是在slave1.user跟slave2.user上面了。
上面我们的数据库初始化工作完成了,接下来就是实现动态数据源的过程
首先我们需要在我们的application.yml配置我们的三个数据源

server:port: 8989
spring:datasource:master:password: rooturl: jdbc:mysql://127.0.0.1:3306/master?useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.jdbc.Driverusername: roottype: com.zaxxer.hikari.HikariDataSourcecluster:- key: slave1password: rooturl: jdbc:mysql://127.0.0.1:3306/slave1?useUnicode=true&characterEncoding=UTF-8idle-timeout: 20000driver-class-name: com.mysql.jdbc.Driverusername: roottype: com.zaxxer.hikari.HikariDataSource- key: slave2password: rooturl: jdbc:mysql://127.0.0.1:3306/slave2?useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.jdbc.Driverusername: root
mybatis:mapper-locations: classpath:/mybatis/mapper/*.xmlconfig-location:  classpath:/mybatis/config/mybatis-config.xml

在上面我们配置了三个数据,其中第一个作为默认数据源也就是我们的master数据源。主要是写操作,那么读操作交给我们的slave1跟slave2
其中 master 数据源是一定要配置 作为我们的默认数据源,其次cluster集群中,其他的数据不配置也不会影响程序的运行(相当于单数据源),如果你想添加新的一个数据源 就在cluster下新增一个数据源即可,其中key为必须项,用于数据源的唯一标识,以及接下来切换数据源的标识。

3、注册数据源

在上面我们已经配置了三个数据源,但是这是我们自定义的配置,springboot是无法给我们自动配置,所以需要我们自己注册数据源.
那么就要实现 EnvironmentAware用于读取上下文环境变量用于构建数据源,同时也需要实现 ImportBeanDefinitionRegistrar接口注册我们构建的数据源。com.yukong.chapter5.register.DynamicDataSourceRegister具体代码如下

/*** 动态数据源注册* 实现 ImportBeanDefinitionRegistrar 实现数据源注册* 实现 EnvironmentAware 用于读取application.yml配置*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);/*** 配置上下文(也可以理解为配置文件的获取工具)*/private Environment evn;/*** 别名*/private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();/*** 由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况*/static {aliases.addAliases("url", new String[]{"jdbc-url"});aliases.addAliases("username", new String[]{"user"});}/*** 存储我们注册的数据源*/private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();/*** 参数绑定工具 springboot2.0新推出*/private Binder binder;/*** ImportBeanDefinitionRegistrar接口的实现方法,通过该方法可以按照自己的方式注册bean** @param annotationMetadata* @param beanDefinitionRegistry*/@Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {// 获取所有数据源配置Map config, defauleDataSourceProperties;defauleDataSourceProperties = binder.bind("spring.datasource.master", Map.class).get();// 获取数据源类型String typeStr = evn.getProperty("spring.datasource.master.type");// 获取数据源类型Class<? extends DataSource> clazz = getDataSourceType(typeStr);// 绑定默认数据源参数 也就是主数据源DataSource consumerDatasource, defaultDatasource = bind(clazz, defauleDataSourceProperties);DynamicDataSourceContextHolder.dataSourceIds.add("master");logger.info("注册默认数据源成功");// 获取其他数据源配置List<Map> configs = binder.bind("spring.datasource.cluster", Bindable.listOf(Map.class)).get();// 遍历从数据源for (int i = 0; i < configs.size(); i++) {config = configs.get(i);clazz = getDataSourceType((String) config.get("type"));defauleDataSourceProperties = config;// 绑定参数consumerDatasource = bind(clazz, defauleDataSourceProperties);// 获取数据源的key,以便通过该key可以定位到数据源String key = config.get("key").toString();customDataSources.put(key, consumerDatasource);// 数据源上下文,用于管理数据源与记录已经注册的数据源keyDynamicDataSourceContextHolder.dataSourceIds.add(key);logger.info("注册数据源{}成功", key);}// bean定义类GenericBeanDefinition define = new GenericBeanDefinition();// 设置bean的类型,此处DynamicRoutingDataSource是继承AbstractRoutingDataSource的实现类define.setBeanClass(DynamicRoutingDataSource.class);// 需要注入的参数MutablePropertyValues mpv = define.getPropertyValues();// 添加默认数据源,避免key不存在的情况没有数据源可用mpv.add("defaultTargetDataSource", defaultDatasource);// 添加其他数据源mpv.add("targetDataSources", customDataSources);// 将该bean注册为datasource,不使用springboot自动生成的datasourcebeanDefinitionRegistry.registerBeanDefinition("datasource", define);logger.info("注册数据源成功,一共注册{}个数据源", customDataSources.keySet().size() + 1);}/*** 通过字符串获取数据源class对象** @param typeStr* @return*/private Class<? extends DataSource> getDataSourceType(String typeStr) {Class<? extends DataSource> type;try {if (StringUtils.hasLength(typeStr)) {// 字符串不为空则通过反射获取class对象type = (Class<? extends DataSource>) Class.forName(typeStr);} else {// 默认为hikariCP数据源,与springboot默认数据源保持一致type = HikariDataSource.class;}return type;} catch (Exception e) {throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //无法通过反射获取class对象的情况则抛出异常,该情况一般是写错了,所以此次抛出一个runtimeexception}}/*** 绑定参数,以下三个方法都是参考DataSourceBuilder的bind方法实现的,目的是尽量保证我们自己添加的数据源构造过程与springboot保持一致** @param result* @param properties*/private void bind(DataSource result, Map properties) {ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});// 将参数绑定到对象binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));}private <T extends DataSource> T bind(Class<T> clazz, Map properties) {ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});// 通过类型绑定参数并获得实例对象return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();}/*** @param clazz* @param sourcePath 参数路径,对应配置文件中的值,如: spring.datasource* @param <T>* @return*/private <T extends DataSource> T bind(Class<T> clazz, String sourcePath) {Map properties = binder.bind(sourcePath, Map.class).get();return bind(clazz, properties);}/*** EnvironmentAware接口的实现方法,通过aware的方式注入,此处是environment对象** @param environment*/@Overridepublic void setEnvironment(Environment environment) {logger.info("开始注册数据源");this.evn = environment;// 绑定配置器binder = Binder.get(evn);}
}

上面代码需要注意的是在springboot2.x系列中用于绑定的工具类如RelaxedPropertyResolver已经无法现在使用Binder代替。上面代码主要是读取application中数据源的配置,先读取spring.datasource.master构建默认数据源,然后在构建cluster中的数据源。
在这里注册完数据源之后,我们需要通过@import注解把我们的数据源注册器导入到spring中 在启动类Chapter5Application.java加上如下注解@Import(DynamicDataSourceRegister.class)
其中我们用到了一个DynamicDataSourceContextHolder 中的静态变量来保存我们已经注册成功的数据源的key,至此我们的数据源注册就已经完成了。

4、配置数据源上下文

我们需要新建一个数据源上下文,用户记录当前线程使用的数据源的key是什么,以及记录所有注册成功的数据源的key的集合。对于线程级别的私有变量,我们首先ThreadLocal来实现。
com.yukong.chapter5.config.DynamicDataSourceContextHolder代码取下

/*** @Auther: yukong* @Date: 2018/8/15 10:49* @Description: 数据源上下文*/
public class DynamicDataSourceContextHolder {private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);/*** 存储已经注册的数据源的key*/public static List<String> dataSourceIds = new ArrayList<>();/*** 线程级别的私有变量*/private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();public static String getDataSourceRouterKey () {return HOLDER.get();}public static void setDataSourceRouterKey (String dataSourceRouterKey) {logger.info("切换至{}数据源", dataSourceRouterKey);HOLDER.set(dataSourceRouterKey);}/*** 设置数据源之前一定要先移除*/public static void removeDataSourceRouterKey () {HOLDER.remove();}/*** 判断指定DataSrouce当前是否存在** @param dataSourceId* @return*/public static boolean containsDataSource(String dataSourceId){return dataSourceIds.contains(dataSourceId);}}

5、动态数据源路由

前面我们以及新建了数据源上下文,用于存储我们当前线程的数据源key那么怎么通知spring用key当前的数据源呢,查阅资料可知,spring提供一个接口,名为AbstractRoutingDataSource的抽象类,我们只需要重写determineCurrentLookupKey方法就可以,这个方法看名字就知道,就是返回当前线程的数据源的key,那我们只需要从我们刚刚的数据源上下文中取出我们的key即可,那么具体代码取下。
com.yukong.chapter5.config.DynamicRoutingDataSource

/*** @Auther: yukong* @Date: 2018/8/15 10:47* @Description: 动态数据源路由配置*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {private static Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);@Overrideprotected Object determineCurrentLookupKey() {String dataSourceName = DynamicDataSourceContextHolder.getDataSourceRouterKey();logger.info("当前数据源是:{}", dataSourceName);return DynamicDataSourceContextHolder.getDataSourceRouterKey();}
}

6、通过aop+注解实现动态数据源的切换

现在spring也已经知道通过key来取对应的数据源,我们现在只需要实现给对应的类或者方法设置他们的数据源的key,并且保存在数据源上下文中即可。这里我们采用注解来设置数据源,通过aop拦截并且保存到数据源上下中。
我们新建一个标识数据源的注解@DataSource具体代码取下
com.yukong.chapter5.annotation.DataSource

/*** 切换数据注解 可以用于类或者方法级别 方法级别优先级 > 类级别*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {String value() default "master"; //该值即key值
}

其中他的默认值是master,因为我们默认数据源的key也是master。也就是说如果你直接用注解,而不指定value的话,那么默认就使用master默认数据源。
然后我们新建一个aop类来拦截。代码如下
com.yukong.chapter5.aop

package com.yukong.chapter5.aop;import com.yukong.chapter5.annotation.DataSource;
import com.yukong.chapter5.config.DynamicDataSourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;@Aspect
@Component
public class DynamicDataSourceAspect {private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);@Before("@annotation(ds)")public void changeDataSource(JoinPoint point, DataSource ds) throws Throwable {String dsId = ds.value();if (DynamicDataSourceContextHolder.dataSourceIds.contains(dsId)) {logger.debug("Use DataSource :{} >", dsId, point.getSignature());} else {logger.info("数据源[{}]不存在,使用默认数据源 >{}", dsId, point.getSignature());DynamicDataSourceContextHolder.setDataSourceRouterKey(dsId);}}@After("@annotation(ds)")public void restoreDataSource(JoinPoint point, DataSource ds) {logger.debug("Revert DataSource : " + ds.value() + " > " + point.getSignature());DynamicDataSourceContextHolder.removeDataSourceRouterKey();}
}

通过aop拦截,获取注解上面的value的值key,然后取判断我们注册的keys集合中是否有这个key,如果没有,则使用默认数据源,如果有,则设置上下文中当前数据源的key为注解的value。
7、测试
最后我们在对应的方法上面加上注解来测试一下即可
我们在UserMapper.java上面加上注解,并且进行测试。

/*** @Auther: yukong* @Date: 2018/8/13 19:47* @Description: UserMapper接口*/
public interface UserMapper {/*** 新增用户* @param user* @return*/@DataSource  //默认数据源int save(User user);/*** 更新用户信息* @param user* @return*/@DataSource  //默认数据源int update(User user);/*** 根据id删除* @param id* @return*/@DataSource  //默认数据源int deleteById(Long id);/*** 根据id查询* @param id* @return*/@DataSource("slave1")  //slave1User selectById(Long id);/*** 查询所有用户信息* @return*/@DataSource("slave2")  //slave2List<User> selectAll();
}

上面代码可以知道,我们的新增,修改,删除方法都是在默认数据master上,我们的id查询是在slave1,我们的查询所有在slave2,我们编写测试类来测试把。

/*** @Auther: yukong* @Date: 2018/8/14 16:34* @Description:*/
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class UserMapperTest {@Autowiredprivate UserMapper userMapper;@Testpublic void save() {User user = new User();user.setUsername("master");user.setPassword("master");user.setSex(1);user.setAge(18);Assert.assertEquals(1,userMapper.save(user));}@Testpublic void update() {User user = new User();user.setId(8L);user.setPassword("newpassword");// 返回插入的记录数 ,期望是1条 如果实际不是一条则抛出异常Assert.assertEquals(1,userMapper.update(user));}@Testpublic void selectById() {User user = userMapper.selectById(2L);System.out.println("id:" + user.getId());System.out.println("name:" + user.getUsername());System.out.println("password:" + user.getPassword());}@Testpublic void deleteById() {Assert.assertEquals(1,userMapper.deleteById(1L));}@Testpublic void selectAll() {List<User> users= userMapper.selectAll();users.forEach(user -> {System.out.println("id:" + user.getId());System.out.println("name:" + user.getUsername());System.out.println("password:" + user.getPassword());});}}

首先测试save方法,它将会把数据存到master库的user表,
现在user表是空的,如图

image.png

运行save方法。

image.png

绿色,测试通过,并且日志提示数据源注册成功,一共三个。并且当前使用的master数据源,我们再去master数据库看看有没有数据。

image.png

如上图,插入成功。
新增方法测试完成了。我们在测试一下修改与删除。

image.png

修改方法也测试通过,查看数据库。

image.png

修改成功,删除方法我就不测试, 我们在测试测试,slave1跟slave2数据源的方法,
首先测试slave1的主键查询方法,先看数据库 slave1有哪些数据。

image.png

slave1.user就一条id为2 的数据并且id为2 的数据就slave1才有,我们测试一下能不能查到。

image.png

运行通过,数据源为slave1并且数据也正确显示。
最后我们来测试一下slave2的selectAll方法把,同样先看看slave2.user中有什么数据。

image.png

从图中,得知slave2.user中有两条数据,id分别为3,4。接下来运行测试方法。
结果如图。

image.png

日志提示数据源切换值slave2,并且id为3,4的数据也成功打印。
那么至此我们的多数据源动态数据源就完成了。

原作者:余空啊
链接:https://www.jianshu.com/p/cac4759b2684

付:从数据库动态获取数据源,注入数据源信息

OldTargetDataSourcesHolder

public class OldTargetDataSourcesHolder {public static OldTargetDataSourcesHolder instance = new OldTargetDataSourcesHolder();private Map<String, DataSource> oldTargetDataSources;private OldTargetDataSourcesHolder() {super();}public Map<String, DataSource> getOldTargetDataSources() {return oldTargetDataSources;}public void setOldTargetDataSources(Map<String, DataSource> oldTargetDataSources) {this.oldTargetDataSources = oldTargetDataSources;}
}

DynamicDataSourceRegister

OldTargetDataSourcesHolder.instance.setOldTargetDataSources(allDataSources);
DataSourceManager
public void addDataSource(List<WfDataSource> configs) {Map<Object, Object> allDataSources = new HashMap<Object, Object>();for (WfDataSource config : configs) {HikariDataSource dataSource = (HikariDataSource) getDataSource(config.getDriver(),config.getUrl(),config.getUsername(),config.getPassword());allDataSources.put(String.valueOf(config.getOrunid()), dataSource);}Map oldTargetDataSources = OldTargetDataSourcesHolder.instance.getOldTargetDataSources();allDataSources.putAll(oldTargetDataSources);datasource.setTargetDataSources(allDataSources);// 将map赋值给父类的TargetDataSourcesdatasource.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
}

SpringBoot 多数据源相关推荐

  1. Springboot多数据源问题之 Could not resolve type alias ‘Blog‘.

    Springboot多数据源之 Could not resolve type alias 'Blog'. 由于配置了多数据源,mapper的xml文件放错了包.如下图 只需要把xml文件放到对应的包里 ...

  2. Springboot多数据源配置详解

    Springboot多数据源配置详解 概念 配置 多数据源使用 概念 一般来说,我们正常的业务只涉及一个数据源,在特定的业务场景中需要使用多个数据源的情况,就需要配置多个数据源来满足特定的业务需求.本 ...

  3. SpringBoot多数据源事务解决方案

    背景 之前有文章提供了springboot多数据源动态注册切换的整合方案,在后续使用过程中,发现在事务控制中有多种bug发生,决定对此问题进行分析与解决 前情提要 多数据源切换流程结构图如下所示,包含 ...

  4. SpringBoot多数据源切换,AOP实现动态数据源切换

    SpringBoot多数据源切换,AOP实现动态数据源切换 操作数据一般都是在DAO层进行处理,可以选择直接使用JDBC进行编程 或者是使用多个DataSource 然后创建多个SessionFact ...

  5. springboot 多数据源配置的几种方式

    springboot多数据源配置的三种方式 application.yml配置 1.@Ds("配置数据源名称") 引入依赖 <dependency> <group ...

  6. springboot多数据源动态切换和自定义mybatis分页插件

    1.配置多数据源 增加druid依赖 完整pom文件 数据源配置文件 route.datasource.driver-class-name= com.mysql.jdbc.Driver route.d ...

  7. springboot多数据源配置_SpringBoot-配置多数据源

    1.4.springboot整合多数据源 你们在项目中有使用到多数据源吗? 4.4.1配置文件中新增两个数据源 spring.datasource.test1.driverClassName = co ...

  8. springboot多数据源动态数据源(主从)

    多数据源 使用Spring Boot时,默认情况下,配置DataSource非常容易.Spring Boot会自动为我们配置好一个DataSource. 如果在application.yml中指定了s ...

  9. springboot多数据源使用EntityManager

    entityManagerFactorySecondary指的是springboot项目中的第二个数据源,此处表示导入第二个数据源的实体管理器 @PersistenceContext(unitName ...

最新文章

  1. 网络营销外包期间站长如何挖掘用户真实需求探索网络营销外包真谛
  2. ajax(jquery)前后台传数组(Springmvc后台)
  3. idea 自动生成mybaits_IDEA利用mybatis-generator自动生成dao和mapper
  4. mysql查询去年本月的数据_MySQL查询本周、上周、本月、上个月份数据的sql代码...
  5. 3005-基于二叉链表的二叉树最大宽度的计算(附思路,注释,可能错误分析)
  6. PLSQL连接本地的Oracle数据库
  7. Linq之Lambda进阶
  8. bigint hive java类型_【干货】Hive常用函数大全
  9. vhms销售系统_kangle虚拟主机管理系统(vhms虚拟主机销售系统)
  10. Android TV开发
  11. 使用Vite搭建基础项目
  12. 和平精英服务器维护到几点,和平精英停服到几点钟?4月4日停服时间公告[多图]...
  13. pta 计算机通信(并查集)
  14. C/C++小写数字转换成大写数字
  15. 使用深度RNN模型构建语义搜索引擎
  16. 综合布线方案设计模版
  17. html绑定带有形参的函数,Python中函数参数类型和参数绑定
  18. 就 3 点,提升工作效率
  19. 关于Max导出插件的七七八八
  20. 背完这999句,英语口语绝不…

热门文章

  1. InnoDB log file 设置多大合适?
  2. 云架构云原生 IaaS,PaaS,SaaS,Serverless
  3. 跨境电商国内平台有哪些
  4. Windows平台OpenGL渲染视频
  5. 论文多次查重会影响重复率吗?
  6. 这届程序YUAN可太优秀了
  7. PHP程序员的职业规划,走进PHP后端工程师的世界
  8. 【弹性布局】【设置主轴,交叉轴对齐方式】
  9. MT4平台上mql4实现的基于macd指标的智能交易EA
  10. ES 按照每隔几分钟,几小时,几天统计折线图