美文网首页
springboot+druid+mybatis 动态数据源切换

springboot+druid+mybatis 动态数据源切换

作者: bin_lifecycle | 来源:发表于2019-11-21 17:21 被阅读0次

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

    1.pom.xml 依赖准备:
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <!--<parent>-->
            <!--<groupId>org.springframework.boot</groupId>-->
            <!--<artifactId>spring-boot-starter-parent</artifactId>-->
            <!--<version>2.2.1.RELEASE</version>-->
            <!--<relativePath/> &lt;!&ndash; lookup parent from repository &ndash;&gt;-->
        <!--</parent>-->
        <groupId>com.sccl</groupId>
        <artifactId>data_source_change</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>data_source_change</name>
        <description>Demo project for Spring Boot</description>
    
        <!--不继承spring-boot-starter-parent,使用依赖管理-->
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>2.1.6.RELEASE</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <!-- SpringBoot Web容器 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!-- SpringBoot 拦截器 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <!-- SpringBoot集成mybatis框架 -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
            <!--阿里数据库连接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.10</version>
            </dependency>
            <!-- Mysql驱动包 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <!--<scope>runtime</scope>-->
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    
    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<Object, Object> 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<String> 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<Object, Object> 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<Book> books1 = bookService.getAllBooks();
            List<Book> 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<Book> getAllBooks();
        List<Book> 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<Book> getAllBooks() {
            return bookMapper.getAllBooks();
        }
        @Transactional
        @DataSource(value = DataSourceEnum.SLAVE)
        @Override
        public List<Book> 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<Book> getAllBooks();
        int addBook(Book book);
    }
    
    

    BookMapper.xml

    BookMapper.xml文件位置
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.sccl.data_source_change.mapper.BookMapper">
        <select id="getAllBooks" resultType="Book">
            select * from book
        </select>
        <insert id="addBook" parameterType="Book">
            insert into book (name,author) values (#{name},#{author})
        </insert>
    </mapper>
    
    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 动态数据源切换

          本文链接:https://www.haomeiwen.com/subject/gvptwctx.html