SpringBoot配置多數據源/讀寫分離整體步驟
- Jar包引入 spring boot + druid + mybatis plus(多數據源 + 分頁)
- application.yml配置多數據源及mybatis plus mapper配置
- 新建動態數據源DynamicDataSource(繼承AbstractRoutingDataSource),ThreadLocal中獲取當前使用哪個數據源
- 自定義多個Datasource,枚舉類對應各個DataSource,及事務集成
- 編寫AOP,增強Mapper方法的調用處,切換具體數據源
- 測試,數據源切換、插入、分頁查詢等
項目源碼
https://github.com/zc-zangchao/multiple-data-source
POM配置
版本
<springboot.version>2.2.0.RELEASE</springboot.version>
<druid.version>1.1.9</druid.version>
<oracle.version>12.1.0.1.0</oracle.version>
<mysql-connector.version>8.0.18</mysql-connector.version>
<mybatis-plus.version>2.1.9</mybatis-plus.version>
<log4jdbc.version>1.2</log4jdbc.version>
具體jar
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- 多數據源時使用mybatis-plus已經包含了mybatis+pageHelper,所以不需要再引用 mybatis + pageHelper -->
<!-- 多數據源配置支撐 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- log4jdbc jdbc日誌增強 -->
<dependency>
<groupId>com.googlecode.log4jdbc</groupId>
<artifactId>log4jdbc</artifactId>
<version>${log4jdbc.version}</version>
</dependency>
<!-- db connect -->
<!-- oracle -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc7</artifactId>
<version>${oracle.version}</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
yml配置
# mybatis mapper locations
mybatis-plus:
mapper-locations: classpath:mapper/*Mapper.xml
spring:
datasource:
druid:
oracle:
url: jdbc:oracle:thin:@127.0.0.1:1521/db
username: root
password: root
driver-class-name: oracle.jdbc.driver.OracleDriver
max-active: 10
max-wait: 10000
mysql:
# tcpRcvBuf/tcpSndBuf 緩衝區參數 rewriteBatchedStatements batchUpdate參數
url: jdbc:log4jdbc:mysql://127.0.0.1:3306/mydb?allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&tcpRcvBuf=1048576&tcpSndBuf=1048576&socketTimeout=180000&rewriteBatchedStatements=true&autoReconnect=true
username: root
password: root
# 開源 SQL 日誌框架,在大多數情況下極大改善了可讀性及調試工作
driver-class-name: net.sf.log4jdbc.DriverSpy
max-active: 10
max-wait: 600000
# sql監控
filters: stat
# 檢測池裏連接的可用性 開啓影響性能 默認false
test-on-borrow: false
# 指明連接是否被空閒連接回收器(如果有)進行檢驗.如果檢測失敗,則連接將被從池中去除 默認true
test-while-idle: true
# 每30秒運行一次空閒連接回收器
time-between-eviction-runs-millis: 30000
# 檢測語句
validation-query: "select 1"
mysql-backup:
# tcpRcvBuf/tcpSndBuf 緩衝區參數 rewriteBatchedStatements batchUpdate參數
url: jdbc:mysql://127.0.0.1:3306/mydb?allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
max-active: 10
max-wait: 600000
# sql監控
filters: stat
# 檢測池裏連接的可用性 開啓影響性能 默認false
test-on-borrow: false
# 指明連接是否被空閒連接回收器(如果有)進行檢驗.如果檢測失敗,則連接將被從池中去除 默認true
test-while-idle: true
# 每30秒運行一次空閒連接回收器
time-between-eviction-runs-millis: 30000
# 檢測語句
validation-query: "select 1"
動態數據源配置
package com.springboot.demo.service.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
package com.springboot.demo.service.datasource;
public class DataSourceContextHolder {
private DataSourceContextHolder(){}
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dbType){
contextHolder.set(dbType);
}
public static String getDataSource(){
return contextHolder.get();
}
public static void clearDataSource(){
contextHolder.remove();
}
}
DataSource及事務配置
package com.springboot.demo.service.datasource.type;
public enum EnumDataSourceType {
ORACLE,
MYSQL,
MYSQL_BACKUP;
}
package com.springboot.demo.service.datasource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.MybatisConfiguration;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusProperties;
import com.baomidou.mybatisplus.spring.boot.starter.SpringBootVFS;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Resource
private MybatisPlusProperties properties;
@Bean(name = "dataSourceOracle")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.oracle")
public DataSource oracleDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dataSourceMysql")
@ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
public DataSource mysqlDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dataSourceMysqlBackup")
@ConfigurationProperties(prefix = "spring.datasource.druid.mysql-backup")
public DataSource mysqlBackupDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 配置默認數據源
dynamicDataSource.setDefaultTargetDataSource(oracleDataSource());
// 配置多數據源
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(EnumDataSourceType.ORACLE.name(), oracleDataSource());
dataSourceMap.put(EnumDataSourceType.MYSQL.name(), mysqlDataSource());
dataSourceMap.put(EnumDataSourceType.MYSQL_BACKUP.name(), mysqlBackupDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 開啓 PageHelper 的支持
paginationInterceptor.setLocalPage(true);
return paginationInterceptor;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dynamicDataSource());
// mybatis本身的核心庫在springboot打包成jar後有個bug,無法完成別名的掃描
factory.setVfs(SpringBootVFS.class);
org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new MybatisConfiguration();
}
factory.setConfiguration(configuration);
// 分頁功能
factory.setPlugins(new Interceptor[]{ paginationInterceptor()});
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory;
}
// @Bean
// public SqlSessionFactory sqlSessionFactory() throws Exception {
// MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
// sqlSessionFactory.setDataSource(dynamicDataSource());
// sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*Mapper.xml"));
//
// MybatisConfiguration configuration = new MybatisConfiguration();
// configuration.setJdbcTypeForNull(JdbcType.NULL);
// configuration.setMapUnderscoreToCamelCase(true);
// configuration.setCacheEnabled(false);
// sqlSessionFactory.setConfiguration(configuration);
// sqlSessionFactory.setPlugins(new Interceptor[]{
// paginationInterceptor() //添加分頁功能
// });
//
// return sqlSessionFactory.getObject();
// }
/**
* 配置@Transactional註解事務
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
AOP增強 切換數據源
package com.springboot.demo.service.datasource.annotation;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
EnumDataSourceType value() default EnumDataSourceType.ORACLE;
}
package com.springboot.demo.service.datasource.aspect;
import com.springboot.demo.service.datasource.DataSourceContextHolder;
import com.springboot.demo.service.datasource.annotation.TargetDataSource;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import lombok.extern.slf4j.Slf4j;
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.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component
@Aspect
@Slf4j
// 默認優先級 最後執行 可調整Order
public class DynamicDataSourceAspect {
@Before("@annotation(targetDataSource)")
public void before(JoinPoint point, TargetDataSource targetDataSource) {
try {
TargetDataSource annotationOfClass = point.getTarget().getClass().getAnnotation(TargetDataSource.class);
String methodName = point.getSignature().getName();
Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
TargetDataSource methodAnnotation = method.getAnnotation(TargetDataSource.class);
methodAnnotation = methodAnnotation == null ? annotationOfClass : methodAnnotation;
EnumDataSourceType dataSourceType = methodAnnotation != null && methodAnnotation.value() != null ? methodAnnotation.value() : EnumDataSourceType.ORACLE;
DataSourceContextHolder.setDataSource(dataSourceType.name());
} catch (NoSuchMethodException e) {
log.warn("Aspect targetDataSource inspect exception.", e);
}
}
@After("@annotation(targetDataSource)")
public void after(JoinPoint point, TargetDataSource targetDataSource) {
DataSourceContextHolder.clearDataSource();
}
}
測試驗證
驗證數據源切換、事務、分頁
package com.springboot.demo.web.service;
import com.baomidou.mybatisplus.plugins.pagination.PageHelper;
import com.springboot.demo.service.dao.domain.User;
import com.springboot.demo.service.dao.mapper.UserMapper;
import com.springboot.demo.service.datasource.annotation.TargetDataSource;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean addUser(User user) {
int result = userMapper.insert(user);
if (result <= 0) {
throw new RuntimeException("exception");
}
return result > 0;
}
@TargetDataSource(EnumDataSourceType.MYSQL_BACKUP)
public User queryUserById(String userId) {
return userMapper.selectByPrimaryKey(userId);
}
@TargetDataSource(EnumDataSourceType.MYSQL)
public List<User> selectAll(int pageNum, int pageSize) {
// 啓動分頁
PageHelper.startPage(pageNum, pageSize);
return userMapper.selectAll();
}
}
package com.springboot.demo.web.controller;
import com.springboot.demo.service.dao.domain.User;
import com.springboot.demo.web.model.ResultInfo;
import com.springboot.demo.web.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.websocket.server.PathParam;
import java.util.List;
@RestController
@Slf4j
public class UserController {
@Resource
private UserService userService;
@GetMapping("/user/query")
public List<User> queryUsers(@PathParam("pageNum") int pageNum, @PathParam("pageSize") int pageSize) {
log.info("Query users.");
return userService.selectAll(pageNum, pageSize);
}
@GetMapping("/user/queryUserById/{userId}")
public User queryUserById(@PathVariable String userId) {
log.info("Query user by Id.");
return userService.queryUserById(userId);
}
@PostMapping("/user/add")
public ResultInfo addUser(@RequestBody User user) {
log.info("Add user.");
userService.addUser(user);
return new ResultInfo();
}
}
參考:
Spring Boot 整合 Durid數據庫連接池
Spring Boot2.0配置Druid數據庫連接池(單數據源、多數據源、數據監控)
使用springboot + druid + mybatisplus完成多數據源配置
數據連接池默認配置帶來的坑testOnBorrow=false,cloes_wait 終於解決了
關於連接池參數testWhileIdle,testOnBorrow,testOnReturn的疑問
使用druid連接池帶來的坑testOnBorrow=false
MySQL之rewriteBatchedStatements
使用log4jdbc更有效的記錄java sql日誌