一、多数据源的使用场景
实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景:
1.1 业务复杂/数据量大
一个应用需要用到不同库的数据,就需要连不同的数据库。
比如我们的项目里,一个服务,同时需要用自己mysql表的数据,还要连上大数据的库获取数据。
1.2 读写分离
为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。
很多数据库拥主从架构。也就是,一台主数据库服务器,是对外提供增删改业务的生产服务器;另一(多)台从数据库服务器,主要进行
读的操作。ꞏ
可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …), 但是有一些规模较小的公司,没有专门的中间件团队搭建读写分
离基础设施,因此需要业务开发人员自行实现读写分离
这里的架构与上图类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、
delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力。
二、如何实现多数据源
2.1 AbstractRoutingDataSource原理
spring框架中,spring-jdbc模块提供了AbstractRoutingDataSource,其内部可以包含了多个DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构图如下所示:
应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(ds1、ds2)选择一个,来访问对应的数据库。
1.当执行数据库持久化操作,只要集成了Spring就一定会通过DataSourceUtils获取Connection
2. 通过Spring注入的DataSource获取Connection 即可执行数据库操作
所以思路就是:只需配置一个实现了DataSource的Bean, 然后根据业务动态提供Connection即可
3.其实Spring已经提供一个DataSource实现类用于动态切换数据源——AbstractRoutingDataSource
4.分析AbstractRoutingDataSource即可实现动态数据源切换:
通过这个类可以实现动态数据源切换。如下是这个类的成员变量
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private Map<Object, DataSource> resolvedDataSources;
- targetDataSources 保存了key和数据库连接的映射关系 (所有数据源)
- defaultTargetDataSource标识默认的连接(默认数据源)
- resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系
而AbstractRoutingDataSource实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,通常用作数据初始化。resolvedDataSources就是在这里赋值
@Override
public void afterPropertiesSet() {
//...
this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size()); //初始化resolvedDataSources
//循环targetDataSources,并添加到resolvedDataSources中
for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
//...
}
5.所以,我们只需创建AbstractRoutingDataSource实现类DynamicDataSource然后 始化targetDataSources和key为数据源标识(可以是字符串、枚举、都行,因为标识是Object)、defaultTargetDataSource即可
6.后续当调用AbstractRoutingDataSource.getConnection 会接着调用提供的模板方法:determineTargetDataSource
7.通过determineTargetDataSource该方法返回的数据库标识 从resolvedDataSources 中拿到对应的数据源
8.所以,我们只需DynamicDataSource中实现determineTargetDataSource为其提供一个数据库标识
总结: 在整个代码中我们只需做4件大事:
1.定义AbstractRoutingDataSource实现类DynamicDataSource
2. 初始化时为targetDataSources设置 不同数据源的DataSource和标识、及defaultTargetDataSource
3. 在determineTargetDataSource中提供对应的数据源标识即可
4、切换数据源标识
2.2 通过AbstractRoutingDataSource实现动态数据源
1 . 配置多数据源 和 AbstractRoutingDataSource的自定义实现类:DynamicDataSource
配置多数据
server:
port: 8080
spring:
datasource:
druid:
ds1:
url: jdbc:mysql://localhost:3306/db_ds1?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
ds2:
url: jdbc:mysql://localhost:3306/db_ds2?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
#配置mybatis参数 自定义SqlSessionFactory后这里的自动配置就不生效了
#mybatis:
# type-aliases-package: com.nb.ds.entity
# mapper-locations: classpath:mapper/*.xml
DynamicDataSource 代码:
package com.nb.ds.datasource;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.Map;
/**
* 动态数据源
* @author lihaoyang6
* @date 2024年04月21日 12:05
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
* @param targetDataSources 目标数据源
* @param defaultTargetDataSource 默认数据
*/
public DynamicDataSource(Map<Object, Object> targetDataSources, DataSource defaultTargetDataSource) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
public static void setDataSource(String dataSource){
CONTEXT_HOLDER.set(dataSource);
}
public static void clearDataSource(){
CONTEXT_HOLDER.remove();
}
}
当然ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); 可以提出来到单独的类里面
DataSourceConfig配置类
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.ds1")
public DataSource dataSource1(){
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.ds2")
public DataSource dataSource2(){
return DruidDataSourceBuilder.create().build();
}
@Bean
public DynamicDataSource dynamicDataSource(DataSource dataSource1, DataSource dataSource2){
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DsConstant.ds1,dataSource1);
targetDataSources.put(DsConstant.ds2,dataSource2);
return new DynamicDataSource(targetDataSources,dataSource1);
}
/**
* 当你自定义SqlSessionFactory Bean时,你需要在自定义的SqlSessionFactory中明确地设置别名包和mapper文件的位置。
* 否则,application.yml中的配置可能不会生效。
*当你使用MyBatis的Spring Boot Starter自动配置时,它会根据application.yml中的配置来创建SqlSessionFactory。但当你自定义SqlSessionFactory时,Spring Boot Starter的自动配置就不会生效了,因此,你需要在自定义的SqlSessionFactory中设置这些属性。
*所以,虽然你在application.yml中配置了别名包和mapper文件的位置,但是你还是需要在自定义的SqlSessionFactory中设置这些属性,以确保它们被正确地使用。
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
// 使用我们的动态数据源来构建SqlSessionFactory
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 设置别名包
sessionFactory.setTypeAliasesPackage("com.nb.ds.entity");
// 设置mapper文件的位置
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
return sessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
当你自定义SqlSessionFactory
Bean时,你需要在自定义的SqlSessionFactory中明确地设置别名包和mapper文件的位置。否则,application.yml中的配置可能不会生效。当你使用MyBatis的Spring Boot
Starter自动配置时,它会根据application.yml中的配置来创建SqlSessionFactory。但当你自定义SqlSessionFactory时,Spring
Boot Starter的自动配置就不会生效了,因此,你需要在自定义的SqlSessionFactory中设置这些属性。所以,虽然你在application.yml中配置了别名包和mapper文件的位置,但是你还是需要在自定义的SqlSessionFactory中设置这些属性,以确保它们被正确地使用。
2.3 AOP+自定义注解 切换多数据源
不同业务的数据源: 一般利用AOP,结合自定义注解动态切换数据源:
1.自定义注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
String value() default "";
}
2.切面类
@Slf4j
@Component
@Aspect
public class DynamicDataSourceAspect {
/**
* @annotation:这个表达式的含义是匹配所有带有特定注解的方法。
* 例如,@annotation(com.foo.MyAnnotation)将匹配所有带有@MyAnnotation注解的方法。
* @within:这个表达式的含义是匹配所有在特定注解的类中的方法,不管这个方法本身有没有这个注解。
* 例如,@within(com.foo.MyAnnotation)将匹配所有在带有@MyAnnotation注解的类中的方法。
*/
@Pointcut("@annotation(com.nb.ds.datasource.DS) " +
"|| @within(com.nb.ds.datasource.DS)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Class targetClass = point.getTarget().getClass();
Method method = signature.getMethod();
DS targetDataSource = (DS)targetClass.getAnnotation(DS.class);
DS methodDataSource = method.getAnnotation(DS.class);
if(targetDataSource != null || methodDataSource != null){
String value;
if(methodDataSource != null){
//优先用方法上的
value = methodDataSource.value();
}else {
//类上的
value = targetDataSource.value();
}
DynamicDataSource.setDataSource(value);
log.debug("set datasource is {}", value);
}
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
log.debug("clean datasource");
}
}
}
DsConstant
public class DsConstant {
public static final String ds1 = "ds1";
public static final String ds2 = "ds2";
}
Service类
@Service
public class UserServiceImpl implements IUserService {
@Autowired
UserMapper userMapper;
@DS(DsConstant.ds1)
@Override
public List<User> findAll() {
return userMapper.selectAll();
}
@DS(DsConstant.ds1)
@Override
public void add() {
User user = new User();
user.setName("张三-"+ RandomUtil.randomInt());
user.setAge(RandomUtil.randomInt());
user.setSex("男");
userMapper.insertUser2(user);
}
}
三、事务处理
3.1 Service只涉及一个数据源
单数据源情况下只有一个DataSource ,Spring Boot会自动配置一个DataSourceTransactionManager。然而,如果有多个DataSource bean,Spring Boot将无法决定应该使用哪一个来配置DataSourceTransactionManager,因此它将不会自动配置DataSourceTransactionManager。
当你使用AbstractRoutingDataSource时,你实际上是在多个数据源之间动态选择。对于Spring Boot来说,它并不知道AbstractRoutingDataSource背后的这个动态选择逻辑,所以它无法自动为你配置DataSourceTransactionManager。
因此,你需要手动创建一个DataSourceTransactionManager bean,并且将你的AbstractRoutingDataSource注入到它里面。这样,DataSourceTransactionManager就可以正确地管理你的多数据源事务了。
@Transactional
@DS(DsConstant.ds1)
@Override
public void addUserTs() {
User user = new User();
user.setName("XIAOHONG-"+ RandomUtil.randomInt());
user.setAge(RandomUtil.randomInt());
user.setSex("nv");
userMapper.insertUser2(user);
User user2 = new User();
user2.setName("XIAOHONG-"+ RandomUtil.randomInt());
user2.setAge(RandomUtil.randomInt());
user2.setSex("nv");
userMapper.insertUser2(user2);
throw new RuntimeException("Error~~~~~~~~~~");
}
报错:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.TransactionManager' available
配置多数据下的事务管理器:
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
3.2 Service涉及多个数据源
用分布式事务