前言
有時候爲了減少數據庫的壓力,就要實現數據庫的讀寫分離,這種情況往往是讀多寫少的情況,例如電商平臺。既然數據庫讀寫分離了,那麼代碼層也就需要讀寫不同的數據庫了。實現方法應該有不少,我知道有插件實現,判斷寫請求還是讀請求來請求不同的數據庫,還有代碼實現,不同的SQL訪問不同的數據源,也就是接下來要說的多數據源。
一、基礎介紹
代碼層多數據源的實現方法也有很多,例如不同的包擁有不同的數據源、AOP實現動態切換數據源等等。目前我使用的方法爲AOP動態切換數據源方法,這種方法可以自定義註解,註解在controller層,根據不同的方法訪問不同的數據源。主要實現方法就是先實例化一批數據源實例Map集合,key爲實例名稱,value爲示例,AOP以自定義的註解爲切點,根據註解的value讀取是哪個數據庫的數據源實例key,來獲取指定的數據源。
二、配置多個數據源
首先,定義一個枚舉來說明一下當前數據源實例key有哪些。
public enum DataSourceKey {
MASTER("master"),
SLAVE("slave");
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private DataSourceKey(String name) {
this.name = name;
}
}
其次,使用ThreadLocal存儲當前使用數據源實例的key。ThreadLocal實例化的時候給一個master的默認值,也就是默認數據源是master數據源。
public class DynamicDataSourceContextHolder {
private static ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceKey.MASTER.getName());
public static List<Object> dataSourceKeys = new ArrayList<Object>();
public static void setDataSourceKey(String key){
CONTEXT_HOLDER.set(key);
}
public static Object getDataSourceKey(){
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceKey(){
CONTEXT_HOLDER.remove();
}
public static Boolean containDataSourceKey(String key){
return dataSourceKeys.contains(key);
}
}
然後,需要實例化多個DataSource的Bean放在ApplicationContext中,這裏我們使用druid的實現,定義好每個Bean的名字,名字要和枚舉的name一致,
@primary的意思就是默認使用這個Bean
dynamicDataSource需要一個存儲數據庫實例的Map集合
sqlSessionFactoryBean可以配置mybatis的相關配置
transactionManager是事務配置
/**
* 數據源配置類
* 在該類中生成多個數據源實例並將其注入到 ApplicationContext 中
* 2019年6月4日 上午10:06:54
*/
@Configuration
public class DataSourceConfigurer {
/**
* 配置數據源
* @return
*/
@Bean(name = "master")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource master() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置數據源
* @return
*/
@Bean(name = "slave")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource slave() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(){
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(2);
dataSourceMap.put(DataSourceKey.MASTER.getName(),master());
dataSourceMap.put(DataSourceKey.SLAVE.getName(),slave());
dynamicRoutingDataSource.setDefaultTargetDataSource(master());
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
return dynamicRoutingDataSource;
}
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean(){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true); //下劃線轉駱駝
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean;
}
/**
* 事務
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dynamicDataSource());
}
}
三、自定義註解
爲了使用AOP代理特定的方法,我們就可以自定義註解。這個註解很簡單,也沒啥太大含義。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
DataSourceKey value() default DataSourceKey.MASTER;
}
四、AOP動態切換數據源
使用AOP,以自定義註解註解在的方法爲切點,動態切換數據源
@Aspect
@Component
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Before("@annotation(targetDataSource))")
public void switchDataSource(JoinPoint joinPoint,TargetDataSource targetDataSource){
if ( !DynamicDataSourceContextHolder.containDataSourceKey( targetDataSource.value().getName() ) ) {
logger.error("DataSource [{}] doesn't exist, use default DataSource [{}]", targetDataSource.value());
} else {
DynamicDataSourceContextHolder.setDataSourceKey( targetDataSource.value().getName() );
logger.info("Switch DataSource to [{}] in Method [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
}
}
@After("@annotation(targetDataSource))")
public void restoreDataSource(JoinPoint joinPoint,TargetDataSource targetDataSource){
DynamicDataSourceContextHolder.clearDataSourceKey();
logger.info("Restore DataSource to [{}] in Method [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
}
}
五、使用
在controller層使用,在不同的方法上添加自定義註解來使用不同的數據源。MASTER是默認數據源,可以不使用註解。
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user")
@TargetDataSource(DataSourceKey.MASTER)
public ResponseEntity<Object> listAll(){
List<JSONObject> result = userService.listAll();
return ResponseEntity.ok(result);
}
@PostMapping("/user")
@TargetDataSource(DataSourceKey.SLAVE)
public ResponseEntity<Object> listAlla(){
List<JSONObject> result = userService.listAll();
return ResponseEntity.ok(result);
}
}
寫在最後的話
數據庫主從同步可以參考網上的實現,我暫時還沒實現,這個後面再說。代碼的多數據源我自己測試過了。我也是看了別人寫的,然後自己敲了一遍,記錄一下。如果我有幸被你搜到了這篇博客,可以留言討論。