前提mysql
這幾天,學員們反饋但願學習一下自定義註解,正好準備高併發課程內容裏有一塊涉及到使用自定義註解完成數據庫切庫的內容。這裏單獨寫一篇文章記錄說明一下。spring
爲何會有數據庫切庫一說sql
首先,許多項目都有主庫與從庫,有的主庫後面甚至會有不少個從庫,主從庫之間的一般同步也很快,這爲數據庫切庫提供了一個基礎,由於能夠去不一樣的數據庫查詢,獲得相同的結果(若是不一樣的數據庫是徹底不一樣的,這個不在咱們這篇文章討論的範圍以內,那個屬於讓項目支持多個數據源)數據庫
其次,隨着項目愈來愈大、操做的用戶愈來愈多,對數據庫的請求操做愈來愈多,很容易想到的是將讀寫請求分開,將寫請求交給主庫處理,讀請求直接從某個從庫讀取。這樣能夠極大的減小大量對主庫的請求,提高主庫的性能。安全
接下來具體說一下如何經過自定義註解完成切庫(代碼使用springboot實現):springboot
第一步、定義咱們本身的切庫註解類mybatis
自定義註解有幾點須要注意:多線程
1)@Target 是做用的目標,接口、方法、類、字段、包等等,具體看:ElementType併發
2)@Retention 是註解存在的範圍,RUNTIME表明的是註解會在class字節碼文件中存在,在運行時能夠經過反射獲取到,具體看:RetentionPolicyapp
3)容許的變量,一般都要給定默認值,好比咱們使用一個service時,能夠@Service,也能夠@Service(xxxx)
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD
})
public @interface RoutingDataSource {
String value() default DataSources.MASTER_DB;
}
第二步、定義須要使用的數據庫及配置
一、數據庫配置:application.properties,這裏要注意不一樣db的前綴區別
## datasource master #
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/master?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=466420182
## datasource slave #
spring.datasourceSlave.type=com.alibaba.druid.pool.DruidDataSource
spring.datasourceSlave.driver-class-name=com.mysql.jdbc.Driver
spring.datasourceSlave.url=jdbc:mysql://localhost:3306/slave?characterEncoding=UTF-8
spring.datasourceSlave.username=root
spring.datasourceSlave.password=466420182
二、定義支持的數據源id:
public interface DataSources {
String MASTER_DB = masterDB;
String SLAVE_DB = slaveDB;
}
三、定義數據庫實體類並配置爲多數據源的形式
這裏不要忽略了經過 MapperScan 指定須要掃描的mybatis的接口類
@Configuration
public class DatasourceConfig {
//destroy-method=close的做用是當數據庫鏈接不使用的時候,就把該鏈接從新放到數據池中,方便下次使用調用.
@Bean(destroyMethod = close, name = DataSources.MASTER_DB)
@ConfigurationProperties(prefix = spring.datasource)
public DataSource dataSource() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean(destroyMethod = close, name = DataSources.SLAVE_DB)
@ConfigurationProperties(prefix = spring.datasourceSlave)
public DataSource dataSourceSlave() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
}
四、配置成動態數據源:
@Configuration
@MapperScan(basePackages = {com.xxx.dao}) // 這裏須要替換爲實際的路徑
public class MybatisConfig {
@Autowired
@Qualifier(Datasources.MASTER_DB)
private DataSource masterDB;
@Autowired
@Qualifier(DataSources.SLAVE_DB)
private DataSource slaveDB;
/**
* 動態數據源
*/
@Bean(name = dynamicDataSource)
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默認數據源
dynamicDataSource.setDefaultTargetDataSource(masterDB);
// 配置多數據源
MapdsMap = Maps.newHashMap();
dsMap.put(DataSources.MASTER_DB, masterDB);
dsMap.put(DataSources.SLAVE_DB, slaveDB);
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
@Bean
@ConfigurationProperties(prefix = mybatis)
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置數據源,此處配置爲關鍵配置,若是沒有將 dynamicDataSource 做爲數據源則不能實現切換
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
}
第三步、使用ThreadLocal安全的管理當前進程使用的數據源鏈接
@Slf4j
public class DataSourceContextHolder {
/**
* 默認數據源
*/
public static final String DEFAULT_DATASOURCE = DataSources.MASTER_DB;
private static final ThreadLocalcontextHolder = new ThreadLocal();
// 設置數據源名
public static void setDB(String dbType) {
log.debug(切換到{}數據源, dbType);
contextHolder.set(dbType);
}
// 獲取數據源名
public static String getDB() {
return (contextHolder.get());
}
// 清除數據源名
public static void clearDB() {
contextHolder.remove();
}
}
第四步、經過編寫切面,對全部咱們自定義切庫註解的方法進行攔截,動態的選擇數據源
這裏是爲下一步提供鋪墊,動態調整DataSourceContextHolder裏存儲的值,使用threadLocal來管理是爲了不多線程之間互相影響。
自定義註解,核心的處理就是寫處理這個註解的邏輯,而後經過指定的攔截方案根據當前的數據作一些動態的處理。好比Spring提供的@Controller、@Service等註解,都是須要咱們在配置文件裏配置好須要掃描的路徑,而後項目啓動時,spring根據配置去指定路徑讀取這些配置,而後這些類才能夠被spring進行管理。
這裏不要忽略了默認數據源要選擇主庫,若是切庫出現什麼問題,好比配置錯誤等,能夠保證訪問主庫來獲得正確的結果;另外,請求完了不要忘記調用提供的clearDB的操做,防止threadLocal誤用帶來的內存泄露。
@Aspect
@Component
@Slf4j
public class DynamicDataSourceAspect {
@Before(@annotation(RoutingDataSource))
public void beforeSwitchDS(JoinPoint point){
//得到當前訪問的class
Class className = point.getTarget().getClass();
//得到訪問的方法名
String methodName = point.getSignature().getName();
//獲得方法的參數的類型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
String dataSource = DataSourceContextHolder.DEFAULT_DATASOURCE;
try {
// 獲得訪問的方法對象
Method method = className.getMethod(methodName, argClass);
// 判斷是否存在@DS註解
if (method.isAnnotationPresent(RoutingDataSource.class)) {
RoutingDataSource annotation = method.getAnnotation(RoutingDataSource.class);
// 取出註解中的數據源名
dataSource = annotation.value();
}
} catch (Exception e) {
log.error(routing datasource exception, + methodName, e);
}
// 切換數據源
DataSourceContextHolder.setDB(dataSource);
}
@After(@annotation(RoutingDataSource))
public void afterSwitchDS(JoinPoint point){
DataSourceContextHolder.clearDB();
}
}
第五步、動態的取出咱們在切面裏設置的數據源的字符串便可
這裏須要把原理介紹一下,在鏈接數據庫時實際上是先選擇一個配置好的spring管理的datasource的id,就是咱們以前在 DatasourceConfig 類裏定義的Datasource實體類的id:masterDB 和 slaveDB。而後根據id去spring的上下文選擇配置,進行數據庫鏈接。有興趣的能夠看一下源碼。
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
log.debug(數據源爲{}, DataSourceContextHolder.getDB());
return DataSourceContextHolder.getDB();
}
}
第六步、取消自動配置數據源,使用咱們這裏定義的數據源配置
在SpringBoot啓動類上一般直接使用@SpringBootApplication就能夠了,這裏須要調整爲:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})
使用
如何使用呢,咱們簡單演示一下:
@Service
public class DataSourceRoutingService {
@Resource
private SysUserMapper sysUserMapper;
@RoutingDataSource(DataSources.MASTER_DB) // 這個註解這時是能夠省略,由於默認就是訪問主庫
public SysUser test1(int id) {
return sysUserMapper.selectByPrimaryKey(id);
}
@RoutingDataSource(DataSources.SLAVE_DB)
public SysUser test2(int id) {
return sysUserMapper.selectByPrimaryKey(id);
}
}
如此,數據庫切庫就OK了。若是你的系統已經有主庫、從庫之分了,那麼趕忙在你的系統裏利用起來吧。
擴展
這裏呢,還能夠支持多個擴展。好比如今一個主庫後面有多個從庫,在切面拿到須要切換從庫時,還能夠選擇隨機選擇一個,或者根據類名、方法名或業務配置等選擇某一個從庫,這樣不但能夠分擔每一個從庫的壓力,也能夠有針對性的讓指定的讀請求打到指定的從庫上。若是有多個主庫,也能夠有更多的選擇~