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

参考了很多网上的教程做了一个简易的多数据源切换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文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342