SpringBoot - 自定義註解完成數據庫切庫

爲什麼會有數據庫切庫一說

首先,許多項目都有主庫與從庫,有的主庫後面甚至會有很多個從庫,主從庫之間的通常同步也很快,這爲數據庫切庫提供了一個基礎,因爲可以去不同的數據庫查詢,得到相同的結果(如果不同的數據庫是完全不同的,這個不在我們這篇文章討論的範圍之內,那個屬於讓項目支持多個數據源)

其次,隨着項目越來越大、操作的用戶越來越多,對數據庫的請求操作越來越多,很容易想到的是將讀寫請求分開,將寫請求交給主庫處理,讀請求直接從某個從庫讀取。這樣可以極大的減少大量對主庫的請求,提升主庫的性能。

接下來具體說一下如何通過自定義註解完成切庫(代碼使用springboot實現):

 

第一步、定義我們自己的切庫註解類

自定義註解有幾點需要注意:

1)@Target 是作用的目標,接口、方法、類、字段、包等等,具體看:ElementType

2)@Retention 是註解存在的範圍,RUNTIME代表的是註解會在class字節碼文件中存在,在運行時可以通過反射獲取到,具體看:RetentionPolicy

3)允許的變量,通常都要給定默認值,比如我們使用一個service時,可以@Service,也可以@Service("xxxx")

@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.METHOD
})
public @interface RoutingDataSource {

    String value() default DataSources.MASTER_DB;
}

 

第二步、定義需要使用的數據庫及配置

1、數據庫配置: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

2、定義支持的數據源id:

public interface DataSources {

    String MASTER_DB = "masterDB";

    String SLAVE_DB = "slaveDB";
}

3、定義數據庫實體類並配置爲多數據源的形式

這裏不要忽略了通過 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();
    }
}

4、配置成動態數據源:

@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);

        // 配置多數據源
        Map<Object, Object> dsMap = 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 ThreadLocal<String> contextHolder = 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了。如果你的系統已經有主庫、從庫之分了,那麼趕緊在你的系統裏利用起來吧。

 

擴展

這裏呢,還可以支持多個擴展。比如現在一個主庫後面有多個從庫,在切面拿到需要切換從庫時,還可以選擇隨機選擇一個,或者根據類名、方法名或業務配置等選擇某一個從庫,這樣不但可以分擔每個從庫的壓力,也可以有針對性的讓指定的讀請求打到指定的從庫上。如果有多個主庫,也可以有更多的選擇~

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