Java框架—SpringBoot+MyBatis实现多数据源配置

SpringBoot 实现多数据源配置

一、多数据源使用场景

在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况:

  • ①业务复杂(数据量大)
    数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目多个平台,各用各的数据库,涉及数据共享。

  • ②读写分离
    为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。
    很多数据库拥主从架构。也就是,一台主数据库服务器,是对外提供增删改业务的生产服务器;另一台或者多台从数据库服务器,主要进行读的操作。ꞏ

读写分离可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …)进行处理,但是有一些公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离。

二、多数据源的实现

1.springBoot+MyBatis分包方式整合实现

1.1 application.yml配置

server:
  port: 8080 # 启动端口
spring:
  datasource: 
    db1: # 数据源1
      jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    db2: # 数据源2
      jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

1.2连接数据源配置类

@Configuration
@MapperScan(basePackages = "com.example.dao.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class DataSourceConfig1 {

    @Primary // 表示默认数据源, 这个注解必须要加,不然spring将分不清楚那个为主数据源
    @Bean("db1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db1") //读取yml文件中的配置的数据源参数
    public DataSource createDb1DataSource(){
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean("db1SqlSessionFactory")
    public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致)
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/db1/*.xml"));
        return bean.getObject();
    }

	// 配置事务
	@Bean(name = "db1TransactionManager")
    public DataSourceTransactionManager db1TransactionManager(@Qualifier("db1DataSource") DataSource dataSource) {
		DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
		dataSourceTransactionManager.setRollbackOnCommitFailure(true);
		dataSourceTransactionManager.setGlobalRollbackOnParticipationFailure(true);
    	return dataSourceTransactionManager;
    }

    @Primary
    @Bean("db1SqlSessionTemplate")
    public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
@Configuration
@MapperScan(basePackages = "com.example.dao.db2", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class DataSourceConfig2 {

    @Bean("db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource getDb1DataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean("db2SqlSessionFactory")
    public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/db2/*.xml"));
        return bean.getObject();
    }

	@Bean(name = "db2TransactionManager")
    public DataSourceTransactionManager db2TransactionManager(@Qualifier("db2DataSource") DataSource dataSource) {
		DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
		dataSourceTransactionManager.setRollbackOnCommitFailure(true);
		dataSourceTransactionManager.setGlobalRollbackOnParticipationFailure(true);
    	return dataSourceTransactionManager;
    }

    @Bean("db2SqlSessionTemplate")
    public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

注:
①需要导入相关springBoot,mybatis依赖包这里不多做介绍
②在 service 层中根据不同的业务注入不同的 dao 层
③如果是主从复制- -读写分离:比如 db1 中负责增删改,db2 中负责查询。但是需要注意的是负责增删改的数据库必须是主库(master)

2.springboot+druid+mybatisplus使用注解整合

2.1application.yml配置

server:
  port: 8080
spring:
  datasource:
    dynamic:
      primary: db1 # 配置默认数据库
      datasource:
        db1: # 数据源1配置
          url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
        db2: # 数据源2配置
          url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        initial-size: 1
        max-active: 20
        min-idle: 1
        max-wait: 60000
  autoconfigure:
    exclude:  com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 去除druid配置

DruidDataSourceAutoConfigure会注入一个DataSourceWrapper,其会在原生的spring.datasource下找 url, username, password 等。动态数据源 URL 等配置是在 dynamic 下,因此需要排除,否则会报错。排除方式有两种,一种是上述配置文件排除,还有一种可以在项目启动类排除(@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class))

2.2使用@DS注解来区分不同数据源

给使用非默认数据源添加注解@DS,@DS可以注解在方法上和类上,同时存在方法注解优先于类上注解
注解在 service 实现或 mapper 接口方法上,不要同时在 service 和 mapper 注解

Mapper上

@DS("db2") 
public interface UserMapper extends BaseMapper<User> {
}

Service上

@Service
@DS("db2")
public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements IModelService {}

方法上

@Select("SELECT * FROM user")
@DS("db2")
List<User> selectAll();

2.3 @Transaction 和 @DS 同时使用的问题

问题: 在开发中同时使用这两个注解运用不当很容易出现找不到表的情况,也就是@DS失效没有切换成功,错误情况就不贴出来了(大致内容数据库不存在“xxx”表)。
原因:

  • 在插入方法上加@Transactional和@DS注解,数据源没有切换
  • 开启事务的同时,会从数据库连接池获取数据库连接;
  • 如果内层的service使用@DS切换数据源,只是又做了一层拦截,但是并没有改变整个事务的连接;
  • 在这个事务内的所有数据库操作,都是在事务连接建立之后,所以会产生数据源没有切换的问题;
  • 为了使@DS起作用,必须替换数据库连接,也就是改变事务的传播机制,产生新的事务,获取新的数据库连接

@Transactional执行流程:
1.service的 upload方法上添加了 @Transactional 注解,Spring事务就会生效。此时,Spring TransactionInterceptor会通过AOP拦截该方法,创建事务。
2.而创建事务,势必就会获得数据源。那么,TransactionInterceptor (事务拦截器) 会使用 Spring DataSourceTransactionManager (数据源事务管理) 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。3.而事务信息,就包括事务对应的 Connection 连接。所以还没走到 Mapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 Mapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 Mapper 添加 @DS 注解所对应的 DataSource 所对应的 Connection 。
DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection:
1.对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用 dynamic-datasource-spring-boot-starter 时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。
2.DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了master、slave 两个数据源,并且默认使用 master 数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于 @DS 获得数据源名,从而获得对应的 DataSource ,如果在 Service 方法上没有添加 @DS 注解,所以它只好返回默认数据源,也就是 master。

当方法上加@Transactional(propagation =Propagation.REQUIRES_NEW),这样在调用另一个事务方法时,TransactionInterceptor会将原事务挂起,开启一个新事务,暂时性的将原事务信息和当前线程解绑。就会重新走一次对应的Connection连接。

开启新事物对原来外部事务影响:内影响外,外不影响内:

  • REQUIRES_NEW 会新开启事务,外层事务不会影响内部事务的提交/回滚
  • REQUIRES_NEW 的内部事务的异常,会影响外部事务的回滚

解决方案: 在方法上加@Transactional(propagation=Propagation.REQUIRES_NEW),数据源切换,且事务有效。它会重新创建新事务,获取新的数据库连接,从而得到@DS的数据源

3.继承AbstractRoutingDataSource实现

3.1application.yml配置

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://xxxxxx:3306/db1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: xxxx
        password: xxxx
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://xxxxx:3306/db2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: xxxx
        password: xxxx
        driver-class-name: com.mysql.cj.jdbc.Driver

DruidDataSourceAutoConfigure会注入一个DataSourceWrapper,其会在原生的spring.datasource下找 url, username, password 等。动态数据源 URL 等配置是在 druid下,因此需要排除,否则会报错。排除方式有两种,一种是上述配置文件排除,还有一种可以在项目启动类排除(@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class))

3.2 配置类和继承类

继承类:

/**
 * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        log.info("determineCurrentLookupKey = {}", getDataSource());
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        log.info("setDataSource = {}", dataSource);
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        log.info("clean datasource");
        contextHolder.remove();
    }
}

配置类:

/**
 * @description: 设置数据源
 **/
@Configuration
public class DateSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.master") // 获取数据源配置
    public DataSource masterDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource(){
        Map<Object,Object> dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master",defaultDataSource);
        dataSourceMap.put("slave",slaveDataSource());
        return new DynamicDataSource(defaultDataSource,dataSourceMap);
    }
}

设置多数据源的key:

public interface DataSourceNames {
    String FIRST = "master";
    String SECOND = "slave";
}

3.3 使用

@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
    DynamicDataSource .setDataSource(datasourceName);
    TestUser testUser = testUserMapper.selectOne(null);
    DynamicDataSource.clearDataSource();
    return testUser.getUserName();
}

3.4 自定义注解方式

以上方法需要在每个查询sql前塞入和删除比较麻烦,可以通过注解方式来实现:

自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}

切面类:

@Aspect
@Component
public class DataSourceAspect implements Ordered {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(com.gdlife.datasources.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        DataSource ds = method.getAnnotation(DataSource.class);
        if (ds == null) {
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
            logger.info("set datasource is " + DataSourceNames.FIRST);
        } else {
            DynamicDataSource.setDataSource(ds.name());
            logger.info("set datasource is " + ds.name());
        }
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.info("clean datasource");
        }
    }
}

使用:

@DataSource(name = DataSourceNames.FIRST )

3.5使用@Transaction问题

使用自定义数据源注解和使用@Transaction注解也会失效,原因同@DS注解,解决方案也同上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值