本文是使用Spring2.0以后新增的AbstractRoutingDataSource类来实现多数据源动态切换,文章末尾附上可执行的源码(注意修改数据库配置)。
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;
}
}
总结一下: 通过自定义动态数据DynamicDataSource重写的determineCurrentLookupKey()方法程序动态的获得不同的数据源名称,从而取出不同的数据源获得连接,实现动态数据源切换的功能
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.3.RELEASEversion>
<relativePath/>
parent>
<groupId>com.lx.study.springgroupId>
<artifactId>multi-datasourceforspringbootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>multi-datasourceforspringbootname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.10version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
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();
}
<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>
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);
}
}
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();
}
}
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());
}
}
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();
}
}
OK,代码基本就是上面那样,咱们来看看结果:
经验证不会,在翻看源码后又确信了我的验证。
只要保证操作数据库时获取的连接和进行事物管理时所获取的连接一致就可以正常进行事物工作,而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,我们每次设置AOP动态织出后,清空ThreadLocal,那么ThreadLocal不会绑定到线程上传播到Transaction从而造成事务操作从库异常。所以只要每次动态织出,清空ThreadLocal便不会影响事物的主从切换。但是,这样一来只能保证第一次所用数据源对应的数据库的事物,关于多数据源事物完美的解决方案还在摸索中,高手可以留言告知
源码地址:点击我获取源码