0


Spring Boot配置多数据源的四种方式

1、导读

在日常开发中我们都是以单个数据库进行开发,在小型项目中是完全能够满足需求的。
但是,当我们牵扯到像淘宝、京东这样的大型项目的时候,单个数据库就难以承受用户的CRUD操作。
那么此时,我们就需要使用多个数据源进行读写分离的操作,这种方式也是目前一种流行的数据管理方式。

2、所需的资源

  1. Spring boot
  2. Mybatis-plus
  3. Alibab Druid数据库连接池
  4. MySql 数据库

3、Spring Boot配置多数据源

数据库

在这里插入图片描述

在YAML文件中定义数据源所需的数据

spring:datasource:type: com.alibaba.druid.pool.DruidDataSource ## 声明数据源的类型mysql-datasource1:## 声明第一个数据源所需的数据url: jdbc:mysql://localhost:3306/mybatis?useSSL=true&serverTimezone=Asia/Shanghaiusername: root
      password:123456driver-class-name: com.mysql.cj.jdbc.Driver

    mysql-datasource2:## 声明第二个数据源所需的数据url: jdbc:mysql://localhost:3306/bookstore?useSSL=true&serverTimezone=Asia/Shanghaiusername: root
      password:123456driver-class-name: com.mysql.cj.jdbc.Driver

    druid:## druid数据库连接池的基本初始化属性initial-size:5## 连接池初始化的大小min-idle:1## 最小空闲的线程数max-active:20## 最大活动的线程数mybatis-plus:mapper-locations: classpath:/mapper/*.xml## 配置MyBatis-Plus扫描Mapper文件的位置type-aliases-package: com.example.sqlite.entity ## 创建别名的类所在的包

mysql-datasource1、mysql-datasource2是自定义的数据。

定义多个数据源

@ConfigurationpublicclassDataSourceConfig{@Bean(name ="mysqlDataSource1")@ConfigurationProperties(prefix ="spring.datasource.mysql-datasource1")publicDataSourcedataSource1(){DruidDataSource build =DruidDataSourceBuilder.create().build();return build;}@Bean(name ="mysqlDataSource2")@ConfigurationProperties(prefix ="spring.datasource.mysql-datasource2")publicDataSourcedataSource2(){DruidDataSource build =DruidDataSourceBuilder.create().build();return build;}}

@ConfigurationProperties注解用于将YAML中指定的数据创建成指定的对象,但是,YAML中的数据必须要与对象对象中的属性同名,不然无法由Spring Boot完成赋值。

由于我们要定义多个数据源,所以在Spring Boot数据源自动配置类中就无法确定导入哪个数据源来完成初始化,所以我们就需要禁用掉Spring Boot的数据源自动配置类,然后使用我们自定义的数据源配置类来完成数据源的初始化与管理。

@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class})publicclassDatasourceDomeApplication{publicstaticvoidmain(String[] args){SpringApplication.run(DatasourceDomeApplication.class, args);}}

在启动类上声明需要禁用的自动配置类:exclude = {DataSourceAutoConfiguration.class}

3.1、实现DataSource接口

缺点:产生大量的代码冗余,在代码中存在硬编码。

3.1.1、代码

@Component@PrimarypublicclassDynamicDataSourceimplementsDataSource{//使用ThreadLocal而不是String,可以在多线程的时候保证数据的可靠性publicstaticThreadLocal<String> flag =newThreadLocal<>();@ResourceprivateDataSource mysqlDataSource1;// 注入第一个数据源@ResourceprivateDataSource mysqlDataSource2;// 注入第二个数据源publicDynamicDataSource(){// 使用构造方法初始化ThreadLocal的值
        flag.set("r");}@OverridepublicConnectiongetConnection()throwsSQLException{// 通过修改ThreadLocal来修改数据源,// 为什么通过修改状态就能改变已经注入的数据源? 这就得看源码了。if(flag.get().equals("r")){return mysqlDataSource1.getConnection();}return mysqlDataSource2.getConnection();}@OverridepublicConnectiongetConnection(String username,String password)throwsSQLException{returnnull;}@OverridepublicPrintWritergetLogWriter()throwsSQLException{returnnull;}@OverridepublicvoidsetLogWriter(PrintWriter out)throwsSQLException{}@OverridepublicvoidsetLoginTimeout(int seconds)throwsSQLException{}@OverridepublicintgetLoginTimeout()throwsSQLException{return0;}@OverridepublicLoggergetParentLogger()throwsSQLFeatureNotSupportedException{returnnull;}@Overridepublic<T>Tunwrap(Class<T> iface)throwsSQLException{returnnull;}@OverridepublicbooleanisWrapperFor(Class<?> iface)throwsSQLException{returnfalse;}}

实现DataSource接口我们本质上只使用了一个方法,就是getConnection()这个无参的方法,但是DataSource接口中所有的方法我们也都需要实现,只是不用写方法体而已,也就是存在了很多的 “废方法” 。
@Primary注解 == @Order(1),用于设置此类的注入顺序。

3.1.2、使用

// 访问第一个数据库的t_user表@RestControllerpublicclassUserController{@ResourceprivateUserService userService;@GetMapping(value ="/user_list")publicList<User>showUserList(){DynamicDataSource.flag.set("read");// 修改数据源的状态List<User> list = userService.list();return list;}}
// 访问第二个数据库的Book表@RestControllerpublicclassBookController{@ResourceprivateBookServiceBookService;@GetMapping(value ="/Book_list")publicList<Book>getBookList(){DynamicDataSource.flag.set("write");// 修改数据源的状态List<Book> list =BookService.list();return list;}}

3.2、继承AbstrictRoutingDataSource类

减少了代码的冗余,但是还是会存在硬编码。

3.2.1、代码

@Primary@ComponentpublicclassDynamicDataSourceextendsAbstractRoutingDataSource{publicstaticThreadLocal<String> flag =newThreadLocal<>();@ResourceprivateDataSource mysqlDataSource1;@ResourceprivateDataSource mysqlDataSource2;publicDynamicDataSource(){
        flag.set("read");}@OverrideprotectedObjectdetermineCurrentLookupKey(){// 通过Key来得到数据源return flag.get();}@OverridepublicvoidafterPropertiesSet(){Map<Object,Object> targetDataSource =newConcurrentHashMap<>();
        targetDataSource.put("read",mysqlDataSource1);// 将第一个数据源设置为默认的数据源。super.setDefaultTargetDataSource(mysqlDataSource1);
        targetDataSource.put("write",mysqlDataSource2);// 将Map对象赋值给AbstrictRoutingDataSource内部的Map对象中。super.setTargetDataSources(targetDataSource);super.afterPropertiesSet();}}

AbstrictRoutingDataSource的本质就是利用一个Map将数据源存储起来,然后通过Key来得到Value来修改数据源。

3.2.2、使用

// 访问第一个数据库的t_user表@RestControllerpublicclassUserController{@ResourceprivateUserService userService;@GetMapping(value ="/user_list")publicList<User>showUserList(){DynamicDataSource.flag.set("read");// 修改数据源的状态List<User> list = userService.list();return list;}}
// 访问第二个数据库的Book表@RestControllerpublicclassBookController{@ResourceprivateBookServiceBookService;@GetMapping(value ="/Book_list")publicList<Book>getBookList(){DynamicDataSource.flag.set("write");// 修改数据源的状态List<Book> list =BookService.list();return list;}}

3.3、使用Spring AOP + 自定义注解的形式

Spring AOP + 自定义注解的形式是一种推荐的写法,减少代码的冗余且不存在硬编码。
此方法适合对指定功能操作指定数据库的模式。

3.3.1、导入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

3.3.2、开启AOP支持

@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class})@EnableAspectJAutoProxy//开启Spring Boot对AOP的支持publicclassAopDatasourceApplication{publicstaticvoidmain(String[] args){SpringApplication.run(AopDatasourceApplication.class, args);}}

3.3.3、定义枚举来表示数据源的标识

publicenumDataSourceType{

    MYSQL_DATASOURCE1,

    MYSQL_DATASOURCE2,}

3.3.4、继承AbstractRoutingDataSource类

@Primary@ComponentpublicclassDataSourceManagementextendsAbstractRoutingDataSource{publicstaticThreadLocal<String> flag =newThreadLocal<>();@ResourceprivateDataSource mysqlDataSource1;@ResourceprivateDataSource mysqlDataSource2;publicDataSourceManagement(){
        flag.set(DataSourceType.MYSQL_DATASOURCE1.name());}@OverrideprotectedObjectdetermineCurrentLookupKey(){return flag.get();}@OverridepublicvoidafterPropertiesSet(){Map<Object,Object> targetDataSource =newConcurrentHashMap<>();
        targetDataSource.put(DataSourceType.MYSQL_DATASOURCE1.name(),mysqlDataSource1);
        targetDataSource.put(DataSourceType.MYSQL_DATASOURCE2.name(),mysqlDataSource2);super.setTargetDataSources(targetDataSource);super.setDefaultTargetDataSource(mysqlDataSource1);super.afterPropertiesSet();}}

3.3.5、自定义注解

@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceTargetDataSource{DataSourceTypevalue()defaultDataSourceType.MYSQL_DATASOURCE1;}

3.3.6、定义注解的实现类

@Component@Aspect@Slf4jpublicclassTargetDataSourceAspect{@Before("@within(TargetDataSource) || @annotation(TargetDataSource)")publicvoidbeforeNoticeUpdateDataSource(JoinPoint joinPoint){TargetDataSource annotation =null;Class<?extendsObject> target = joinPoint.getTarget().getClass();if(target.isAnnotationPresent(TargetDataSource.class)){// 判断类上是否标注着注解
             annotation = target.getAnnotation(TargetDataSource.class);
             log.info("类上标注了注解");}else{Method method =((MethodSignature) joinPoint.getSignature()).getMethod();if(method.isAnnotationPresent(TargetDataSource.class)){// 判断方法上是否标注着注解,如果类和方法上都没有标注,则报错
                annotation = method.getAnnotation(TargetDataSource.class);
                log.info("方法上标注了注解");}else{thrownewRuntimeException("@TargetDataSource注解只能用于类或者方法上, 错误出现在:["+
                        target.toString()+" "+ method.toString()+"];");}}// 切换数据源DataSourceManagement.flag.set(annotation.value().name());}}

在有的博客中也会使用@Around环绕通知的方式,但是环绕通知需要执行joinPoint.process()方法来调用目标对象的方法,最后返回执行的值,不然得不到所需要的数据。
我这里使用了@Before前置通知,效果是一样的,因为@Around就会包含@Before。

@Around("@within(TargetDataSource) || @annotation(TargetDataSource)")publicObjectbeforeNoticeUpdateDataSource(ProceedingJoinPoint joinPoint){// 省略逻辑代码Object result =null;try{
            result = joinPoint.proceed();}catch(Throwable e){
            e.printStackTrace();}return result;}

ProceedingJoinPoint 对象只能在@Around环绕通知中使用,在其他通知中使用就会报错。

3.3.7、使用

// 访问第一个数据源。@RestController// 将注解标注在类上,表示本类中所有的方法都是使用数据源1@TargetDataSource(value =DataSourceType.MYSQL_DATASOURCE1)publicclassUserController{@ResourceprivateUserService userService;@GetMapping(value ="/user_list")publicList<User>showUserList(){System.out.println(DataSourceType.MYSQL_DATASOURCE1.name());List<User> list = userService.list();return list;}}
// 访问第二个数据源@RestControllerpublicclassBookController{@ResourceprivateBookServiceBookService;@GetMapping(value ="/Book_list")// 将注解标注在方法上,表示此方法使用数据源2@TargetDataSource(value =DataSourceType.MYSQL_DATASOURCE2)publicList<Book>getBookList(){List<Book> list =BookService.list();return list;}}

3.4、通过SqlSessionFactory指定的数据源来操作指定目录的XML文件

使用此方法则不会与上面所述的类有任何关系,本方法会重新定义类。
本方法也是一种推荐的方法,适用于对指定数据库的操作,也就是适合读写分离。不会存在代码冗余和存在硬编码。

3.4.1、项目的目录结构

对所需要操作的数据库的Mapper层和dao层分别建立一个文件夹。

在这里插入图片描述

3.4.2、配置YAML文件

spring:datasource:type: com.alibaba.druid.pool.DruidDataSource
    mysql-datasource:jdbc-url: jdbc:mysql://localhost:3306/mybatis?useSSL=true&serverTimezone=Asia/Shanghaiusername: root
      password:123456driver-class-name: com.mysql.cj.jdbc.Driver

    sqlite-datasource:jdbc-url: jdbc:mysql://localhost:3306/bookstore?useSSL=true&serverTimezone=Asia/Shanghaiusername: root
      password:123456driver-class-name: com.mysql.cj.jdbc.Driver

    druid:initial-size:5min-idle:1max-active:20mybatis-plus:mapper-locations: classpath:/mapper/*.xmltype-aliases-package: com.example.sqlite.entity

3.4.3、针对Mapper层通过SqlSessionFactory指定数据源来操作

3.4.3.1、创建MySql数据源
@Configuration@MapperScan(basePackages ="com.example.sqlite.dao.mysql", sqlSessionFactoryRef ="MySQLSqlSessionFactory")publicclassMySQLDataSourceConfig{@Bean(name ="MySQLDataSource")@Primary@ConfigurationProperties(prefix ="spring.datasource.mysql-datasource")publicDataSourcegetDateSource1(){returnDataSourceBuilder.create().build();}@Bean(name ="MySQLSqlSessionFactory")@PrimarypublicSqlSessionFactorytest1SqlSessionFactory(@Qualifier("MySQLDataSource")DataSource datasource)throwsException{MybatisSqlSessionFactoryBean bean =newMybatisSqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(// 设置mybatis的xml所在位置newPathMatchingResourcePatternResolver().getResources("classpath*:mapper/mysql/*.xml"));return bean.getObject();}@Bean("MySQLSqlSessionTemplate")@PrimarypublicSqlSessionTemplatetest1SqlSessionTemplate(@Qualifier("MySQLSqlSessionFactory")SqlSessionFactory sessionFactory){returnnewSqlSessionTemplate(sessionFactory);}@BeanpublicPlatformTransactionManagertransactionManager(@Qualifier("MySQLDataSource")DataSource dataSource){returnnewDataSourceTransactionManager(dataSource);}}
3.4.3.2、创建Sqlite数据源
@Configuration@MapperScan(basePackages ="com.example.sqlite.dao.sqlite", sqlSessionFactoryRef ="SqliteSqlSessionFactory")publicclassSqliteDataSourceConfig{@Bean(name ="SqliteDateSource")@ConfigurationProperties(prefix ="spring.datasource.sqlite-datasource")publicDataSourcegetDateSource1(){returnDataSourceBuilder.create().build();}@Bean(name ="SqliteSqlSessionFactory")publicSqlSessionFactorytest1SqlSessionFactory(@Qualifier("SqliteDateSource")DataSource datasource)throwsException{MybatisSqlSessionFactoryBean  bean =newMybatisSqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(newPathMatchingResourcePatternResolver().getResources("classpath*:mapper/sqlite/*.xml"));return bean.getObject();}@Bean("SqliteSqlSessionTemplate")publicSqlSessionTemplatetest1SqlSessionTemplate(@Qualifier("SqliteSqlSessionFactory")SqlSessionFactory sessionFactory){returnnewSqlSessionTemplate(sessionFactory);}@BeanpublicPlatformTransactionManagertransactionManager(@Qualifier("SqliteDateSource")DataSource dataSource){returnnewDataSourceTransactionManager(dataSource);}}
  1. @MapperScan注解中的basePackages指向的是指定的Dao层。
  2. @MapperScan注解中sqlSessionFactoryRef 用来指定使用某个SqlSessionFactory来操作数据源。
  3. bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(“classpath*:mapper/sqlite/*.xml”)); 指向的是操作执行数据库的Mapper层。

如果使用SQLite数据库,那么就必须在项目中内嵌SQLite数据库,这个一个轻量级的数据库,不同于Mysql,SQLite不需要服务器,SQLite适合使用于移动APP开发。
像微信,用户的聊天记录就是使用这个数据库进行存储。SQLite也可以使用在Web端,只是不太方便。

3.4.4、使用

// 访问第一个数据库@RestControllerpublicclassUserController{@ResourceprivateUserService userService;@GetMapping(value ="/user_list")publicList<User>showUserList(){List<User> list = userService.list();return list;}}
// 访问第二个数据库@RestControllerpublicclassAddressController{@ResourceprivateAddressService addressService;@GetMapping(value ="/address_list")publicList<Address>getAddressList(){List<Address> list = addressService.list();return list;}}

使用此种方法不会存在任何代码的冗余以及硬编码的存在,但是需要分层明确。
唯一的不足就是添加一个数据源就需要重新写一个类,而这个类中的代码大部分又是相同的。

4、总结

  1. 实现DataSource接口这种写法是不推荐的。
  2. 推荐使用Spring Boot + 自定义注解的方式与SqlSessionFactory方式。

另外,Spring AOP中各种通知的执行顺序如下图所示:在这里插入图片描述


本文转载自: https://blog.csdn.net/qq_45515182/article/details/126330084
版权归原作者 中国胖子风清扬 所有, 如有侵权,请联系我们删除。

“Spring Boot配置多数据源的四种方式”的评论:

还没有评论