主要的配置類就是這五個: DsAspect、 DataSourceConfiguration 、MyRoutingDataSource、MybatisConfiguration、TransactionConfig。後面我逐個的解釋下每一個類的做用。css
spring:
# 數據源配置
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
defaultDs: master
master:
name: master
url: jdbc:mysql://ip:3306/wx_edu?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
initial-size: 10
min-idle: 10
max-active: 100
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT version()
validation-query-timeout: 10000
test-while-idle: true
test-on-borrow: false
test-on-return: false
remove-abandoned: true
remove-abandoned-timeout: 86400
filters: stat,wall
connection-properties: druid.stat.mergeSql=true;
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
config:
enabled: true
# slave 數據源
slave:
name: slave
url: jdbc:mysql://ip:3307/wx_edu?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#鏈接參數
initial-size: 10
min-idle: 10
max-active: 100
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT version()
validation-query-timeout: 10000
test-while-idle: true
test-on-borrow: false
test-on-return: false
remove-abandoned: true
remove-abandoned-timeout: 86400
filters: stat,wall
connection-properties: druid.stat.mergeSql=true;
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
config:
enabled: true
mybatis-plus:
global-config:
#主鍵類型 0:"數據庫ID自增", 1:"用戶輸入ID",2:"全局惟一ID (數字類型惟一ID)", 3:"全局惟一ID UUID";
id-type: 0
#字段策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷"
field-strategy: 0
#駝峯下劃線轉換
db-column-underline: true
#刷新mapper 調試神器
refresh-mapper: true
#數據庫大寫下劃線轉換
#capital-mode: true
#邏輯刪除配置(下面3個配置)
logic-delete-value: 0
logic-not-delete-value: 1
# SQL 解析緩存,開啓後多租戶 @SqlParser 註解生效
# sql-parser-cache: true
複製代碼
主要是配置多個數據源的Bean,上代碼:html
@Configuration
public class DataSourceConfiguration {
/** * 默認是數據源 */
@Value("${spring.datasource.druid.defaultDs}")
private String defaultDs;
@Bean(name = "dataSourceMaster")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource dataSourceMaster() {
DataSource druidDataSource = DruidDataSourceBuilder.create().build();
DbContextHolder.addDataSource(CommonEnum.DsType.DS_MASTER.getValue(), druidDataSource);
return druidDataSource;
}
@Bean(name = "dataSourceSlave")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource dataSourceSlave() {
DataSource druidDataSource = DruidDataSourceBuilder.create().build();
DbContextHolder.addDataSource(CommonEnum.DsType.DS_SLAVE.getValue(), druidDataSource);
return druidDataSource;
}
@Bean(name = "myRoutingDataSource")
public MyRoutingDataSource dataSource(@Qualifier("dataSourceMaster") DataSource dataSourceMaster, @Qualifier("dataSourceSlave") DataSource dataSourceSlave) {
MyRoutingDataSource dynamicDataSource = new MyRoutingDataSource();
Map<Object, Object> targetDataResources = new HashMap<>();
targetDataResources.put(CommonEnum.DsType.DS_MASTER.getValue(), dataSourceMaster);
targetDataResources.put(CommonEnum.DsType.DS_SLAVE.getValue(), dataSourceSlave);
//設置默認數據源
dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster);
dynamicDataSource.setTargetDataSources(targetDataResources);
DbContextHolder.setDefaultDs(defaultDs);
return dynamicDataSource;
}
}
複製代碼
這個沒啥好解釋的,就是把配置文件封裝成了dataSource的Bean,其中MyRoutingDataSource
纔是咱們要用的數據源,包括事務配置也要用它。java
MyRoutingDataSourcemysql
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getCurrentDsStr();
}
}
複製代碼
其中AbstractRoutingDataSource
是Spring的jdbc模塊下提供的一個抽象類,該類充當了DataSource
的路由中介, 能在運行時, 根據某種key值來動態切換到真正的DataSource
上,重寫其中的determineCurrentLookupKey()
方法,能夠實現數據源的切換。意思就是想玩多數據源就使用這個類就對了。我這裏還用到了一個DbContextHolder
工具類(至關於數據源的持有者),代碼以下,基本上是在網上拷貝的,其中作了一點點修改:web
public class DbContextHolder {
/** * 項目中配置數據源 */
private static Map<String, DataSource> dataSources = new ConcurrentHashMap<>();
/** * 默認數據源 */
private static String defaultDs = "";
/** * 爲何要用鏈表存儲(準確的是棧) * <pre> * 爲了支持嵌套切換,如ABC三個service都是不一樣的數據源 * 其中A的某個業務要調B的方法,B的方法須要調用C的方法。一級一級調用切換,造成了鏈。 * 傳統的只設置當前線程的方式不能知足此業務需求,必須模擬棧,後進先出。 * </pre> */
private static final ThreadLocal<Deque<String>> contextHolder = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
/** * 設置當前線程使用的數據源 * * @param dsName */
public static void setCurrentDsStr(String dsName) {
if (StringUtils.isBlank(dsName)) {
log.error("==========>dbType is null,throw NullPointerException");
throw new NullPointerException();
}
if (!dataSources.containsKey(dsName)) {
log.error("==========>datasource not exists,dsName={}", dsName);
throw new RuntimeException("==========>datasource not exists,dsName={" + dsName +"}");
}
contextHolder.get().push(dsName);
}
/** * 獲取當前使用的數據源 * * @return */
public static String getCurrentDsStr() {
return contextHolder.get().peek();
}
/** * 清空當前線程數據源 * <p> * 若是當前線程是連續切換數據源 * 只會移除掉當前線程的數據源名稱 * </p> */
public static void clearCurrentDsStr() {
Deque<String> deque = contextHolder.get();
deque.poll();
if (deque.isEmpty()){
contextHolder.remove();
}
}
/** * 添加數據源 * * @param dsName * @param dataSource */
public static void addDataSource(String dsName, DataSource dataSource) {
if (dataSources.containsKey(dsName)) {
log.error("==========>dataSource={} already exist", dsName);
//throw new RuntimeException("dataSource={" + dsName + "} already exist");
return;
}
dataSources.put(dsName, dataSource);
}
/** * 獲取指定數據源 * * @return */
public static DataSource getDefaultDataSource() {
if (StringUtils.isBlank(defaultDs)) {
log.error("==========>default datasource must be configured");
throw new RuntimeException("default datasource must be configured.");
}
if (!dataSources.containsKey(defaultDs)) {
log.error("==========>The default datasource must be included in the datasources");
throw new RuntimeException("==========>The default datasource must be included in the datasources");
}
return dataSources.get(defaultDs);
}
/** 設置默認數據源 * @param defaultDsStr */
public static void setDefaultDs(String defaultDsStr) {
defaultDs = defaultDsStr;
}
/**獲取全部 數據源 * @return */
public static Map<String, DataSource> getDataSources() {
return dataSources;
}
/** * @return */
public static String getDefaultDs() {
return defaultDs;
}
複製代碼
這是MybatisPlus配置類,若是你用的是Mybatis要簡單一點。由於Mybatis只須要配置SqlSessionFactory
,而 MybatisPlus是配置MybatisSqlSessionFactoryBean
spring
@Slf4j
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})
@MapperScan(basePackages = {"com.sqt.edu.*.mapper*","com.sqt.edu.*.api.mapper*"})
public class MybatisConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "myRoutingDataSource") MyRoutingDataSource myRoutingDataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean(name = "mybatisSqlSessionFactoryBean")
@Primary
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier(value = "myRoutingDataSource") DataSource dataSource) throws Exception {
log.info("==========>開始注入 MybatisSqlSessionFactoryBean");
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
Set<Resource> result = new LinkedHashSet<>(16);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*.xml")));
result.addAll(Arrays.asList(resolver.getResources("classpath*:config/mapper/*/*.xml")));
result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*/*.xml")));
} catch (IOException e) {
log.error("獲取【classpath:mapper/*/*.xml,classpath:config/mapper/*/*.xml】資源錯誤!異常信息:{}", e);
}
bean.setMapperLocations(result.toArray(new org.springframework.core.io.Resource[0]));
bean.setDataSource(dataSource);
bean.setVfs(SpringBootVFS.class);
com.baomidou.mybatisplus.core.MybatisConfiguration configuration = new com.baomidou.mybatisplus.core.MybatisConfiguration();
configuration.setLogImpl(StdOutImpl.class);
configuration.setMapUnderscoreToCamelCase(true);
//添加 樂觀鎖插件
configuration.addInterceptor(optimisticLockerInterceptor());
bean.setConfiguration(configuration);
GlobalConfig globalConfig = GlobalConfigUtils.defaults();
//設置 字段自動填充處理
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
bean.setGlobalConfig(globalConfig);
log.info("==========>注入 MybatisSqlSessionFactoryBean 完成!");
return bean;
}
}
複製代碼
這裏配置的SqlSessionFactory
和MybatisSqlSessionFactoryBean
都須要MyRoutingDataSource
這個數據源。sql
數據源切換切面配置類數據庫
@Order(0)
@Aspect
@Component
@Slf4j
public class DsAspect {
/** * 配置AOP切面的切入點 * 切換放在service接口的方法上 */
@Pointcut("execution(* com.sqt..service..*Service.*(..))")
public void dataSourcePointCut() {
}
/** * 根據切點信息獲取調用函數是否用TargetDataSource切面註解描述, * 若是設置了數據源,則進行數據源切換 */
@Before("dataSourcePointCut()")
public void before(JoinPoint joinPoint) {
if (StringUtils.isNotBlank(DbContextHolder.getCurrentDsStr())) {
log.info("==========>current thread {} use dataSource[{}]",
Thread.currentThread().getName(), DbContextHolder.getCurrentDsStr());
return;
}
String method = joinPoint.getSignature().getName();
Method m = ((MethodSignature) joinPoint.getSignature()).getMethod();
try {
if (null != m && m.isAnnotationPresent(DS.class)) {
// 根據註解 切換數據源
DS td = m.getAnnotation(DS.class);
String dbStr = td.value();
DbContextHolder.setCurrentDsStr(dbStr);
log.info("==========>current thread {} add dataSource[{}] to ThreadLocal, request method name is : {}",
Thread.currentThread().getName(), dbStr, method);
} else {
DbContextHolder.setCurrentDsStr(DbContextHolder.getDefaultDs());
log.info("==========>use default datasource[{}] , request method name is : {}",
DbContextHolder.getDefaultDs(), method);
}
} catch (Exception e) {
log.error("==========>current thread {} add data to ThreadLocal error,{}", Thread.currentThread().getName(), e);
throw e;
}
}
/** * 執行完切面後,將線程共享中的數據源名稱清空, * 數據源恢復爲原來的默認數據源 */
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint) {
log.info("==========>clean datasource[{}]", DbContextHolder.getCurrentDsStr());
DbContextHolder.clearCurrentDsStr();
}
}
複製代碼
這個類就是一個簡單的切面配置,做用就是在Service方法以前切換數據源,自定義一個DS()
註解,做用到Service方法上而且標明是master仍是slave便可。api
重點來了!重點來了!通過上面那些配置,多數據源已經配置好了。可是此時事務是不生效的,不管你是把@Transactional
做用到Service類上仍是方法上,都不生效!此時你還須要配置一個事務管理器,而且把MyRoutingDataSource
咱們自定義的數據源給事務管理器。看TransactionConfig:緩存
@Aspect
@Configuration
@Slf4j
public class TransactionConfig {
@Autowired
ConfigurableApplicationContext applicationContext;
private static final int TX_METHOD_TIMEOUT = 300;
private static final String AOP_POINTCUT_EXPRESSION = "execution(*com.sqt..service..*Service.*(..))";
@Bean(name = "txAdvice")
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
// 只讀事務,不作更新操做
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 當前存在事務就使用當前事務,當前不存在事務就建立一個新的事務
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
requiredTx.setTimeout(TX_METHOD_TIMEOUT);
Map<String, TransactionAttribute> txMap = new HashMap<>();
txMap.put("add*", requiredTx);
txMap.put("save*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("create*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("batch*", requiredTx);
txMap.put("modify*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("remove*", requiredTx);
txMap.put("exec*", requiredTx);
txMap.put("set*", requiredTx);
txMap.put("do*", requiredTx);
txMap.put("get*", readOnlyTx);
txMap.put("query*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("*", requiredTx);
source.setNameMap(txMap);
TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager(), source);
return txAdvice;
}
@Bean
public Advisor txAdviceAdvisor(@Qualifier("txAdvice") TransactionInterceptor txAdvice) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice);
}
/**自定義 事務管理器 管理咱們自定義的 MyRoutingDataSource 數據源 * @return */
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(applicationContext.getBean(MyRoutingDataSource.class));
return transactionManager;
}
複製代碼
配置DataSourceTransactionManager是重點! ! ! 配置DataSourceTransactionManager是重點! ! !
因爲我是自定義的切面配置事務,因此這個代碼略長。重點是配置事務管理器,而且把咱們動態路由數據源(MyRoutingDataSource)交給事務管理器,這樣咱們的事務纔會回滾!
AbstractRoutingDataSource
,並將多個數據源註冊進去。