SpringBoot 实现多数据源配置
目录
一、多数据源使用场景
在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况:
-
①业务复杂(数据量大)
数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目多个平台,各用各的数据库,涉及数据共享。 -
②读写分离
为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。
很多数据库拥主从架构。也就是,一台主数据库服务器,是对外提供增删改业务的生产服务器;另一台或者多台从数据库服务器,主要进行读的操作。ꞏ
读写分离可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …)进行处理,但是有一些公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离。
二、多数据源的实现
1.springBoot+MyBatis分包方式整合实现
1.1 application.yml配置
server:
port: 8080 # 启动端口
spring:
datasource:
db1: # 数据源1
jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: # 数据源2
jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
1.2连接数据源配置类
@Configuration
@MapperScan(basePackages = "com.example.dao.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class DataSourceConfig1 {
@Primary // 表示默认数据源, 这个注解必须要加,不然spring将分不清楚那个为主数据源
@Bean("db1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db1") //读取yml文件中的配置的数据源参数
public DataSource createDb1DataSource(){
return DataSourceBuilder.create().build();
}
@Primary
@Bean("db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致)
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/db1/*.xml"));
return bean.getObject();
}
// 配置事务
@Bean(name = "db1TransactionManager")
public DataSourceTransactionManager db1TransactionManager(@Qualifier("db1DataSource") DataSource dataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
dataSourceTransactionManager.setRollbackOnCommitFailure(true);
dataSourceTransactionManager.setGlobalRollbackOnParticipationFailure(true);
return dataSourceTransactionManager;
}
@Primary
@Bean("db1SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@Configuration
@MapperScan(basePackages = "com.example.dao.db2", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class DataSourceConfig2 {
@Bean("db2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource getDb1DataSource(){
return DataSourceBuilder.create().build();
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/db2/*.xml"));
return bean.getObject();
}
@Bean(name = "db2TransactionManager")
public DataSourceTransactionManager db2TransactionManager(@Qualifier("db2DataSource") DataSource dataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
dataSourceTransactionManager.setRollbackOnCommitFailure(true);
dataSourceTransactionManager.setGlobalRollbackOnParticipationFailure(true);
return dataSourceTransactionManager;
}
@Bean("db2SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
注:
①需要导入相关springBoot,mybatis依赖包这里不多做介绍
②在 service 层中根据不同的业务注入不同的 dao 层
③如果是主从复制- -读写分离:比如 db1 中负责增删改,db2 中负责查询。但是需要注意的是负责增删改的数据库必须是主库(master)
2.springboot+druid+mybatisplus使用注解整合
2.1application.yml配置
server:
port: 8080
spring:
datasource:
dynamic:
primary: db1 # 配置默认数据库
datasource:
db1: # 数据源1配置
url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: # 数据源2配置
url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 1
max-active: 20
min-idle: 1
max-wait: 60000
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 去除druid配置
DruidDataSourceAutoConfigure会注入一个DataSourceWrapper,其会在原生的spring.datasource下找 url, username, password 等。动态数据源 URL 等配置是在 dynamic 下,因此需要排除,否则会报错。排除方式有两种,一种是上述配置文件排除,还有一种可以在项目启动类排除(@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class))
2.2使用@DS注解来区分不同数据源
给使用非默认数据源添加注解@DS,@DS可以注解在方法上和类上,同时存在方法注解优先于类上注解。
注解在 service 实现或 mapper 接口方法上,不要同时在 service 和 mapper 注解
Mapper上
@DS("db2")
public interface UserMapper extends BaseMapper<User> {
}
Service上
@Service
@DS("db2")
public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements IModelService {}
方法上
@Select("SELECT * FROM user")
@DS("db2")
List<User> selectAll();
2.3 @Transaction 和 @DS 同时使用的问题
问题: 在开发中同时使用这两个注解运用不当很容易出现找不到表的情况,也就是@DS失效没有切换成功,错误情况就不贴出来了(大致内容数据库不存在“xxx”表)。
原因:
- 在插入方法上加@Transactional和@DS注解,数据源没有切换
- 开启事务的同时,会从数据库连接池获取数据库连接;
- 如果内层的service使用@DS切换数据源,只是又做了一层拦截,但是并没有改变整个事务的连接;
- 在这个事务内的所有数据库操作,都是在事务连接建立之后,所以会产生数据源没有切换的问题;
- 为了使@DS起作用,必须替换数据库连接,也就是改变事务的传播机制,产生新的事务,获取新的数据库连接
@Transactional执行流程:
1.service的 upload方法上添加了 @Transactional 注解,Spring事务就会生效。此时,Spring TransactionInterceptor会通过AOP拦截该方法,创建事务。
2.而创建事务,势必就会获得数据源。那么,TransactionInterceptor (事务拦截器) 会使用 Spring DataSourceTransactionManager (数据源事务管理) 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。3.而事务信息,就包括事务对应的 Connection 连接。所以还没走到 Mapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 Mapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 Mapper 添加 @DS 注解所对应的 DataSource 所对应的 Connection 。
DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection:
1.对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用 dynamic-datasource-spring-boot-starter 时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。
2.DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了master、slave 两个数据源,并且默认使用 master 数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于 @DS 获得数据源名,从而获得对应的 DataSource ,如果在 Service 方法上没有添加 @DS 注解,所以它只好返回默认数据源,也就是 master。
当方法上加@Transactional(propagation =Propagation.REQUIRES_NEW),这样在调用另一个事务方法时,TransactionInterceptor会将原事务挂起,开启一个新事务,暂时性的将原事务信息和当前线程解绑。就会重新走一次对应的Connection连接。
开启新事物对原来外部事务影响:内影响外,外不影响内:
- REQUIRES_NEW 会新开启事务,外层事务不会影响内部事务的提交/回滚
- REQUIRES_NEW 的内部事务的异常,会影响外部事务的回滚
解决方案: 在方法上加@Transactional(propagation=Propagation.REQUIRES_NEW),数据源切换,且事务有效。它会重新创建新事务,获取新的数据库连接,从而得到@DS的数据源
3.继承AbstractRoutingDataSource实现
3.1application.yml配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://xxxxxx:3306/db1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/db2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
DruidDataSourceAutoConfigure会注入一个DataSourceWrapper,其会在原生的spring.datasource下找 url, username, password 等。动态数据源 URL 等配置是在 druid下,因此需要排除,否则会报错。排除方式有两种,一种是上述配置文件排除,还有一种可以在项目启动类排除(@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class))
3.2 配置类和继承类
继承类:
/**
* @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
log.info("determineCurrentLookupKey = {}", getDataSource());
return getDataSource();
}
public static void setDataSource(String dataSource) {
log.info("setDataSource = {}", dataSource);
contextHolder.set(dataSource);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
log.info("clean datasource");
contextHolder.remove();
}
}
配置类:
/**
* @description: 设置数据源
**/
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master") // 获取数据源配置
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource(){
Map<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master",defaultDataSource);
dataSourceMap.put("slave",slaveDataSource());
return new DynamicDataSource(defaultDataSource,dataSourceMap);
}
}
设置多数据源的key:
public interface DataSourceNames {
String FIRST = "master";
String SECOND = "slave";
}
3.3 使用
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
DynamicDataSource .setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DynamicDataSource.clearDataSource();
return testUser.getUserName();
}
3.4 自定义注解方式
以上方法需要在每个查询sql前塞入和删除比较麻烦,可以通过注解方式来实现:
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String name() default "";
}
切面类:
@Aspect
@Component
public class DataSourceAspect implements Ordered {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.gdlife.datasources.annotation.DataSource)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource ds = method.getAnnotation(DataSource.class);
if (ds == null) {
DynamicDataSource.setDataSource(DataSourceNames.FIRST);
logger.info("set datasource is " + DataSourceNames.FIRST);
} else {
DynamicDataSource.setDataSource(ds.name());
logger.info("set datasource is " + ds.name());
}
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
logger.info("clean datasource");
}
}
}
使用:
@DataSource(name = DataSourceNames.FIRST )
3.5使用@Transaction问题
使用自定义数据源注解和使用@Transaction注解也会失效,原因同@DS注解,解决方案也同上。