普及知识
什么是动态多数据源
动态多数据源好处
实现方式一:
- 这种方式一旦新增新的数据源、又要增加新的配置代码,重新编译代码。只能实现
静态多数据源加载与切换
- 具体怎么实现看我上一篇文章【SpringBoot】实现JdbcTemplate、Druid、Dynamic-Datasource的多数据源动态切换就行了
实现方式二:
要求:
公司云平台项目每个商户一个数据库,所以在写后台代码时,需要根据应用层传递过来的商户id,进行动态切换数据源。
- 因此方式一就不能满足这个需求,所以就引出了方式二
多数据源的动态切换
,还可以实现在程序的运行过程中,实现动态加载
一个或多个新的数据源。本文主要讲如何通过继承AbstractRoutingDataSource类实现多数据源的动态切换与加载
SpringBoot提供了AbstractRoutingDataSource
类,可以根据用户定义的规则选择当前的数据源,这样我们可以在每次数据库查询操作前执之前,设置使用的数据源。从而实现动态加载、切换数据源。
determineCurrentLookupKey()
方法可以让用户根据自己定义的规则在某一个SQL执行之前动态地选择想要的数据源
分析一下AbstractRoutingDataSource抽象类的源码
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
public AbstractRoutingDataSource() {
}
//关键:以Map的结构存储的我们配置的多个数据源的键值对 key为数据源名称,value为对应数据源
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
//关键: 设置默认数据源
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
}
public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
}
//关键:数据源重新赋值
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
Iterator var1 = this.targetDataSources.entrySet().iterator();
while(var1.hasNext()) {
Entry<Object, Object> entry = (Entry)var1.next();
Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
}
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource)dataSource;
} else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String)dataSource);
} else {
throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}
public <T> T unwrap(Class<T> iface) throws SQLException {
return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
}
//关键:如何选择数据源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//关键:就是在这里调用我们实现的determineCurrentLookupKey方法返回的名称
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
//关键:需要重写的方法,就是根据当前方法的名称查找对应的数据源的,查询不到则使用默认数据源
// 用当前返回的Object去作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。没有就使用默认数据源。
protected abstract Object determineCurrentLookupKey();
}
对于该抽象类,了解两组变量即可:
Map
Map,这两组变量是相互对应的。
继承该抽象类的时候,必须实现一个抽象方法:protected abstract Object determineCurrentLookupKey()
,用于指定到底需要使用哪一个数据源。
实现动态数据源逻辑
自定义DynamicDataSource类
,继承AbstractRoutingDataSource类并实现determineCurrentLookupKey方法
(具体逻辑是从当前线程的ThreadLocal中获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称);
在application.yml
中配置多个数据源;
解析在application.yml
中配置的多个数据源,然后生成DynamicDataSource
实例,并设置默认数据源(defaultTargetDataSource)和其他数据源(targetDataSources),然后通过afterPropertiesSet()
方法将数据源分别进行复制
到resolvedDataSources
和resolvedDefaultDataSource
中。
定义SqlSessionFactory并注入DynamicDataSource以及MapperLocations
调用AbstractRoutingDataSource的#getConnection
的方法的时候,会先调用determineTargetDataSource
方法获取具体的数据源,而在这个方法中会进一步调用我们在DynamicDataSource
类中自定义的determineCurrentLookupKey
方法,最后在返回DataSource后
再进行getConnection
的调用。剩下就是具体的SQL逻辑执行了。
启动类或者配置类使用@MapperScan注解
扫描Mapper接口以及引用sqlSessionFactory
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.11version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
dependency>
一个d01一个db02,我这边只配2个 需要多个的 增加即可
spring:
datasource:
db01:
driverClassName: com.mysql.cj.jdbc.Driver
password: root
url: jdbc:mysql://localhost:3306/springboot_quartz1?characterEncoding=utf-8&serverTimezone=UTC
username: root
db02:
driverClassName: com.mysql.cj.jdbc.Driver
password: root
url: jdbc:mysql://localhost:3306/springboot_quartz2?characterEncoding=utf-8&serverTimezone=UTC
username: root
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz1` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;
USE `springboot_quartz1`;
DROP TABLE IF EXISTS `sys_task`;
CREATE TABLE `sys_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
`description` varchar(255) DEFAULT NULL COMMENT '任务描述',
`cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
`bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
`job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
`job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
`create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
insert into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values
(1,'1111','1111','1111','1111','1111','1111','1111','2021-04-15 17:04:55','1111','2021-04-11 17:04:59');
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;
USE `springboot_quartz2`;
DROP TABLE IF EXISTS `sys_task`;
CREATE TABLE `sys_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
`description` varchar(255) DEFAULT NULL COMMENT '任务描述',
`cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
`bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
`job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
`job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
`create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
insert into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values
(1,'2222','2222','2222','2222','2222','2222','2222','2021-04-15 17:04:55','2222','2021-04-11 17:04:59');
CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;
USE `springboot_quartz3`;
DROP TABLE IF EXISTS `sys_task`;
CREATE TABLE `sys_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
`description` varchar(255) DEFAULT NULL COMMENT '任务描述',
`cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
`bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
`job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
`job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
`create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
insert into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values
(1,'3333','3333','3333','3333','3333','3333','3333','2021-04-15 17:04:55','3333','2021-04-11 17:04:59');
自定义一个动态数据源上下文类,该类通过ThreadLocal的静态常量
存储当前线程是需要访问哪一个数据源。方便在同一个线程上下文中共享数据源名称
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal();
public static String get() {
return contextHolder.get();
}
public static void set(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static void clear() {
contextHolder.remove();
}
}
创建一个动态数据源,继承自Spring的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
类,并重写determineCurrentLookupKey
方法
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Logger log = LoggerFactory.getLogger(getClass());
//缓存添加的数据源
private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();
//缓存默认数据源
private Object dynamicDefaultTargetDataSource = null;
/**
* 重新加载数据源
*/
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dynamicTargetDataSources.putAll(targetDataSources);
super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
}
/**
* 设置默认数据源
*/
@Override
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
}
/**
* 获取默认数据源
*/
public Object getDynamicDefaultTargetDataSource() {
return dynamicDefaultTargetDataSource;
}
/**
* 获取已加载的数据源
*/
public Map<Object, Object> getDynamicTargetDataSources() {
return dynamicTargetDataSources;
}
/**
* 重写至AbstractRoutingDataSource,运行时就是在这里根据名称动态切换数据源的
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
解析在application.yml中配置的多个数据源
,并初始化动态数据源实体类DynamicDataSource
以及sqlSessionFactory
@Configuration
//sqlSessionFactoryRef 表示定义了 key ,表示一个唯一 SqlSessionFactory 实例
@MapperScan(basePackages = {
DynamicDataSourceConfiguration.MAPPER_INTERFACE_PACKAGE}, sqlSessionTemplateRef = "sqlSessionTemplate")
public class DynamicDataSourceConfiguration {
/**
* 扫描的mapper接口路径
* 注意:com.oyjp.ds3.mapper是我项目里的Mapper接口全路径(这里需要改成你们项目中的全路径)
*/
public static final String MAPPER_INTERFACE_PACKAGE = "com.oyjp.ds3.mapper";
/**
* 扫描的mapper文件路径
* 注意:classpath*:com/oyjp/ds3/mapper/*.xml是我项目里的xml的路径(这里需要改成你们项目中xml的路径)
*/
public static final String MAPPER_XML_PACKAGE = "classpath*:com/oyjp/ds3/mapper/*.xml";
//一个一个的将属性注入
/*
@Value("${spring.datasource.db01.url}")
private String db01DBUrl;
@Value("${spring.datasource.db01.username}")
private String db01DBUser;
@Value("${spring.datasource.db01.password}")
private String db01DBPassword;
@Value("${spring.datasource.db01.driverClassName}")
private String db01DriverClassName;
@Value("${spring.datasource.db02.url}")
private String db02DBUrl;
@Value("${spring.datasource.db02.username}")
private String db02DBUser;
@Value("${spring.datasource.db02.password}")
private String db02DBPassword;
@Value("${spring.datasource.db02.driverClassName}")
private String db02DriverClassName;
*/
/**
* 初始化db01数据源
*/
@Bean(name = "db01DataSource")
//根据配置前缀将属性注入到对象属性中,数据源db01的配置前缀为spring.datasource.db01
@ConfigurationProperties(prefix = "spring.datasource.db01")
public DataSource db01DataSource() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
dataSource.setName("db01");//数据源名称
return dataSource;
}
/**
* 初始化db02数据源
*/
@Bean(name = "db02DataSource")
//根据配置前缀将属性注入到对象属性中,数据源db01的配置前缀为spring.datasource.db02
@ConfigurationProperties(prefix = "spring.datasource.db02")
public DataSource db02DataSource() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
dataSource.setName("db02");//数据源名称
return dataSource;
}
/**
* 初始化动态数据源,并注入db01,db02,以及设置默认数据源为db01
*/
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier("db01DataSource") DataSource db01DataSource, @Qualifier("db02DataSource") DataSource db02DataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource;
Map<Object, Object> map = new HashMap<>();
map.put("db01", db01DataSource);
map.put("db02", db02DataSource);
//默认数据源
dynamicDataSource.setDefaultTargetDataSource(db01DataSource);
//数据源列表,与名称绑定
dynamicDataSource.setTargetDataSources(map);
return dynamicDataSource;
}
/**
* 配置 SqlSessionFactoryBean
* 将 MyBatis 的 mapper 位置和持久层接口的别名设置到 Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
bean.setDataSource(dynamicDataSource);
//此处设置为了解决找不到mapper文件的问题
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PACKAGE));
return bean.getObject();
}
/**
* 设置动态数据源DynamicDataSource到会话工厂
*/
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 将动态数据源添加到事务管理器中,并生成新的bean
*
* 切库与事务注意:
* 1.有@Transactional注解的方法,方法内部不可以做切换数据库 操作
* 2.在同一个service其他方法调用带@Transactional的方法,事务不起作用
* 3.在应用中因为使用了 DAO 层的切面切换数据源,所以 @Transactional 注解不能加在类上,只能用于方法;有 @Trasactional注解的方法无法切换数据源
* @return 事务管理实例
*/
@Bean
public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
dynamicDataSource.setDefaultTargetDataSource(db01DataSource);
这一行代码表示没有指定数据源时,默认使用默认数据源。在实际的项目开发中,不可能总是在访问数据库之前,调用DataSourceContextHolder .set
设置数据源,这样不好维护、繁琐、代码可阅读性也不好。所以,可以自定义一个注解,用于标识当前方法是要切换到数据源,然后用一个切面(AOP)在进入方法前指定数据源、退出方法前清空数据源。
import java.lang.annotation.*;
/**
* 数据源注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface DS {
/**
* 没有设置数据源名称,默认取default名称数据源
*/
String value() default "default";
}
通过AOP+注解
实现数据源的动态切换,在调用切入点下面的方法时,会先进入前置通知方法
中,将注解@DS
配置数据源名称设置到DataSourceContextHolder的ThreadLocal中
,在方法返回前清空ThreadLocal设置的数据源
。
@Order
是很重要的,必须确保DynamicDataSourceAspect 的执行优先于TranctionInterceptor。不然数据源的指定就无法生效(数据源的指定在数据库连接的获取之后!!)
/**
* 动态数据源AOP切面
*/
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//切点
@Pointcut("execution(* com.oyjp.ds3.service..*(..))")
public void aspect() {
}
//调用方法前结束后,根据注解@DS设置数据源
@Before("aspect()")
private void before(JoinPoint point) {
Object target = point.getTarget();
String method = point.getSignature().getName();
Class<?> classz = target.getClass();// 获取目标类
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
.getMethod().getParameterTypes();
try {
Method m = classz.getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(DS.class)) {
DS data = m.getAnnotation(DS.class);
logger.info("method :{},datasource:{}", m.getName(), data.value());
DataSourceContextHolder.set(data.value());// 数据源放到当前线程中
}
} catch (Exception e) {
logger.error("get datasource error ", e);
//默认选择master
DataSourceContextHolder.set("master");// 数据源放到当前线程中
}
}
//调用方法结束后,清空数据源
@AfterReturning("aspect()")
public void after(JoinPoint point) {
DataSourceContextHolder.clear();
}
}
//指定aop事务执行顺序,已保证在切换数据源的后面
@EnableTransactionManagement(order = 2)
//排除数据源自动配置
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
在需要切换数据源的service的方法上面标注自定义注解@DS
即可
mapper接口
@Mapper
public interface SysTaskMapper {
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
SysTask queryById(@Param("id") Integer id);
}
Mapper文件
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.oyjp.ds3.mapper.SysTaskMapper">
<resultMap type="com.oyjp.ds3.bean.SysTask" id="SysTaskMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="jobName" column="job_name" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
<result property="cronExpression" column="cron_expression" jdbcType="VARCHAR"/>
<result property="beanClass" column="bean_class" jdbcType="VARCHAR"/>
<result property="jobStatus" column="job_status" jdbcType="VARCHAR"/>
<result property="jobGroup" column="job_group" jdbcType="VARCHAR"/>
<result property="createUser" column="create_user" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateUser" column="update_user" jdbcType="VARCHAR"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
resultMap>
<select id="queryById" resultMap="SysTaskMap">
select * from sys_task where id = #{id}
select>
mapper>
Service接口
public interface SysTaskService {
/**
* 查询db01数据源
*
* @param id
* @return
*/
SysTask queryById1(Integer id);
/**
* 查询db02数据源
*
* @param id
* @return
*/
SysTask queryById2(Integer id);
/**
* 查询db03数据源
*
* @param id
* @return
*/
SysTask queryById3(Integer id);
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
SysTask queryById(Integer id);
}
Service接口实现类
@Service("SysTaskService")
public class SysTaskServiceImpl implements SysTaskService {
@Resource
private SysTaskMapper SysTaskMapper;
/**
*查询单条数据源,没有指定数据源,查询默认数据db01的数据
*/
@Override
public SysTask queryById(Integer id) {
return this.SysTaskMapper.queryById(id);
}
/**
* 查询db01数据源原
*/
@Override
@DS("db01")
public SysTask queryById1(Integer id) {
return this.SysTaskMapper.queryById(id);
}
/**
* 查询db02数据源原
*/
@Override
@DS("db02")
public SysTask queryById2(Integer id) {
return this.SysTaskMapper.queryById(id);
}
/**
* 查询db03数据源原
*/
@Override
@DS("db03")
public SysTask queryById3(Integer id) {
return this.SysTaskMapper.queryById(id);
}
}
伪代码-controller
@RestController
public class SysTaskController {
Logger log = LoggerFactory.getLogger(SysTaskController.class);
/**
* 服务对象
*/
@Resource
private SysTaskService sysTaskService;
/**
* 切换到数据源db01
*/
@GetMapping("/tasks1/{id}")
public SysTask selectOne1(@PathVariable("id") Integer id) {
return this.sysTaskService.queryById1(id);
}
/**
* 切换到数据源db02
*/
@GetMapping("/tasks2/{id}")
public SysTask selectOne2(@PathVariable("id") Integer id) {
return this.sysTaskService.queryById2(id);
}
/**
* 切换到数据源db03,没有配置此数据源,因此默认查询的是db01的数据
*/
@GetMapping("/tasks3/{id}")
public SysTask selectOne3(@PathVariable("id") Integer id) {
return this.sysTaskService.queryById3(id);
}
}
也可以在方法内通过DataSourceContextHolder.set()方法修改数据源名称,从而在进行数据源操作时触发DynamicDataSource.determineCurrentLookupKey获取到我们修改后的数据源名称
/**
* 切换到db01数据源
*/
@GetMapping("/tasks4/{id}")
public SysTask selectOne4(@PathVariable("id") Integer id) {
DataSourceContextHolder.set("db01");
return this.sysTaskService.queryById(id);
}
/**
* 切换到db02数据源
*/
@GetMapping("/tasks5/{id}")
public SysTask selectOne5(@PathVariable("id") Integer id) {
DataSourceContextHolder.set("db02");
return this.sysTaskService.queryById(id);
}
/**
* 切换到db03数据源,没有配置此数据源,因此默认查询的是db01的数据
*/
@GetMapping("/tasks6/{id}")
public SysTask selectOne6(@PathVariable("id") Integer id) {
DataSourceContextHolder.set("db03");
return this.sysTaskService.queryById(id);
}
基于上面的代码进行新增
/**
* 实现ApplicationContextAware接口设置applicationContext
* 提供static方法供调用者使用,不要求使用者受spring容器管理
*/
@Component
public class SpringContext implements ApplicationContextAware {
public static ApplicationContext applicationContext;
public static ApplicationContext getInstance() {
return SpringContext.applicationContext;
}
public static Object getBean(String name) {
return getInstance().getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return getInstance().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return getInstance().getBean(name, clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContext.applicationContext = applicationContext;
}
}
动态数据源服务类,提供一些动态添加数据源、删除数据源方法
@Service
public class DynamicDataSourceService {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceService.class);
//可重入锁
private final Lock lock = new ReentrantLock();
/**
* 删除数据源
*
* @param dsName 数据源名称
* @return
*/
public boolean delDataSources(String dsName) {
lock.lock();
try {
//获取Spring容器动态数据源对象
DynamicDataSource dynamicDataSource = SpringContext.getInstance().getBean(DynamicDataSource.class);
//获取已加载的所有数据源
Map<Object, Object> dynamicTargetDataSources = dynamicDataSource.getDynamicTargetDataSources();
//返回结果
boolean result = false;
//如果存在当前数据源
if (dynamicTargetDataSources.containsKey(dsName)) {
//获取Druid管理下的数据源
Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
for (DruidDataSource druidDataSource : druidDataSourceInstances) {
if (druidDataSource.getName().equals(dsName)) {
//删除数据源监控列表
DruidDataSourceStatManager.removeDataSource(druidDataSource);
//删除数据源
dynamicTargetDataSources.remove(dsName);
//将删除后的数据源集合赋值给父类的TargetDataSources
dynamicDataSource.setTargetDataSources(dynamicTargetDataSources);
log.info(dsName + "数据源删除成功");
result = true;
break;
}
}
}
return result;
} finally {
lock.unlock();
}
}
/**
* 创建Druid数据源
*
* @param dsName 数据源名称
* @param driveClass 数据源驱动
* @param url 数据库url
* @param username 用户名
* @param password 密码
* @return
*/
public boolean addDataSource(String dsName, String driveClass, String url, String username, String password) {
lock.lock();
try {
//测试当前连接
if (!testDatasource(dsName, driveClass, url, username, password)) return false;
//获取Spring容器动态数据源对象
DynamicDataSource dynamicDataSource = SpringContext.getInstance().getBean(DynamicDataSource.class);
//获取已加载的所有数据源
Map<Object, Object> dynamicTargetDataSources = dynamicDataSource.getDynamicTargetDataSources();
//创建Druid数据源
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setName(dsName);
druidDataSource.setDriverClassName(driveClass);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
druidDataSource.init();
//添加数据源
dynamicTargetDataSources.put(dsName, druidDataSource);
//将删除后的数据源集合赋值给父类的TargetDataSources
dynamicDataSource.setTargetDataSources(dynamicTargetDataSources);
log.info(dsName + "数据源初始化成功");
log.info(dsName + "数据源的概况:" + druidDataSource.dump());
return true;
} catch (Exception e) {
log.error(dsName + "数据源初始化异常:{}",e.getMessage());
return false;
} finally {
lock.unlock();
}
}
/**
* 测试数据源是否能够连接
*
* @param dsName 数据源名称
* @param driveClass 驱动
* @param url url
* @param username 账号
* @param password 密码
* @return true可以 false不可以
*/
public boolean testDatasource(String dsName, String driveClass, String url, String username, String password) {
try {
Class.forName(driveClass);
//设置连接数据库的等待时间,单位秒
DriverManager.setLoginTimeout(3);
DriverManager.getConnection(url, username, password);
log.info(dsName + "数据源建立连接正常");
return true;
} catch (Exception e) {
log.info(dsName + "数据源建立连接异常:{}", e.getMessage());
return false;
}
}
}
@RestController
public class SysTaskController {
Logger log = LoggerFactory.getLogger(SysTaskController.class);
/**
* 服务对象
*/
@Resource
private SysTaskService sysTaskService;
@Resource
private DBService dbService;
@Resource
private DynamicDataSourceService dynamicDataSourceService;
/**
* 切换到数据源db01-aop
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("/tasks1/{id}")
public SysTask selectOne1(@PathVariable("id") Integer id) {
return this.sysTaskService.queryById1(id);
}
/**
* 切换到数据源db02-aop
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("/tasks2/{id}")
public SysTask selectOne2(@PathVariable("id") Integer id) {
return this.sysTaskService.queryById2(id);
}
/**
* 切换到数据源db02-aop
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("/tasks3/{id}")
public SysTask selectOne3(@PathVariable("id") Integer id) {
return this.sysTaskService.queryById3(id);
}
/**
* 动态加载数据源
*/
@GetMapping("/tasks/addDataSource")
public String addDs() {
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/springboot_quertz3?characterEncoding=utf-8&serverTimezone=UTC";
String username = "root";
String password = "root";
String dbName = "db03";
//动态加载数据源
if (dynamicDataSourceService.addDataSource(dbName, driverClassName, url, username, password)) {
return "success";
}
return "fail";
}
/**
* 动态加载数据源
*/
@GetMapping("/tasks/delDataSource/{dsName}")
public String delDs(@PathVariable("dsName") String dsName) {
//删除数据源
if (dynamicDataSourceService.delDataSources(dsName)) {
return "success";
}
return "fail";
}
}
AbstractRoutingDataSource 只支持单库事务
,也就是说切换数据源要在开启事务之前执行
。
DataSourceTransactionObject
对象中进行后续的commit rollback
等事务操作。出现多数据源动态切换失败的原因是因为在事务开启后
,数据源就不能再进行随意切换了,也就是说,一个事务对应一个数据源
。
传统的Spring管理事务是放在·Service业务层操作的
,所以更换数据源的操作要放在这个操作之前进行
。也就是切换数据源操作放在Controller层,可是这样操作会造成Controller层代码混乱的结果。
Springboot 从数据库读取数据库配置信息,动态切换多数据源 最详细实战教程
Springboot 整合druid+mybatis+jta分布式事务+多数据源aop注解动态切换 (一篇到位)
在使用 Spring Boot 和 MyBatis 动态切换数据源时遇到的问题以及解决方法