天天看點

阿裡雲rds資料庫遷移實戰(多資料源)

  由于某幾個業務表資料量太大,資料由業務寫,資料部門讀。

  寫壓力不大,讀卻很容易導緻長時間等待問題(讀由單獨系統進行讀),導緻連接配接被占用,進而容易并發稍稍增長導緻全庫卡死!

  于是,就拆庫呗。

  業務系統拆分就不要做了(微服務化),沒那工夫。

  直接原系統拆兩個資料源出來,對某幾個高壓力表的寫就單獨用這個資料源,進而減輕壓力。

是以,分庫工作就變為了兩個步驟:

  1. 兩個資料源讀寫業務;

  2. 将新資料庫寫動作同步回讀庫;

  再由于友善性,資料庫也是使用阿裡的rds資料庫,一個變為兩個!

  代碼上做兩個資料源很簡單,尤其是在原有代碼就寫得比較清晰的情況下;

如下是使用springboot和mybatis做的多資料源配置:

  1. 配置多個資料源類;

  2. 啟用mybatis多資料源,加載不同配置bean;

  3. 根據掃描路徑差別使用的資料源;

  4. 根據掃描路徑将需要拆分的表與原表差別;

  5. 測試時可使用同同機器上多庫形式運作,上線後為多執行個體同庫運作;

  6. 驗證功能可用性;如有問題,及時修改;

  具體配置如下:

// 原資料源配置
@Configuration
@MapperScan(basePackages = MainDataSourceConfig.SCAN_BASE_PACKAGE, sqlSessionFactoryRef = "sqlSessionFactory")
public class MainDataSourceConfig {

    public static final String SCAN_BASE_PACKAGE = "com.xxx.dao.mapper.main";

    /**
     * xml 配置檔案掃描路徑
     */
    public static final String SCAN_XML_MAPPER_LOCATION = "classpath:mybatis/mappers/mysql/main/**/*Mapper.xml";

    //jdbcConfig
    @Value("${jdbc.main.url}")
    private String jdbcUrl;
    @Value("${jdbc.main.driver}")
    private String driverName;
    @Value("${pool.main.maxPoolSize}")
    private int maxPoolSize;
    @Value("${jdbc.main.username}")
    private String jdbcUserName;
    @Value("${jdbc.main.password}")
    private String jdbcPwd;
    @Value("${pool.main.maxWait}")
    private int jdbcMaxWait;
    @Value("${pool.main.validationQuery}")
    private String validationQuery;

    @Bean(name = "druidDataSource")
    @Primary
    public DruidDataSource druidDataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(jdbcUrl);
        ds.setDriverClassName(driverName);
        ds.setMaxActive(maxPoolSize);
        ds.setUsername(jdbcUserName);
        ds.setPassword(jdbcPwd);
        ds.setRemoveAbandoned(true);
        ds.setMaxWait(jdbcMaxWait);
        ds.setValidationQuery(validationQuery);
        return ds;
    }
    
    @Bean(name = "dataSourceTransactionManager")
    @Primary
    public DataSourceTransactionManager dataSourceTransactionManager(){
        DataSourceTransactionManager dm = new DataSourceTransactionManager();
        dm.setDataSource(druidDataSource());
        return dm;
    }

    @Bean(name="sqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] mapperXmlResource = resolver.getResources(SCAN_XML_MAPPER_LOCATION);
        sqlSessionFactory.setDataSource(druidDataSource());
        sqlSessionFactory.setMapperLocations(mapperXmlResource);
        return sqlSessionFactory.getObject();
    }
    
}

// 新資料源配置,僅僅改了下配置名,但是還不得不另一個配置類
@Configuration
@MapperScan(basePackages = ExtraDataSourceConfig.SCAN_BASE_PACKAGE, sqlSessionFactoryRef = "sqlSessionFactoryExt")
public class ExtraDataSourceConfig {

    public static final String SCAN_BASE_PACKAGE = "com.xxx.dao.mapper.ext";

    /**
     * xml 配置檔案掃描路徑
     */
    public static final String SCAN_XML_MAPPER_LOCATION = "classpath:mybatis/mappers/mysql/ext/**/*Mapper.xml";

    //jdbcConfig
    @Value("${jdbc.ext.url}")
    private String jdbcUrl;
    @Value("${jdbc.ext.driver}")
    private String driverName;
    @Value("${pool.ext.maxPoolSize}")
    private int maxPoolSize;
    @Value("${jdbc.ext.username}")
    private String jdbcUserName;
    @Value("${jdbc.ext.password}")
    private String jdbcPwd;
    @Value("${pool.ext.maxWait}")
    private int jdbcMaxWait;
    @Value("${pool.ext.validationQuery}")
    private String validationQuery;

    @Bean(name = "druidDataSourceExt")
    public DruidDataSource druidDataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(jdbcUrl);
        ds.setDriverClassName(driverName);
        ds.setMaxActive(maxPoolSize);
        ds.setUsername(jdbcUserName);
        ds.setPassword(jdbcPwd);
        ds.setRemoveAbandoned(true);
        ds.setMaxWait(jdbcMaxWait);
        ds.setValidationQuery(validationQuery);
        return ds;
    }
    
    @Bean(name = "dataSourceTransactionManagerExt")
    public DataSourceTransactionManager dataSourceTransactionManager(){
        DataSourceTransactionManager dm = new DataSourceTransactionManager();
        dm.setDataSource(druidDataSource());
        return dm;
    }

    @Bean(name="sqlSessionFactoryExt")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] mapperXmlResource = resolver.getResources(SCAN_XML_MAPPER_LOCATION);
        sqlSessionFactory.setDataSource(druidDataSource());
        sqlSessionFactory.setMapperLocations(mapperXmlResource);
        return sqlSessionFactory.getObject();
    }
    
}      

  然後,将需要分離的表操作轉移到相應的包路徑下,即可實作多資料源操作了!

而多資料源配置對于基于xml配置spring來說,可能更加直覺更加簡單,甚至xml檔案都不用分離!

<!-- 原資料源配置 -->
    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.main.url}" />
        <property name="username" value="${jdbc.main.username}" />
        <property name="password" value="${jdbc.main.password}" />
        <property name="maxActive" value="${jdbc.main.maxActive}" />
        <property name="maxWait" value="${jdbc.main.maxWait}" />
    </bean>
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:mybatis-config.xml" />
        <property name="mapperLocations">
            <list>
                <value>classpath:mybatis/mappers/mysql/main/**/*Mapper.xml</value>
            </list>
        </property>
    </bean>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager" /> 
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="basePackage" value="com.xxx.dao.automapper.main" />
      <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
    
    <!-- 第二個資料源的配置 -->
    <bean name="dataSourceExt" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.ext.url}" />
        <property name="username" value="${jdbc.ext.username}" />
        <property name="password" value="${jdbc.ext.password}" />
        <property name="maxActive" value="${jdbc.ext.maxActive}" />
        <property name="maxWait" value="${jdbc.ext.maxWait}" />
    </bean>
    <bean id="sqlSessionFactoryExt" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSourceExt" />
      <property name="configLocation" value="classpath:config/mybatis-config.xml" />
      <property name="mapperLocations">
          <list>
            <value>classpath:mybatis/mappers/mysql/ext/**/*Mapper.xml</value>
          </list>
      </property>
    </bean>
    <bean id="transactionManagerExt" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSourceExt" />
    </bean>
    <tx:annotation-driven transaction-manager="transactionManagerExt" /> 
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryExt"/>
        <property name="basePackage" value="com.xxx.dao.automapper.ext"/>
    </bean>      

  是以,還是那句話:不是所有聽起來好的東西就一定是好,在這裡轉換為不是所有聽起來友善的東西用起來就一定友善!

代碼ok後,還剩下一個問題:獨立後的寫動作同步問題!

  如果是自行搭建的mysql服務,我們很自然地考慮使用binlog同步(主從)來做!具體配置方法也不複雜,自行查找資料即可!

  如果使用阿裡雲服務,則不是binlog那樣的配置了(但其實質仍然是對binlog的訂閱寫)。不過倒也都是頁面操作!(網上不一定好找資料,但是官網上一定是最全的)

要進行資料庫遷移,大體步驟分為:

  1. 先要将現有資料遷移到新執行個體上;

  2. 将部分新資料寫入新執行個體(部分資料仍直接寫現有執行個體,做到業務剝離的同時還可以減少資料同步的開銷);

  3. 将新執行個體資料同步回原執行個體庫;

DTS服務(資料傳輸服務),專門用于做資料遷移和資料同步!

  其打開方式為:

  1. 自然是花錢買服務了,買好後才能進入操作頁面;這裡服務要分兩個:一是新執行個體rds資料庫,二是新執行個體同步回原執行個體的同步服務;

  2. 設定ip白名單,以使mysql可通路;

  3. 建立高權限使用者,如root,以使後續可高權限操作mysql,同步時可使用該賬号或者另用一個普通讀寫賬号;

  4. 将全量資料刷入新執行個體,這裡可以選擇阿裡雲免費的資料庫遷移服務,也可以自己将資料dump下來,然後自行導緻新庫;(不過服務既然是免費的,那為啥不用呢!)

  5. 設定單向同步,從新執行個體到原執行個體,此時是不會有資料同步的,因為沒有新寫入;

  6. 資料刷入,同步設定完成後,就可以釋出新代碼了,此時最好将前端入口停止,否則可能出現資料錯亂問題;

  7. 釋出代碼後,需要自行驗證。此時,先選擇一台機器進行驗證,可以選擇兩種方式驗證:一是自行調用關鍵接口進行驗證;二是将該機器綁定eip外網,使用該外網進行頁面通路驗證(更完整的驗證);驗證的方向主要有兩個:1. 接口正常響應,沒有錯誤發生(此處應該要有監控設施,否則隻能憑感覺);2. 資料有沒有正常同步(一般同步都是秒級的);

  8. 将代碼釋出到叢集中,觀察各機器運作情況!此處主要檢視資料庫連接配接情況,是否存在連接配接失敗情況,應用監控是主要手段,也可以通過mysql的show full processlist; 進行檢視應用連接配接db情況;

  9. 觀察正常後,此時可以将前端應用入口打開,此時如有條件,應限制ip通路,使變更進行充分測試無誤;

  10. 一切無誤後,完全開放通路服務;監控使用者資料,遷移完成;

至此,整個遷移就完成了,其實思路是很簡單的,關鍵是要小心操作。一個不小心的操作,就可能帶來很大的隐患,畢竟,資料無小事!請保持對資料和代碼的敬畏!

臨了臨了,附幾個操作的貼心小技巧,避免入坑!

  1. 買rds資料庫時,盡量買與原執行個體相同的區域(大區和可用區都相同),否則後期在做同步的時候會花更多的錢,因為跨區的網絡通信會讓你支付更多;

  2. 新執行個體資料庫容量可以稍微降配以節省錢,因為畢竟你是将原來的部分功能拆分出來的,沒必要一開始就為全部将來買單;

  3. 買同步服務時,注意幾點:1. 按量計算(按小時)比預付費(包月)更貴,但是也更容易訂制化,如果僅僅操作兩個rds間同步,且短時間内不會下線服務,則建議選擇預付費包月形式;2. 将區域選擇正确,比如同區域同步将更便宜;3. 能單向同步就不要雙向同步了,便宜的同時,也減小了誤操作帶來的影響;4. 同步性能一般小流量選擇small即可,高配的同步用不上關鍵是貴;

  4. 同步服務盡早開啟,但是後期對于賬号密碼的變更,一定要及時更改同步配置,否則将帶來資料一緻性問題;(人工發現往往較晚,盡量設定監控報警)

  5. 資料庫白名單中,需要加入阿裡雲資料傳輸服務的白名單,否則無法檢查資料庫響應性及同步作業;

  6. 選擇同步對象時,盡量以庫作為機關!如果選擇以表為同步機關,将存在後續新增表時,不會同步回原執行個體情況。如果實在不能以庫作為機關,在後續疊代時,一定記得添加此處同步表;(關注點太多,麻煩)

  7. 後期做資料變更時,注意操作對象所屬執行個體,别一頓操作猛如虎,然後沒什麼卵用,因為我們隻是選擇了單向同步;

  8. 自己可以不定期地做checksum檢查,以确認同步功能正常工作;(checksum table test)

  9. 代碼上分庫一定要做準确了,因為這裡可能是一定時間内的唯一可信參考資料;(簡單但是關鍵)

最後,我還想說下使用别人服務和自己動手的一些個人感覺:

  1. 使用自己搭建的服務,最大的好處在于可以做任意的改變不受限,而且不需要付出額外的可見費用;

  2. 使用自己的服務的可能壞處是:如果你不是這方面的專家,往往會被自己埋下的各種坑難住;遇到問題沒能力處理;考慮方面不周全,容易引發安全問題;對未來的因素沒辦法考慮,使後期運作困難;如果你是專家,那多半這些都不是事兒;

  3. 使用别人的服務,最大的好處就是簡單易用,且有人維護;這些服務往往都是一路填坑過來的,時間越久往往越可靠(百年老字号最佳,哈哈);安全性、擴充性、性能調優、高可用等等;

  4. 使用别人的服務,其壞處主要是錢的問題,這個自不必說。還有個不是錢的問題的壞處,那就是你不能随意訂制你想要的功能了,你的能力被别人限制住了,這個可能促使你轉場到自己提供服務;另外,各家提供的服務都不一樣,不像自己搭建的服務,網上會有各種資料可查,是以有一定的學習成本,具體取決于官方設計與官方文檔的完整性(當然一般都會很簡單);其實還有一個,就不說了,懂的都懂;

  好了,借着資料庫遷移的小事,扯了這些淡。隻當是抛磚引玉了!歡迎指教!

不要害怕今日的苦,你要相信明天,更苦!

繼續閱讀