Spring多数据源控制 && sqlserver集成

105 阅读6分钟

一、使用场景

1,一个应用链接多个数据库

image.png

2,读写分离:

image.png

二、如何实现多数据源-自己实现

1,利用SringJDBC模块提供的AbstractRoutingDataSource 基本的图示:

image.png

优先自己实现

image.png ==> image.png 核心思想就是Spring框架在获取数据源的时候走到我们自己实现的数据源BeanZ中,然后根据key的不同获取到不同的Database。

1,我们只需创建AbstractRoutingDataSource实现类DynamicDataSource然后 始化targetDataSources和key为 数据源标识(可以是字符串、枚举、都行,因为标识是Object)、defaultTargetDataSource即可

2.后续当调用AbstractRoutingDataSource.getConnection 会接着调用提供的模板方法: determineTargetDataSource  
3.通过determineTargetDataSource该方法返回的数据库标识 从resolvedDataSources 中拿到对应的数据源 4.我们只需DynamicDataSource中实现determineTargetDataSource为其提供一个数据库标识

数据源配置

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    datasource1:
      url: jdbc:mysql://127.0.0.1:3306/database1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: root
      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/database2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: root
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver

数据源配置注入

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }
   
}


}

将多个数据源进行注入,设置默认的key值。

@Component
@Primary   // 将该Bean设置为主要注入Bean,
public class DynamicDataSource implements DataSource, InitializingBean {


    // 当前使用的数据源标识
    public static ThreadLocal<String> name=new ThreadLocal<>();

    // 写
    @Autowired
    DataSource dataSource1;
    // 读
    @Autowired
    DataSource dataSource2;


    @Override
    public Connection getConnection() throws SQLException {
        if(name.get().equals("W")){
            return dataSource1.getConnection();
        }else{
            return dataSource2.getConnection();
        }
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        if(name.get().equals("W")){
            return dataSource1.getConnection();
        }else{
            return dataSource2.getConnection();
        }

    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    /**
     * InitializingBean 实现此接口需要重写afterPropertiesSet,初始化的过程。
     * 其实也可以在构造函数中进行初始化。
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
            //todo ..初始化
        name.set("W");
    }

具体的使用

@RestController
@RequestMapping("frend")
@Slf4j
public class FrendController {

    @Autowired
    private FrendService frendService;

    @GetMapping(value = "select")
    public List<Frend> select(){
        DynamicDataSource.name.set("R");
        return frendService.list();
    }

    

    @GetMapping(value = "selectA")
    public String selectA(){

        return "你正在访问A资源";
    }



    @GetMapping(value = "insert")
    public void in(){
        Frend frend = new Frend();
        frend.setName("徐庶");
        DynamicDataSource.name.set("W");
        frendService.save(frend);
    }

}

三、使用Spring提供的AbstractRoutingDataSource

  • 此项一般结合及自定义注解使用

  • 自定义注解 + Spring提供的AbstractRoutingDataSource 避免了自己写的实现类【DynamicDataSource】中一些没有实现的方法

数据源注入

@Component
@Primary   // 将该Bean设置为主要注入Bean,
public class DynamicDataSource extends AbstractRoutingDataSource  {


    // 当前使用的数据源标识
    public static ThreadLocal<String> name=new ThreadLocal<>();

    // 写
    @Autowired
    DataSource firstDataSource;
    // 读
    @Autowired
    DataSource secondDataSource;


    /**
     * 返回当前数据源标识
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    @Override
    public void afterPropertiesSet() {
        //1,为targetDataSource初始化所有数据源
        Map<Object,Object> targetDataSources= new HashMap<>();
        targetDataSources.put(DataSourceMark.W.name(),firstDataSource);
        targetDataSources.put(DataSourceMark.R.name(),secondDataSource);
        super.setTargetDataSources(targetDataSources);

        //2,为defaultDataSource设置默认的数据源
        super.setDefaultTargetDataSource(firstDataSource);
        super.afterPropertiesSet();
    }
}


自定义注解

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {

    String value() default "W";
}

注解在具体类的使用

---------【FrendReadServiceImpl】读使用--------------
@Service
@WR("R")
public class FrendReadServiceImpl implements FrendReadService {

    @Autowired
    FrendMapper frendMapper;

    @Override
    public List<Frend> list() {
        return frendMapper.list();
    }

}

---------------【FrendWriteServiceImpl】写使用------------------------------
@Service
@WR("W")
public class FrendWriteServiceImpl implements FrendWriteService {

    @Autowired
    FrendMapper frendMapper;

    @Override
    public void save(Frend frend) {
        frendMapper.save(frend);

    }
}

切点切面中对于数据源选择的使用

使用前置通知(优先推荐):
在获取数据源之前设置获取数据源的key.

@Before("execution(* com.tuling.dynamic.datasource.service.impl.*.*(..))")
public void dynamicDataSource(JoinPoint joinPoint) {
    WR annotation = joinPoint.getTarget().getClass().getAnnotation(WR.class);
    if(annotation!=null){
        String value = annotation.value();
        DynamicDataSource.name.set(value);
        log.info("使用的数源:"+value);
    }
}

或者使用环绕通知亦可以。

@Component
@EnableAspectJAutoProxy //启动AOP
@Aspect
@Slf4j
public class DynamicAnnotation {

//切点
    @Pointcut("execution(* com.tuling.dynamic.datasource.service.*.*(..))")
    public void pointMethod() {
    }

    ;

//切面
    @Around("pointMethod()")
    public Object dynamicDataSource(ProceedingJoinPoint joinPoint) {
        Object obj=new Object();
        try {


            WR annotation = joinPoint.getTarget().getClass().getAnnotation(WR.class);
            if (annotation != null) {
                String value = annotation.value();
                System.out.println("类上的注解" + value);

                //获取方法的注解
                System.out.println("方法:" + joinPoint.getSignature().getName() + " 使用的数据源: " + value);
                DynamicDataSource.name.set(value);//设置使用的数据源

                    obj = joinPoint.proceed();
                    log.info("方法执行完成之后处理..");

            }
        } catch (Throwable e) {
            e.printStackTrace();

        }
        return obj;

    }

}

拓展:切换数据源的自定义注解在具体方法中的使用:

@RestController
@RequestMapping("frend")
@Slf4j
public class FrendController {

    @Autowired
    private FrendService frendService;

    /**
     * 注解切换
     * */
    @WR("R")
    @GetMapping(value = "selectR")
    public List<Frend> selectR(){

        return frendService.list();
    }

    @GetMapping(value = "insertW")
    @WR("W")
    public void insertW(){
        Frend frend = new Frend();
        frend.setName("徐庶");
        frendService.save(frend);
    }
}

@Component
@EnableAspectJAutoProxy //启动AOP
@Aspect
@Slf4j
public class DynamicAnnotation {

    @Pointcut("execution(* com.tuling.dynamic.datasource..*.*(..))")
    public void pointMethod(){

    };

    @Before("execution(* com.tuling.dynamic.datasource..*.*(..)) && @annotation(wr)")  
    public void dynamicDataSource(JoinPoint joinPoint, WR wr){
        //获取方法的注解
        String value = wr.value();
        System.out.println("方法:"+joinPoint.getSignature().getName()+ " 使用的数据源: "+value);
        DynamicDataSource.name.set(value);//设置使用的数据源

    }

}

五、使用Mybatis插件框架集成多个数据源,用于读写分离的场景。

  • 手动编写代码侵入性太高了。
  • 不同的业务数据库则比方案不太适用。
  • 此框架只使用与mybatis数据源,hebanat防范则不适用此方案。

原理:

扩展mybatis的sql执行器Executor,如果执行的是query的查询方法则设备数据源的key=R, 如果是update则设置key=W写操作 注意:插件有四大对象都可以被增强。

image.png

设置mybatis插件的代理

@Intercepts注解可以配置需要增加的类,本案例中实现了update和query方法,也可以增加insert等方法

@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(DataSourceMark.R.getName());
        }else {
            DynamicDataSource.name.set(DataSourceMark.W.getName());
        }
        //修改当前线程要选择的数据源的key
        return invocation.proceed();
    }
}

将自己实现的DynamicDataSourcePlugin注入到myBatis中
在配置类中增加interceptor即可。

@Bean
public Interceptor dynamicDataSourcePlugin(){
    return new DynamicDataSourcePlugin();
}

mybatis注入Interceptor 的源码如下图截屏: image.png

六、集成多个Mybatis框架

image.png

数据源配置

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    datasource-w:
      url: jdbc:mysql://127.0.0.1:3306/database1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: root
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver
    datasource-r:
      url: jdbc:mysql://127.0.0.1:3306/database2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: root
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver

设置两套mapper

public interface RFrendMapper {
    @Select("SELECT * FROM friend")
    List<Frend> list();

    @Insert("INSERT INTO  friend(`name`) VALUES (#{name})")
    void save(Frend frend);
}

image.png

设置写数据源

@Configuration
// 继承mybatis:
// 1. 指定扫描的mapper接口包(主库)
// 2. 指定使用sqlSessionFactory是哪个(主库)
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.w",sqlSessionFactoryRef = "wSqlSessionFactory")
public class dataSourceConfigW {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource-w")
    public DataSource wDataSource() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public SqlSessionFactory wSqlSessionFactory() throws Exception {

        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(wDataSource());
        // 指定主库对应的mapper.xml文件
        /*sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/order/*.xml"));
        如果是使用註解的方式實現则使用@MapperScan來指定mappper即可。
          */
        return sessionFactory.getObject();

    }
}

设置读数据源

@Configuration
// 继承mybatis:
// 1. 指定扫描的mapper接口包(主库)
// 2. 指定使用sqlSessionFactory是哪个(主库)
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.r",
        sqlSessionFactoryRef = "rSqlSessionFactory")
public class dataSourceConfigR {


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource-r")
    public DataSource rDataSource() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public SqlSessionFactory rSqlSessionFactory() throws Exception {

        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(rDataSource());
        return sessionFactory.getObject();

    }
}

具体使用: 在service中注入不同的mapper

@Service
public class FrendServiceImpl implements FrendService {

    @Autowired
   private WFrendMapper wFrendMapper;

    @Autowired
    private  RFrendMapper rFrendMapper;


    @Override
    public List<Frend> list() {
        return rFrendMapper.list();
    }

    @Override
    public void save(Frend frend) {
        wFrendMapper.save(frend);
    }
}

附录:

关于切面的配置

  • 前置通知@Before的源码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Before {
    String value();

    String argNames() default "";
}

所以如下的使用异常:
@Before("within(com.tuling.dynamic.datasource.service.impl..*) && @annotation(wr)")
public void dynamicDataSource(JoinPoint joinPoint,WR wr) {
    String value = wr.value();
    log.info("自定义注解值:"+value);
    DynamicDataSource.name.set(value);

}
  • 环绕通知范围要注意范围,最好指定到具体的包或者类,防止不必要的性能消耗。如果整个项目使用环绕通知,则会有异常,比如:读取数据源的配置文件时没有实现接口则环绕通知会失败。