自定义多数据源动态切换(SpringBoot+Mybatis)实现数据库读写分离
本文是使用Spring2.0以后新增的AbstractRoutingDataSource类来实现多数据源动态切换,文章末尾附上可执行的源码(注意修改数据库配置)。
1,自定义多数据源动态切换原理
-
AbstractRoutingDataSource类是spring提供用来控制当前线程最终选择某个数据源的路由器。我们自定义一个动态数据源DynamicDataSource类来继承AbstractRoutingDataSource。AbstractRoutingDataSource继承于 AbstractDataSource,AbstractDataSource继承于javax.sql.DataSource,DataSource通过getDataSource()方法得到连接从而操作数据库。如下图:AbstractDataSource,AbstractDataSource继承于javax.sql.DataSource,DataSource通过getDataSource()方法得到连接从而操作数据库。如下图:
-
DynamicDataSource实现determineCurrentLookupKey()方法,该方法返回最终要选择的数据源名称key,然后通过key路由器将会路由到正确的数据源上面,得到预期数据源。
具体如下:
1. DynamicDataSource先继承DynamicDataSource实现determineCurrentLookupKey()方法;
package com.lx.study.spring.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Description: 动态数据源 Dynamic data source
* @Auther: lixiao
* @Date: 2019/2/22 19:26
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
//数据源路由器,获取最终被执行的数据源
//The data source router, which gets the data source that is finally executed
@Override
protected Object determineCurrentLookupKey() {
//从本地线程中获取最终被执行的数据源名称
//Gets the name of the data source to be executed from the local thread
String dataSource = DynamicDataSourceHolder.getDataSource();
logger.error("------------------当前数据源:{}---------------------",dataSource);
return dataSource;
}
}
- AbstractRoutingDataSource 调用自己的determineTargetDataSource()方法,determineTargetDataSource()方法中就调用了我们实现的determineCurrentLookupKey()方法得到key然后从resolvedDataSources中按照key取出指定的数据源
- 来看看AbstractRoutingDataSource 的一些属性吧。targetDataSources是我们设置的自定义数据源集(这个是需要我们手动设置的),defaultTargetDataSource这个使我们设置的默认的数据源(也是需要手动设置的),resolvedDataSources这个是将我们传入进来的数据源集进过包装后的最终在程序运行时切换的数据源集(这个是自动设置的,不需要我们手动设置),resolvedDefaultDataSource这个是默认的系统在运行时选择的数据源
- 那么resolvedDataSources是怎么样被targetDataSources填充的呢?答案也在源码里,它在程序加载完配置文件后,填充到resolvedDataSources里面的,调用的方法是afterPropertiesSet(),在afterPropertiesSet()方法中调用resolveSpecifiedDataSource()方法将不同厂商的数据源转换成DataSource存储,什么时候设置targetDataSources我们将在下面的具体实现的核心代码部分看见
- 最后便是调用从DataSource一层一层继承下来的getConnection()方法来获取连接,从而操作数据库。
总结一下: 通过自定义动态数据DynamicDataSource重写的determineCurrentLookupKey()方法程序动态的获得不同的数据源名称,从而取出不同的数据源获得连接,实现动态数据源切换的功能
2,自定义数据源动态实现(基于Spring AOP+SpringBoot+Mybatis)
1,准备工作
- 导入所需依赖(怎么建SpringBoot项目这里不做介绍)
<?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 http://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.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lx.study.spring</groupId>
<artifactId>multi-datasourceforspringboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>multi-datasourceforspringboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok是一个idea插件,可以快速生成pojo对象的get/set/toString/hash/equaes方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<!-- druid数据源连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- SpringAop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- spring读取配置文件的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 准备web项目常见的controller(可以不要,我们可以用单元测试),service,mapper层以及实体类User和数据库配置
package com.lx.study.spring.controller;
import com.lx.study.spring.bean.pojo.User;
import com.lx.study.spring.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedList;
import java.util.List;
/**
* @Description:
* @Auther: lixiao
* @Date: 2019/2/23 17:58
*/
@RestController
@RequestMapping("test/")
public class TestController {
@Autowired
TestService test;
@RequestMapping("/test1")
public List<User> test(){
List<User> list = new LinkedList<>();
list.add(test.quety1(1));
list.add(test.quety2(1));
return list;
}
}
package com.lx.study.spring.service;
import com.lx.study.spring.bean.annotation.DataSource;
import com.lx.study.spring.bean.pojo.User;
import java.util.List;
/**
* @Description:
* @Auther: lixiao
* @Date: 2019/2/23 17:59
*/
public interface TestService {
//使用默认数据源即第一个数据源 test1
User quety1(Integer id);
/**
* 使用第二个数据源 即test2
*/
User quety2(Integer id);
int addUser(User user);
List<User> getAll();
}
import java.util.List;
/**
* @Description:
* @Auther: lixiao
* @Date: 2019/2/23 17:54
*/
@Service
@DataSource(dataSource = "dataSource2")
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public User quety1(Integer id) {
return testMapper.getUserByID(id);
}
@Override
public User quety2(Integer id) {
return testMapper.getUserByID(id);
}
@Override
public int addUser(User user) {
return testMapper.addUser(user);
}
@Override
public List<User> getAll() {
return testMapper.getAll();
}
public static void main(String[] args){
}
}
package com.lx.study.spring.mapper;
import com.lx.study.spring.bean.pojo.User;
import java.util.List;
/**
* @Description:
* @Auther: lixiao
* @Date: 2019/2/25 11:43
*/
public interface TestMapper {
User getUserByID(Integer id);
int addUser(User user);
List<User> getAll();
}
<?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.lx.study.spring.mapper.TestMapper">
<select id="getUserByID" resultType="com.lx.study.spring.bean.pojo.User">
select * from user1 where id=#{id}
</select>
<select id="getAll" resultType="com.lx.study.spring.bean.pojo.User">
select * from user1
</select>
<insert id="addUser" >
insert into user1(name) value(#{name})
</insert>
</mapper>
- 建两个数据库,并建立相同的表user1,并添加一条记录
2,核心代码
- 类DBProperties 用来读取所有配置的数据源,这里利用的是@ConfigurationProperties注解,该注解用法自行百度,这里不做多解释
package com.lx.study.spring.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 自定义数据源配置
* @Auther: lixiao
* @Date: 2019/2/23 14:38
*/
@Data
@Component
@ConfigurationProperties(prefix = "druid")
public class DBProperties {
//Hikari 数据源
//private HikariDataSource test1;
//private HikariDataSource test2;
//使用Druid数据源
private DruidDataSource dataSource1;
private DruidDataSource dataSource2;
private Map<Object, Object> dataSources = new HashMap<>();
private String defaultName;
/**
* 初始化自定义数据源集 Initializes the custom datasource set
*/
public void init(){
dataSources.put("dataSource1",dataSource1);
dataSources.put("dataSource2",dataSource2);
}
/**
* 获得默认的数据源 Get the default data source
* @return
*/
public DruidDataSource getDefaultDataSource(){
return (DruidDataSource)this.dataSources.get(defaultName);
}
}
- 类DynamicDataSourceHolder 这个类是实现动态数据源切换的关键,因为DynamicDataSource是单例的所以我们采用ThreadLocal一来保证线程的安全性,而来保证同一个线程获取数据源的唯一性。putDataSource(name)方法用于切换数据源,get()方法用于获取数据源,clear()方法用于清空本地线程的所有数据源,让其使用默认的数据源
package com.lx.study.spring.config;
/**
* @Description: 动态数据源持有者,负责利用ThreadLocal存取本线程使用的数据源的名称
* @Auther: lixiao
* @Date: 2019/2/22 19:30
*/
public class DynamicDataSourceHolder {
/**
* 本地线程共享对象
*/
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void putDataSource(String name) {
THREAD_LOCAL.set(name);
}
public static String getDataSource() {
return THREAD_LOCAL.get();
}
/**
* 清除本线程指定的数据源使用默认数据源
*/
public static void clear() {
THREAD_LOCAL.remove();
}
}
- 类DataSourceConfig,这个类使用将我们定义好的数据源集装载到数据源路由器中(AbstractRoutingDataSource)。也是在这个类设置我们上面提到的targetDataSources(自定义数据源集)和DefaultTargetDataSource(默认的自定义数据源),换需要配置事物管理对象,关于事物管理在后面会详细讲!到
package com.lx.study.spring.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 数据源配置,将自定义的所有的数据源传给数据源路由器
* Data source configuration, which routes all custom data sources to the data source router
* @Auther: lixiao
* @Date: 2019/2/23 17:41
*/
@Configuration
public class DataSourceConfig {
@Autowired
private DBProperties dbProperties;
/**
* 配置数据源 Configure data sources
* @return
*/
@Bean(name = "dataSource")
public DataSource dataSource() {
//初始化自定义数据源集 Initializes the custom datasource set
dbProperties.init();
/**
* 采用AbstractRoutingDataSource的对象包装多数据源
* Adopting AbstractRoutingDataSource object packing more data sources
*/
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(dbProperties.getDataSources());
/**
* 设置默认的数据源,当拿不到指定数据源或者未指定数据源时使用该配置
* Sets the default data source and uses this configuration when the specified data source is not available or is not specified
*/
dataSource.setDefaultTargetDataSource(dbProperties.getDefaultDataSource());
return dataSource;
}
/**
* 配置事务管理 Configuration transaction management
* @return
*/
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource());
}
}
- 注解@DataSource,用于标注使用那个数据库
package com.lx.study.spring.bean.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* @Description: 用以切换数据源的注解 Annotations to switch data sources
* @Auther: lixiao
* @Date: 2018/2/23 16:44
*/
@Component
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataSource {
String dataSource() default "";
}
5,最后一个,也是核心的一个类DataSourceAspect,它是一个切面,来实现动态切换数据库,本文一共演示了3种拦截逻辑。1是基于注解拦截。2是基于类拦截。3是基于方法拦截,具体怎么选择和拓展可更具业务选择。需要注意的是,每个被拦截的方法或类,在执行完方法后,必须清空本地线程中的数据源,也就是调用DynamicDataSourceHolder的clear()方法,至于原因在下面要讲的事物管理中会说到。
package com.lx.study.spring.aop;
import com.lx.study.spring.bean.annotation.DataSource;
import com.lx.study.spring.config.DynamicDataSourceHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* @Description: 数据源切换控制切面 Data source toggle control aspect
* @Auther: lixiao
* @Date: 2019/2/23 16:54
*/
@Component
@Aspect
@Order(0) //保证先被执行
public class DataSourceAspect {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
//模拟操作从表的一些方法
private static String[] queryMethods = {"query","select","getAll","find"};
private static String[] updateMethods = {"insert","add","delete","update"};
private static List queryMethod = new LinkedList(Arrays.asList(queryMethods));
private static List updateMethod = new LinkedList(Arrays.asList(updateMethods));
//---------------------------------------------- 用注解拦截 ----------------------------------------------
/**
* 注解拦截切面表达式 Annotations to intercept
* @annotation 用于拦截所有被该注解标注的方法 Used to intercept all methods annotated with this annotation
* @within 用于拦截被所有该注解标注的类 Used to intercept all classes annotated by this annotation
* */
@Pointcut("@annotation(com.lx.study.spring.bean.annotation.DataSource) || @within(com.lx.study.spring.bean.annotation.DataSource) ")
public void pointcut() {}
@Before("pointcut()")
public void annotationMethodBefore(JoinPoint joinPoint){
Class<?> clazz = joinPoint.getTarget().getClass();
DataSource annotation = clazz.getAnnotation(DataSource.class);
//先判断类上是否有DataSource注解,如果没有在判断方法上是否有注解
if(annotation == null){//类上没有
//获取方法上的注解
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
annotation = method.getAnnotation(DataSource.class);
//如果还是为null则退出,这次方法调用将使用默认的数据源
if(annotation == null){
return;
}
}
//获取注解上得值
String dataSourceName = annotation.dataSource();
logger.debug("---------------------------切换到数据源:"+dataSourceName+"----------------------------------");
//因为有默认数据源的存在,所以不用担心注解上的值无对应的数据源,当找不到指定数据源时,会使用默认的数据源
DynamicDataSourceHolder.putDataSource(dataSourceName);
}
//执行完切面后,将线程共享中的数据源名称清空 让程序使用默认数据源
@After("pointcut()")
public void annotationMethodAfter(JoinPoint joinPoint){
DynamicDataSourceHolder.clear();
}
//---------------------------------基于AOP,拦截某种\某个类-------------------------------------------------------------
@Pointcut("execution( * com.lx.study.spring.service.impl.*Impl.*(..))")
public void pointcut2(){}
@Before("pointcut2()")
public void dataSourcePointCut(JoinPoint joinPoint) {
Class<?> aClass = joinPoint.getTarget().getClass();
DataSource annotation = aClass.getAnnotation(DataSource.class);
//取出类上的注解,并将ThreadLocal 中的数据源设置为指定的数据源, 当然 也可以按照业务需要不适用注解,直接固定某一数据源
if(annotation != null){
String dataSource = annotation.dataSource();
DynamicDataSourceHolder.putDataSource(dataSource);
}else{
return;
}
}
@After("pointcut2()")
public void annotationMethodAfter1(JoinPoint joinPoint){
DynamicDataSourceHolder.clear();
}
//--------------针对方法,比如数据库做主从,select连接从表(dataSource1),update(query,update,delete)连接主表(dataSource2)------------------------
@Pointcut("execution( * com.lx.study.spring.mapper.*Mapper.*(..))")
public void pointcut3(){}
@Before("pointcut3()")
public void byMehods(JoinPoint joinPoint){
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
String name = method.getName();
//判断是否查询相关方法(走从库)
boolean isQuery = queryMethod.contains(name);
//判断是否修改相关方法(走主库)
boolean isUpdate = updateMethod.contains(name);
if(isQuery){
DynamicDataSourceHolder.putDataSource("dataSource1");
}else if(isUpdate){
DynamicDataSourceHolder.putDataSource("dataSource2");
}
return;
}
@After("pointcut3()")
public void byMehods(){
DynamicDataSourceHolder.clear();
}
}
3,结果展示
OK,代码基本就是上面那样,咱们来看看结果:
- 基于注解
- 基于类
3. 基于方法
3,多数据源事物管理
1,切换数据源会不会造成事务切换错误?
经验证不会,在翻看源码后又确信了我的验证。
只要保证操作数据库时获取的连接和进行事物管理时所获取的连接一致就可以正常进行事物工作,而Spring为了保证两个获取的连接connection一致,内部采用了ThreadLocal来存储这个连接,因为Spring在AOP后不能再向应用程序传递参数,所以保证不管在什么时候,在线程内部通过ThreadLocal拿出来的对象都是唯一的。来看看部分源码。PlatformTransactionManager是Spring管理事物的底层接口,不管是编程式事物(TransactionTemplate)还是声明式事物(DataSourceTransactionManager)追朔到最顶层都是PlatformTransactionManager。
当我们在使用事务的时候,需要调用getTransaction(TransactionDefinition definition) 方法获取一个事务状态对象。
接着看它的实现类AbstractPlatformTransactionManager对该方法的实现
因为doGetTransaction()是抽象方法,所以接着往子类DataSourceTransactionManager上找,可以看到该方法是通过TransactionSynchronizationManager的getResource()方法获得ConnectionHolder(连接管理器),而传给getResource()的参数通过源码可以发现当前的DataSource
再继续看TransactionSynchronizationManager的源码,你会发现,spring正是用ThreadLocal来存储这些这些数据源的,而获取数据源的key正是当前线程的传进来的数据源。所以,当 当前线程没有切换数据源之前,通过同一个数据源获得的连接是唯一的。
总结
1,该方案保证不了数据源事物切换,关于多数据源事物完美的解决方案还在摸索中,高手可以留言告知
源码地址:点击我获取源码