一、正常使用流程
https://www.kancloud.cn/tracy5546/dynamic-datasource
特性
- 支持 數據源分組 ,適用於多種場景 純粹多庫 讀寫分離 一主多從 混合模式。
- 支持數據庫敏感配置信息 加密 ENC()。
- 支持每個數據庫獨立初始化表結構schema和數據庫database。
- 支持無數據源啟動,支持懶加載數據源(需要的時候再創建連接)。
- 支持 自定義注解 ,需繼承DS(3.2.0+)。
- 提供並簡化對Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供對Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等組件的集成方案。
- 提供 自定義數據源來源 方案(如全從數據庫加載)。
- 提供項目啟動后 動態增加移除數據源 方案。
- 提供Mybatis環境下的 純讀寫分離 方案。
- 提供使用 spel動態參數 解析數據源方案。內置spel,session,header,支持自定義。
- 支持 多層數據源嵌套切換 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 **基於seata的分布式事務方案。
- 提供 本地多數據源事務方案。
#約定
- 本框架只做 切換數據源 這件核心的事情,並不限制你的具體操作,切換了數據源可以做任何CRUD。
- 配置文件所有以下划線
_
分割的數據源 首部 即為組的名稱,相同組名稱的數據源會放在一個組下。 - 切換數據源可以是組名,也可以是具體數據源名稱。組名則切換時采用負載均衡算法切換。
- 默認的數據源名稱為 master ,你可以通過
spring.datasource.dynamic.primary
修改。 - 方法上的注解優先於類上注解。
- DS支持繼承抽象類上的DS,暫不支持繼承接口上的DS。
使用
- 引入dynamic-datasource-spring-boot-starter。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
- 配置數據源。
spring:
datasource:
dynamic:
primary: master #設置默認的數據源或者數據源組,默認值即為master
strict: false #嚴格匹配數據源,默認false. true未匹配到指定數據源時拋異常,false使用默認數據源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0開始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 內置加密,使用請查看詳細文檔
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上會配置一個默認庫master,一個組slave下有兩個子庫slave_1,slave_2
# 多主多從 純粹多庫(記得設置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
- 使用 @DS 切換數據源。
@DS 可以注解在方法上或類上,同時存在就近原則 方法上注解 優先於 類上注解。
注解 | 結果 |
---|---|
沒有@DS | 默認數據源 |
@DS("dsName") | dsName可以為組名也可以為具體某個庫的名稱 |
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
二、源碼調試
- 開啟動態數據源的debug日志。
logging:
level:
com.baomidou.dynamic: debug
檢查日志輸出是否正確。
- 斷點調試DynamicDataSourceAnnotationInterceptor。
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
private static final String DYNAMIC_PREFIX = "#";
private final DataSourceClassResolver dataSourceClassResolver;
private final DsProcessor dsProcessor;
public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
this.dsProcessor = dsProcessor;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
//這里把獲取到的數據源標識如master存入本地線程
String dsKey = determineDatasourceKey(invocation);
● DynamicDataSourceContextHolder.push(dsKey);
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
- 斷點調試DynamicRoutingDataSource。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();
private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap<>();
@Override
public DataSource determineDataSource() {
//從本地線程獲取key解析最終真實的數據源
● String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}
private DataSource determinePrimaryDataSource() {
log.debug("從默認數據源中返回數據");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
return determinePrimaryDataSource();
} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("從 {} 組數據源中返回數據源", ds);
return groupDataSources.get(ds).determineDataSource();
} else if (dataSourceMap.containsKey(ds)) {
log.debug("從 {} 單數據源中返回數據源", ds);
return dataSourceMap.get(ds);
}
return determinePrimaryDataSource();
}
}
不可用版本
某些版本存在一些問題,強烈不建議使用。
強烈推薦使用最新版本。
以下版本不建議使用。
- v3.3.3 嚴重BUG-Advisor啟動失敗導致無法切換數據源 。
- v3.3.0 打包后啟動強制依賴seata。
- v3.1.0 使用NamedInheritableThreadLocal導致並發高切換數據源失敗。
- v2.3.3 嵌套切換數據源會導致會到主數據源。
- v2.3.2 記不清具體BUG了,不用就對了。
- v2.3.1 記不清具體BUG了,不用就對了。
- v2.2.2 記不清具體BUG了,不用就對了。
三、@DS注解失效的情況
1.開啟了spring的事務。
原因: spring開啟事務后會維護一個ConnectionHolder,保證在整個事務下,都是用同一個數據庫連接。
請檢查整個調用鏈路涉及的類的方法和類本身還有繼承的抽象類上是否有@Transactional
注解。
2.方法內部調用。
查看以下示例 回答 外部調用 userservice.test1()
能在執行到 test2()
切換到second數據源嗎?
public UserService {
@DS("first")
public void test1() {
// do something
test2();
}
@DS("second")
public void test2() {
// do something
}
}
答案:!!!不能不能不能!!!! 數據源核心原理是基於aop代理實現切換,內部方法調用不會使用aop。
解決方法:
把test2()方法提到另外一個service,單獨調用。
3.shiroAop代理。
在shiro框架中(UserRealm)使用@Autowire 注入的類, 緩存注解和事務注解都失效。
@Component
public class UserRealm extends AuthorizingRealm {
@Lazy
@Autowired
private IUserService userService;
//... 省略其他無關的內容
}
解決方法:
1.在Shiro框架中注入Bean時, 不使用@Autowire, 使用ApplicationContextRegister.getBean()方法,手動注入bean。
2.使用@Autowire+@Lazy注解,設置注入到Shiro框架的Bean延時加載(推薦)。
4.PostConstruct初始化順序。
初始化包括: @PostConstruct 注解, InitializingBean接口, 自定義init-method
@Component
public class MyConfiguration {
@Resource
private UserMapper userMapper;
@DS("slave")
@PostConstruct
public void init(){
// 無法選擇正確的數據源
userMapper.selectById(1);
}
}
解決方法:監聽容器啟動完成事件, 在容器完成后做初始化。
@Component
public class MyConfiguration {
@DS("slave")
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
// 成功選擇正確的數據源
userMapper.selectById(1);
}
}
相關spring源碼 : `org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
在初始化完成后, bean 進入增強階段, 所以這個階段的任何AOP都是無效的。
5.Druid版本過低。
請升級druid1.1.22及以上版本,這個版本修復了在高並發下偶發的切換失效問題。
6.@Async或者java8的ParallelStream並行流之類方法。
這種情況都是新開了線程去處理,不受當前線程管控了。 可以在新開的方法上加對應的DS注解解決。
擴展閱讀
shiro失效原因
在spring初始化bean的階段中,大致上分為三段: BeanFactoryPostProcessor -> BeanPostProcessor -> Bean
這三種都是單例bean. 只不過會按照這個優先級進行初始化, shiro引起AOP失效的原因:
ShiroFilterFactoryBean 是一個 BeanPostProcessor , 比普通的單例Bean都優先加載, 所以他依賴注入的bean都無法正確的進行切面。
ShiroFilterFactoryBean 實際的依賴情況:
ShiroFilterFactoryBean -> SecurityManager -> UserRealm -> IUserService
IUserService依賴的其他 service 也會失效
IUserService-> MenuService -> RoleService