springboot+druid+mybatis 动态数据源切换DEMO(暂无分布式事务)

参考了很多网上的教程做了一个简易的多数据源切换demo:

1.pom.xml 依赖准备:


    4.0.0
    
        
        
        
        
    
    com.sccl
    data_source_change
    0.0.1-SNAPSHOT
    data_source_change
    Demo project for Spring Boot

    
    
        
            
                org.springframework.boot
                spring-boot-dependencies
                2.1.6.RELEASE
                pom
                import
            
        
    

    
        1.8
    

    
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            1.3.2
        
        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        
        
        
            mysql
            mysql-connector-java
            
        

        
            org.springframework.boot
            spring-boot-devtools
            runtime
            true
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    



2.yml文件
server:
  port: 8099
  servlet:
    context-path: /data
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      # 注意(名称不支持大写和下划线可用中横线 比如 错误 的命名(slave_**, slaveTwo))
      master: #主库(数据源-1)
        url: jdbc:mysql://localhost:3306/chapter05-1
        username: root
        password: 123456
      slave: #从库(数据源-2)
        open: true
        url: jdbc:mysql://localhost:3306/chapter05-2
        username: root
        password: 123456

mybatis:
  type-aliases-package: com.sccl.data_source_change.domain #包别名
  mapper-locations: classpath*:mybatis/*Mapper*.xml #扫描mapper映射文件
3.项目目录结构:
项目结构
3.1 自定义注解:DataSource
package com.sccl.data_source_change.aspectj.annotation;


import com.sccl.data_source_change.enumConst.DataSourceEnum;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**自定义多数据源切换注解
 * Create by wangbin
 * 2019-11-18-15:25
 */

/**
 * 注解说明:
 * @author wangbin
 * @date 2019/11/18 15:36

源码样例:

 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Inherited
 public @interface MthCache {
 String key();
 }

 @Target 注解

 功能:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。

 ElementType的取值包含以下几种:

 TYPE:类,接口或者枚举
 FIELD:域,包含枚举常量
 METHOD:方法
 PARAMETER:参数
 CONSTRUCTOR:构造方法
 LOCAL_VARIABLE:局部变量
 ANNOTATION_TYPE:注解类型
 PACKAGE:包
 =======================================================================================
 @Retention 注解

 功能:指明修饰的注解的生存周期,即会保留到哪个阶段。

 RetentionPolicy的取值包含以下三种:

 SOURCE:源码级别保留,编译后即丢弃。
 CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值。
 RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。

 ====================================================================================
 @Documented 注解

 功能:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。
 ========================================================================================
 @Inherited注解

 功能:允许子类继承父类中的注解。

 注意!:

 @interface意思是声明一个注解,方法名对应参数名,返回值类型对应参数类型。
 */
 @Target(ElementType.METHOD) //此注解使用于方法上
 @Retention(RetentionPolicy.RUNTIME) //此注解的生命周期为:运行时,在编译后的class文件中存在,在jvm运行时保留,可以被反射调用
public @interface DataSource {
    /**
     * 切换数据源值
     */
    DataSourceEnum value() default DataSourceEnum.MASTER;
}
3.2 数据源枚举 DataSourceEnum
package com.sccl.data_source_change.enumConst;

/**
 * Create by wangbin
 * 2019-11-19-16:54
 */
public enum DataSourceEnum {
    MASTER("master"),
    SLAVE("slave");
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

     DataSourceEnum(String name) {
        this.name = name;
    }
}

3.3

动态数据源 DynamicDataSource

package com.sccl.data_source_change.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

/** 动态数据源
 * Create by wangbin
 * 2019-11-18-16:06
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

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

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDB();
    }
}

动态数据源环境变量控制DynamicDataSourceContextHolder

package com.sccl.data_source_change.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** 当前线程数据源,负责管理数据源的环境变量
 * Create by wangbin
 * 2019-11-18-16:11
 */
public class DynamicDataSourceContextHolder {
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 设置数据源名
     */
    public static void setDB(String dbType){
        log.info("切换到{}数据源", dbType);
        CONTEXT_HOLDER.set(dbType);
    }
    /**
     * 获取数据源名
     */
    public static String getDB(){
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清理数据源名
     */
    public static void clearDB(){
        CONTEXT_HOLDER.remove();
    }
}

多数据源配置 DruidMutilConfig

package com.sccl.data_source_change.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.sccl.data_source_change.datasource.DynamicDataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.lang.Nullable;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * druid 配置多数据源
 *
 * @author sccl
 */
@Configuration
public class DruidMutilConfig {
    @Bean(name = "masterDataSource")
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties("spring.datasource.druid.slave")
    //该注解表示:读取配置时,比较open属性的值和havingValue的值是否一致,二者相同时本配置才生效
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "open", havingValue = "true")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    /**
     * 如果还有数据源,在这继续添加 DataSource Bean
     */
    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Nullable @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.MASTER.getName(), masterDataSource);
        ((DruidDataSource)masterDataSource).setPassword(((DruidDataSource)masterDataSource).getPassword());//解密数据源密码

        if (slaveDataSource != null){
            targetDataSources.put(DataSourceEnum.SLAVE.getName(), slaveDataSource);
            ((DruidDataSource)slaveDataSource).setPassword(((DruidDataSource)slaveDataSource).getPassword());
        }

        // 还有数据源,在targetDataSources中继续添加

        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

}

3.4 数据源切面 DsAspect
package com.sccl.data_source_change.aspectj;

import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 多数据源处理切面
 * 事务管理:
 * 事务管理在开启时,需要确定数据源,也就是说数据源切换要在事务开启之前,
 * 我们可以使用Order来配置执行顺序,在AOP实现类上加Order注解,
 * 就可以使数据源切换提前执行,order值越小,执行顺序越靠前。
 * Create by wangbin
 * 2019-11-18-15:55
 */
@Aspect
@Order(1) //order值越小,执行顺序越靠前。
@Component
public class DsAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 所有添加了DataSource自定义注解的方法都进入切面
     */
    @Pointcut("@annotation(com.sccl.data_source_change.aspectj.annotation.DataSource)")
    public void dsPointCut() {

    }
    // 这里使用@Around,在调用目标方法前,进行aop拦截,通过解析注解上的值来切换数据源。
    // 在调用方法结束后,清除数据源。
    // 也可以使用@Before和@After来编写,原理一样,这里就不多说了。
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            //获取方法上的注解
            DataSource dataSource = method.getAnnotation(DataSource.class);
            if (dataSource != null) {
                //切换数据源
                DynamicDataSourceContextHolder.setDB(dataSource.value().getName());
            }
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDB();
        }
    }
}
3.5 实体、控制层、Service层、mapper层

实体 Book

package com.sccl.data_source_change.domain;

import lombok.Data;

/**
 * Create by wangbin
 * 2019-08-07-0:55
 */
@Data
public class Book {
    private Integer id;
    private String name;
    private String author;
}

控制层 BookController

package com.sccl.data_source_change.controller;


import com.sccl.data_source_change.domain.Book;
import com.sccl.data_source_change.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**在controller层中注入不同的mapper实例,操作不同的数据源
 * Create by wangbin
 * 2019-08-07-1:26
 */
@RestController
public class BookController {
    @Autowired
    private BookService bookService;
    @GetMapping("/test1")//测试查询主从库的数据
    public void test1(){
        List books1 = bookService.getAllBooks();
        List books2 = bookService.getAllBooks2();
        System.out.println("books1:"+books1);
        System.out.println("books2:"+books2);
    }
    @GetMapping("/test2")//测试主从双库写入
    public void test2(){
        Book book = new Book();
        book.setName("罗宾逊");
        book.setAuthor("漂流记");
        int bookNumber = bookService.addBook(book);
        Book book2 = new Book();
        book2.setName("飞驰人生");
        book2.setAuthor("韩寒");
        int number = 1/0;//自定义错误,查看事务是否回滚
        int bookNumber2 = bookService.addBook2(book2);
        System.out.println("向master数据库添加数据:"+bookNumber);
        System.out.println("向slave数据库添加数据:"+bookNumber2);
    }
}

BookService 与BookServiceImpl与BookMapper

package com.sccl.data_source_change.service;


import com.sccl.data_source_change.domain.Book;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-18-17:56
 */
public interface BookService  {
    List getAllBooks();
    List getAllBooks2();
    int addBook(Book book);
    int addBook2(Book book);
}

package com.sccl.data_source_change.service;


import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.domain.Book;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-18-17:57
 */
@Service
public class BookServiceImpl  implements BookService {
    @Autowired
    private BookMapper bookMapper;
    @Transactional
    @Override
    public List getAllBooks() {
        return bookMapper.getAllBooks();
    }
    @Transactional
    @DataSource(value = DataSourceEnum.SLAVE)
    @Override
    public List getAllBooks2() {
        return bookMapper.getAllBooks();
    }
    @Transactional
    @Override
    public int addBook(Book book) {
        return bookMapper.addBook(book);
    }
    @Transactional
    @DataSource(value = DataSourceEnum.SLAVE)
    @Override
    public int addBook2(Book book) {
        return bookMapper.addBook(book);
    }
}


package com.sccl.data_source_change.mapper;


import com.sccl.data_source_change.domain.Book;

import java.util.List;

/**
 * Create by wangbin
 * 2019-08-07-1:18
 */
public interface BookMapper {
    List getAllBooks();
    int addBook(Book book);
}

BookMapper.xml

BookMapper.xml文件位置



    
    
        insert into book (name,author) values (#{name},#{author})
    

4.测试:
master数据库中的数据

slave数据库中的数据
1.测试 url:http://localhost:8099/data/test1
测试test1

测试结果:
查询到双库中的数据
2.测试 url:http://localhost:8099/data/test2
测试test2,先注释掉自定义错误

测试结果:
向双库写入数据
mster库中添加成功

slave库中添加成功

测试事务:由于测试二涉及到双库写入,这里用以前的事务是没法进行有效的事务控制的,如果写入的过程中,某个库的写入发生异常,前一个库的事务已经提交,就会造成前一个库数据添加成功,第二个库没添加成功的情况,也就是只会回滚发生异常的数据库的事务,之前提交的数据库事务没法回滚

再次测试,将测试二中自定义的错误放开,再次访问url:http://localhost:8099/data/test2
异常出现
master库,事务没回滚
slave库,事务回滚了

这种情况如果发生在一个业务方法中很明显是不对的,采用以前的事务管理没法让事务都回滚,需要采用分布式事务来处理这种情况,目前暂时还没整合好加入了分布式事务的多数据源切换

现在这个demo只适合做读写分离,因为读的操作不用加入事务控制,只需要控制写入方的事务回滚即可,下次将写一篇关于分布式事务的多数据源切换demo文章

你可能感兴趣的:(springboot+druid+mybatis 动态数据源切换DEMO(暂无分布式事务))