自定義註解完成資料庫切庫

weixin_34214500發表於2018-03-09

第一步定義我們自定義切庫註解類(使用spring boot 實現)

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

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(); 
    } 
  } 

  • 通過編寫切面,對所有我們自定義切庫註解的方法進行攔截,動態的選擇資料來源

1、這裡是為下一步提供鋪墊,動態調整DataSourceContextHolder裡儲存的值,使用threadLocal來管理是為了避免多執行緒之間互相影響。

2、自定義註解,核心的處理就是寫處理這個註解的邏輯,然後通過指定的攔截方案根據當前的資料做一些動態的處理。比如Spring提供的@Controller、@Service等註解,都是需要我們在配置檔案裡配置好需要掃描的路徑,然後專案啟動時,spring根據配置去指定路徑讀取這些配置,然後這些類才可以被spring進行管理。

3、這裡不要忽略了預設資料來源要選擇主庫,如果切庫出現什麼問題,比如配置錯誤等,可以保證訪問主庫來得到正確的結果;另外,請求完了不要忘記呼叫提供的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();
    }
}

如何使用呢,我們簡單演示一下:

@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了。如果你的系統已經有主庫、從庫之分了,那麼趕緊在你的系統裡利用起來吧。

擴充套件

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

如果有多個主庫,也可以有更多的選擇~

相關文章