参考了很多网上的教程做了一个简易的多数据源切换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/> <!– lookup parent from repository –>-->
<!--</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
<?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.测试:
1.测试 url:http://localhost:8099/data/test1
2.测试 url:http://localhost:8099/data/test2
测试结果:
测试事务:由于测试二涉及到双库写入,这里用以前的事务是没法进行有效的事务控制的,如果写入的过程中,某个库的写入发生异常,前一个库的事务已经提交,就会造成前一个库数据添加成功,第二个库没添加成功的情况,也就是只会回滚发生异常的数据库的事务,之前提交的数据库事务没法回滚
再次测试,将测试二中自定义的错误放开,再次访问url:http://localhost:8099/data/test2
这种情况如果发生在一个业务方法中很明显是不对的,采用以前的事务管理没法让事务都回滚,需要采用分布式事务来处理这种情况,目前暂时还没整合好加入了分布式事务的多数据源切换
现在这个demo只适合做读写分离,因为读的操作不用加入事务控制,只需要控制写入方的事务回滚即可,下次将写一篇关于分布式事务的多数据源切换demo文章