springboot結合druid使用多數據源,動態切換

前言

有時候爲了減少數據庫的壓力,就要實現數據庫的讀寫分離,這種情況往往是讀多寫少的情況,例如電商平臺。既然數據庫讀寫分離了,那麼代碼層也就需要讀寫不同的數據庫了。實現方法應該有不少,我知道有插件實現,判斷寫請求還是讀請求來請求不同的數據庫,還有代碼實現,不同的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);
		
	}
	
	
	
}

寫在最後的話

數據庫主從同步可以參考網上的實現,我暫時還沒實現,這個後面再說。代碼的多數據源我自己測試過了。我也是看了別人寫的,然後自己敲了一遍,記錄一下。如果我有幸被你搜到了這篇博客,可以留言討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章