由於某幾個業務表數據量太大,數據由業務寫,數據部門讀。
寫壓力不大,讀卻很容易導致長時間等待問題(讀由單獨系統進行讀),導致連接被占用,從而容易並發稍稍增長導致全庫卡死!
於是,就拆庫唄。
業務系統拆分就不要做了(微服務化),沒那工夫。
直接原系統拆兩個數據源出來,對某幾個高壓力表的寫就單獨用這個數據源,從而減輕壓力。
所以,分庫工作就變為了兩個步驟:
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. 使用別人的服務,其壞處主要是錢的問題,這個自不必說。還有個不是錢的問題的壞處,那就是你不能隨意訂制你想要的功能了,你的能力被別人限制住了,這個可能促使你轉場到自己提供服務;另外,各家提供的服務都不一樣,不像自己搭建的服務,網上會有各種資料可查,所以有一定的學習成本,具體取決於官方設計與官方文檔的完整性(當然一般都會很簡單);其實還有一個,就不說了,懂的都懂;
好了,借着數據庫遷移的小事,扯了這些淡。只當是拋磚引玉了!歡迎指教!