1.概述
繼承AbstractRoutingDataSource接口實現讀寫分離配置。使用的主要技術如下:
- SpringBoot 2.1.12.RELEASE
- MybatisPlus
- alibaba.druid數據庫連接池
- mysql數據庫
- SpringAop
2.配置文件
mybatis-plus:
# 如果是放在src/main/java目錄下 classpath:/com/yourpackage/*/mapper/*Mapper.xml
# 如果是放在resource目錄 classpath:/mapper/*Mapper.xml
mapper-locations: classpath:com/bbdog/dao/xml/*Mapper.xml
#實體掃描,多個package用逗號或者分號分隔
typeAliasesPackage: com.bbdog.dao.model
global-config:
#主鍵類型 0:"數據庫ID自增", 1:"用戶輸入ID",2:"全局唯一ID (數字類型唯一ID)", 3:"全局唯一ID UUID";
id-type: 0
#字段策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷"
field-strategy: 1
#刷新mapper 調試神器
refresh-mapper: true
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
#配置JdbcTypeForNull
jdbc-type-for-null: 'null'
spring:
datasource:
master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.virtualIp}:3306/bbdog
username: master
password: ******
#----數據庫連接池配置----------------------
# 下面為連接池的補充設置,應用到上面所有數據源中
# 初始化大小,最小,最大
initialSize: 5
minIdle: 1
maxActive: 50
# 配置獲取連接等待超時的時間
maxWait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打開PSCache,並且指定每個連接上PSCache的大小
poolPreparedStatements: false
#maxPoolPreparedStatementPerConnectionSize: 20
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
filters: stat,wall,slf4j
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
connectionProperties:
druid:
stat:
mergeSql: true
slowSqlMillis: 5000
# 合並多個DruidDataSource的監控數據
#useGlobalDataSourceStat: true
slave:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.virtualIp}:3306/bbdog
username: slave
password: ******
#----數據庫連接池配置----------------------
# 下面為連接池的補充設置,應用到上面所有數據源中
# 初始化大小,最小,最大
initialSize: 5
minIdle: 1
maxActive: 50
# 配置獲取連接等待超時的時間
maxWait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打開PSCache,並且指定每個連接上PSCache的大小
poolPreparedStatements: false
#maxPoolPreparedStatementPerConnectionSize: 20
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
filters: stat,wall,slf4j
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
connectionProperties:
druid:
stat:
mergeSql: true
slowSqlMillis: 5000
# 合並多個DruidDataSource的監控數據
#useGlobalDataSourceStat: true
3.SpringBoot啟動類設置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) // 設置動態數據源需要,禁用數據源自動配置
@EnableTransactionManagement//開啟springBoot事務
@MapperScan("com.bbdog.dao.mapper*")
@EnableCaching//開啟基於注解的緩存
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}
4.創建數據源類型
public enum SourceName {
read("read"), write("write");
private String value;
SourceName(String value) {
this.value = value;
}
public String value() {
return this.value;
}
}
5.構建切換數據源類
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 將 read 數據源的 key作為默認數據源的 key
*/
@Override
protected String initialValue() {
return SourceName.read.value();
}
};
/**
* 數據源的 key集合,用於切換時判斷數據源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切換數據源
*
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 獲取數據源
*
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 重置數據源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* 判斷是否包含數據源
*
* @param key 數據源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加數據源keys
*
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
6.繼承AbstractRoutingDataSource接口實現動態數據源
public class AutoChooseDataSource extends AbstractRoutingDataSource {
/**
* 如果不希望數據源在啟動配置時就加載好,可以定制這個方法,從任何你希望的地方讀取並返回數據源
* 比如從數據庫、文件、外部接口等讀取數據源信息,並最終返回一個DataSource實現類對象即可
*/
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
/**
* 如果希望所有數據源在啟動配置時就加載好,這里通過設置數據源Key值來切換數據,定制這個方法
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* 設置默認數據源
*
* @param defaultDataSource
*/
@Override
public void setDefaultTargetDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
/**
* 設置數據源
*
* @param dataSources
*/
@Override
public void setTargetDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
// 將數據源的 key 放到數據源上下文的 key 集合中,用於切換時判斷數據源是否有效
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
7.數據源配置類設置
參照自動配置類MybatisPlusAutoConfiguration.java中的SqlSessionFactory配置來為添加自己的動態數據源
@SuppressWarnings("ConstantConditions")
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({SqlSessionFactory.class, MybatisSqlSessionFactoryBean.class})
@ConditionalOnBean(DataSource.class)//容器中有DataSource類就可以調用該配置類的方法了
@EnableConfigurationProperties(MybatisPlusProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisPlusAutoConfiguration {
...
/*
將自己配置的動態數據源放入容器中,容器會自動注入到該方法的入參。
由於容器中有多個DataSource類,所以要將自己的動態數據源設置為默認@Primary
*/
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
....
return factory.getObject();
}
}
數據源配置類內容:
@Configuration
public class DruidConfiguration {
/**
* 動態數據源配置**********************************↓↓↓↓↓↓↓↓↓↓↓↓↓↓
***************************/
@Bean(name = "write", destroyMethod = "close", initMethod = "init")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource master() {
return druidDataSource();
}
@Bean(name = "read", destroyMethod = "close", initMethod = "init")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slave() {
return druidDataSource();
}
@Bean("dataSource")
@Primary//自動裝配時當出現多個Bean候選者時,被注解為@Primary的Bean將作為首選者,否則將拋出異常
public DataSource autoChooseDataSource() {
AutoChooseDataSource autoChooseDataSource = new AutoChooseDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(SourceName.write.value(), master());
dataSourceMap.put(SourceName.read.value(), slave());
// 將 read 數據源作為默認指定的數據源
autoChooseDataSource.setDefaultTargetDataSource(slave());
// 將 read 和 write 數據源作為指定的數據源
autoChooseDataSource.setTargetDataSources(dataSourceMap);
return autoChooseDataSource;
}
@Bean
public PlatformTransactionManager transactionManager() {
// 配置事務管理, 使用事務時在方法頭部添加@Transactional注解即可
return new DataSourceTransactionManager(autoChooseDataSource());
}
/**
* 動態數據源配置**********************************↑↑↑↑↑↑↑↑↑↑↑↑↑↑
***************************/
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
8.創建數據源切換注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 數據源key值
* @return
*/
SourceName value();
}
9.創建數據源切換切面
@Aspect
@Order(-1) // 該切面應當先於 @Transactional 執行
@Component
public class DynamicDataSourceAspect {
private static Logger _log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
/**
* 切換數據源
*
* @param point
* @param dataSource
*/
@Before("@annotation(dataSource))")
public void switchDataSource(JoinPoint point, DataSource dataSource) {
if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value().name())) {
_log.error("DataSource [{}] 不存在,使用默認 DataSource [{}] ",
dataSource.value(),
DynamicDataSourceContextHolder.getDataSourceKey());
} else {
// 切換數據源
DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().name());
_log.debug("切換 DataSource 至 [{}] ,引起切換方法是 [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(),
point.getSignature());
}
}
/**
* 重置數據源
*
* @param point
* @param dataSource
*/
@After("@annotation(dataSource))")
public void restoreDataSource(JoinPoint point, DataSource dataSource) {
// 將數據源置為默認數據源
DynamicDataSourceContextHolder.clearDataSourceKey();
_log.debug("重置 DataSource 至 [{}] ,引起重置的方法是 [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(),
point.getSignature());
}
}
10.示例
/**
* 刪除角色
*
* @param role
* @return
*/
@Override
@DataSource(SourceName.write)
@Transactional
public R deleteRoleById(Role role) {
role.setUpdateTime(Utils.getCurrentFormatDateStr());
role.setValid("0");
Integer delete = baseMapper.updateById(role);
this.clearPermissionsInRoleId(role.getRoleId());
return new R(1 == delete);
}