MySQL數據庫如何定製分庫分表中間件詳解

數據庫 MySQL ElasticSearch 算法 設計 Hadoop 來一杯82年的Java 2019-04-30

一般來說,影響數據庫最大的性能問題有兩個,一個是對數據庫的操作,一個是數據庫中的數據太大。對於前者我們可以藉助緩存來減少一部分讀操作,針對一些複雜的報表分析和搜索可以交給 HadoopElasticSearch 。對於後者,我們就只能分庫分表,讀寫分離。

互聯網行業隨著業務的複雜化,大多數應用都會經歷數據的垂直分區。一個複雜的流程會按照領域拆分成不同的服務,每個服務中心都擁有自己獨立的數據庫。拆分後服務共享,業務更清晰,系統也更容易擴展,同時減少了單庫數據庫連接數的壓力,也在一定程度上提高了單表大數據量下索引查詢的效率。當然業務隔離,也可以避免一個業務把數據庫拖死導致所有業務都死掉,我們將這種按照業務維度,把一個庫拆分為多個不同的庫的方式叫做垂直拆分

垂直拆分也包含針對長表(屬性很多)做冷熱分離的拆分。例如,在商品系統設計中,一個商品的生產商、供銷商以及特有屬性,這些字段變化頻率低,查詢次數多,叫做冷數據;而商品的份額,關注量等類似的統計信息變化頻率較高,叫做活躍數據或者熱數據。在 MySQL 中,冷數據查詢多更新少,適合用 MyISAM 存儲引擎,而熱數據更新比較頻繁適合用 InnoDB,這也是垂直拆分的一種。

當單表數據量隨著業務發展繼續膨脹,在 MySQL 中當數據量達到千萬級時,就需要考慮進行水平拆分了,這樣數據就分散到不同的表上,單表的索引大小得到控制,可以提升查詢性能。當數據庫的實例吞吐量達到性能瓶頸後,我們需要水平擴展數據庫的實例,讓多個數據庫實例分擔請求,這種根據分片算法,將一個庫拆分成多個一樣結構的庫,將多個表拆分成多個結構相同的表就叫做水平拆分

MySQL數據庫如何定製分庫分表中間件詳解

數據拆分也有很多缺點,數據分散,數據庫的 Join 操作變得更加複雜,分片後數據的事務一致性很難保證,同時數據的擴容和維護難度增加,拆分規則也可能導致某個業務需要同時查詢所有的表然後進行聚合。如果需要排序和函數計算則更加複雜,所以不到萬不得已可以先不必拆分。

根據分庫分表方案中實施切片邏輯的層次不同,我們將分庫分表的實現方案分成以下4種:

1. 在應用層直接分片

這種方式將分片規則直接放在應用層,雖然侵入了業務,開發人員不僅既需要實現業務邏輯也需要實現分庫分表的配置的開發,但是實現起來簡單,適合快速上線,通過編碼方式也更容易實現跨表遍歷的情況。後期故障也更容易定位,大多數公司都會在業務早期採用此種方式過渡,後期分表需求增多,則會尋求中間件來解決,以下代碼為銅板街早期訂單表在 DAO 層將分片信息以參數形式傳到 mybatis 的 mapper 文件中的實現方案。

 1 @Override
2 public OrderDO findByPrimaryKey(String orderNo) {
3
4 Assert.hasLength(orderNo, "訂單號不能為空");
5
6 Map<String, Object> map = new HashMap<String, Object>(3);
7 map.put("tableSuffix", orderRouter.routeTableByOrderNo(orderNo));
8 map.put("dbSuffix", orderRouter.routeDbByOrderNo(orderNo));
9 map.put("orderNo", orderNo);
10
11 Object obj = getSqlSession().selectOne("NEW_ORDER.FIND_BY_PRIMARYKEY", map);
12 if (obj != null && obj instanceof OrderDO) {
13 return (OrderDO) obj;
14 }
15 return null;
16 }

2. 在ORM層直接分片

這種方式通過擴展第三方 ORM 框架,將分片規則和路由機制嵌入到 ORM 框架中,如hibernate 和 mybatis,也可以基於 spring jdbctemplate 來實現,目前實現方案較少。

3. 客戶端定製 JDBC 協議

這種方式比較常見對業務侵入低。通過定製 JDBC 協議,針對業務邏輯層提供與 JDBC一致的接口,讓開發人員不必要關心分庫分表的具體實現,分庫分表在 JDBC 內部搞定,對業務層透明。目前流行的 ShardingJDBC、TDDL 便採用了這種方案。這種方案需要開發人員熟悉 JDBC 協議,研發成本較低,適合大多數中型企業。

4. 代理分片

此種分片方式,是在應用層和數據庫層增加一個代理,把分片的路由規則配置在代理層,代理層提供與 JDBC 兼容的接口給應用層,開發人員不用關心分片邏輯實現,只需要在代理層配置即可。增加代理服務器,需要解決代理的單點問題增加硬件成本,同時所有的數據庫請求增加了一層網絡傳輸影響性能,當然維護也需要更資深的專家,目前採用這種方式的框架有 cobar 和 mycat。

MySQL數據庫如何定製分庫分表中間件詳解

切片算法

選取分片字段

分片後,如果查詢的標準是根據分片的字段,則根據切片算法,可以路由到對應的表進行查詢。如果查詢條件中不包含分片的字段,則需要將所有的表都掃描一遍然後在進行合併。所以在設計分片的時候我們一般會選擇一個查詢頻率較高的字段作為分片的依據,後續的分片算法會基於該字段的值進行。例如根據創建時間字段取對應的年份,每年一張表,取電話號碼裡面的最後一位進行分表等,這個分片的字段我們一般會根據查詢頻率來選擇。例如在互金行業,用戶的持倉數據,我們一般選擇用戶 id 進行分表,而用戶的交易訂單也會選擇用戶 id 進行分表,但是如果我們要查詢某個供應商下在某段時間內的所有訂單就需要遍歷所有的表,所以有時候我們可能會需要根據多個字段同時進行分片,數據進行冗餘存儲。

分片算法

分片規則必須保證路由到每張物理表的數據量大致相同,不然上線後某一張表的數據膨脹的特別快,而其他表數據相對很少,這樣就失去了分表的意義,後期數據遷移也有很高的複雜度。通過分片字段定位到對應的數據庫和物理表有哪些算法呢?(我們將分表後在數據庫上物理存儲的表名叫物理表,如 trade_order_01,trade_order_02,將未進行切分前的表名稱作邏輯表如 trade_order)大致可以有以下分類:

  • 按日期 如年份,季度,月進行分表,這種維度分表需要注意在邊緣點的垮表查詢。例如如果是根據創建時間按月進行分片,則查詢最近3天的數據可能需要遍歷兩張表,這種業務比較常見,但是放中間件層處理起來就比較複雜,可能在應用層特殊處理會簡單點。
  • 哈希 ,這種是目前比較常用的算法,但是這裡謹慎推薦,因為他的後期擴容是件很頭痛的事情,例如根據用戶 ID 對64取模,得到一個0到63的數字,這裡最多可以切分64張表{0,1,2,3,4… 63},前期可能用不到這麼多,我們可以藉助一致性哈希的算法,每4個連續的數字分成放到一張表裡。例如 0,1,2,3 分到00這張表,4,5,6,7分到04這張表,用算法表示 floor(userID % 64 / 4) * 4 假設 floor為取整的效果。

按照一致性哈希算法,當需要進行擴容一倍時需要遷移一半的數據量,雖然不至於遷移所有的數據,如果沒有工具也是需要很大的開發量。下圖中根據分表字段對16取餘後分到4張表中,後面如果要擴容一倍則需要遷移一半的數據。

截取 這種算法將字段中某一段位置的數據截取出來,例如取電話號碼裡面的尾數,這種方式實現起來簡單,但在上線前一定要預測最終的數據分佈是否會平均。比如地域,姓氏可能並不平均等,以4結尾的電話號碼也相對偏少。

特別注意點

分庫和分表算法需要保證不相關,上線前一定要用線上數據做預測。例如分庫算法用“用戶id%64 分64個庫” 分表算法也用 “用戶id%64 分64張表”,總計 64 * 64 張表,最終數據都將落在 以下 64張表中 00庫00表,01庫01表… 63庫63表, 其他 64 * 63張表則沒有數據。這裡可以推薦一個算法,分庫用 用戶ID/64 % 64 , 分表用 用戶ID%64 測試1億筆用戶id發現分佈均勻。

在分庫分表前需要規劃好業務增長量,以預備多大的空間,計算分表後可以支持按某種數據增長速度可以維持多久。

如何實現客戶端分片

客戶端需要定製 JDBC 協議,在拿到待執行的 sql 後,解析 sql,根據查詢條件判斷是否存在分片字段。如果存在,再根據分片算法獲取到對應的數據庫實例和物理表名,重寫 sql,然後找到對應的數據庫 datasource 並獲取物理連接,執行 sql,將結果集進行合併篩選後返回。如果沒有分片字段,則需要查詢所有的表,注意,即使存在分片字段,但是分片字段在一個範圍內,可能也需要查詢多個表,針對 select 以外的 sql 如果沒有傳分片字段建議直接拋出異常。

MySQL數據庫如何定製分庫分表中間件詳解

JDBC 協議

我們先回顧下一個完整的通過 JDBC 執行一條查詢 sql 的流程,其實 druid 也是在 JDBC 上做增強來做監控的,所以我們也可以適當參考 druid 的實現。

 1 @Test
2 public void testQ() throws SQLException,NamingException{
3 Context context = new InitialContext();
4 DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/myDataSource");
5 Connection connection = dataSource.getConnection();
6 PreparedStatement preparedStatement = connection.prepareStatement("select * from busi_order where id = ?");
7 preparedStatement.setString(1,"1");
8 ResultSet resultSet = preparedStatement.executeQuery();
9 while(resultSet.next()){
10 String orderNo = resultSet.getString("order_no");
11 System.out.println(orderNo);
12 }
13
14 preparedStatement.close();
15 connection.close();
16 }
  • datasource 需要提供根據分片結果獲取對應的數據源的datasource,返回的connection應該是定製後的 connection,因為在執行 sql 前還無法知道是哪個庫哪個表,所以只能返回一個邏輯意義上的 connection。
  • 哈希 connection 定製的 connection,需要實現獲取 statement,執行 sql 關閉。設置auto commit 等方法,在執行 sql 和獲取 statement 的時候應該進行路由找到物理表後 在執行操作。由於該 connection 是邏輯意義上的,針對關閉,設置 auto commit 等需要將關聯的多個物理 connection 一起設置。
  • statement 定製化的 statement,由於和 connection 都提供了執行 sql 的方法,所以我們可以將執行 sql 都交給一個執行器執行,connection 和 statement 中都通過這個執行器執行sql。在執行器重解析 sql 獲取物理連接,結果集處理等操作。
  • resultset resultset 是一個迭代器,遍歷的時候數據源由數據庫提供,但我們在某些有排序和 limit 的查詢中,可能迭代器直接在內存中遍歷數據。

SQL解析

sql 解析一般藉助 druid 框架裡面的 SQLStatementParser 類。解析好的數據都在 SQLStatement 中,當然有條件的可以自己研究 SQL 解析,不過可能工作量有點大。

  • 解析出 sql 類型,目前生成環境主要還是4中 sql 類型: SELECT DELETE UPDATE INSERT ,目前是直接解析 sql 是否以上面4個單詞開頭即可,不區分大小寫。
  • insert 類型需要區分,是否是批量插入,解析出 insert 插入的列的字段名稱和對應的值,如果插入的列中不包含分片字段,將無法定位到具體插入到哪個物理表,此時應該拋出異常。
  • delete 和 update 都需要解析 where 後的條件,根據查詢條件裡的字段,嘗試路由到指定的物理表,注意此時可能會出現 where 條件裡面 分片字段可能是一個範圍,或者分片字段存在多個限制。
  • select 和其他類型不同的是,返回結果是一個 list,而其他三種 sql 直接返回狀態和影響行數即可。同時 select 可能出現關聯查詢,以及針對查詢結果進行篩選的操作,例如where 條件中除了普通的判斷表達式,還可能存在 limit,order by,group by,having等,select 的結果中也可能包含聚合統計等信息,例如 sum,count,max,min,avg等,這些都需要解析出來方便後續結果集的處理,後續重新生成 sql 主要是替換邏輯表名為物理表名,並獲取對應的數據庫物理連接。
  • 針對 avg 這種操作,如果涉及查詢多個物理表的,可能需要改寫 sql 去查詢 sum 和count 的數據或者 avg 和 count 的數據,改寫需要注意可能原 sql 裡面已經包含了count,sum等操作了。

分片路由算法

分片算法,主要通過一個表達式,從分片字段對應的值獲取到分片結果,可以提供簡單地 EL表達式,就可以實現從值中截取某一段作為分表數據,也可以提供通用的一致性哈希算法的實現,應用方只需要在 xml 或者註解中配置即可,以下為一致性哈希在銅板街的實現。

 1 /**
2 * 最大真實節點數
3 */
4 private int max;
5
6 /**
7 * 真實節點的數量
8 */
9 private int current;
10
11 private int[] bucket;
12
13 private Set suffixSet;
14
15 public void init() {
16 bucket = new int[max];
17 suffixSet = new TreeSet();
18
19 int length = max / current;
20 int lengthIndex = 0;
21
22 int suffix = 0;
23
24 for (int i = 0; i < max; i++) {
25 bucket[i] = suffix;
26 lengthIndex ++;
27 suffixSet.add(suffix);
28 if (lengthIndex == length){
29 lengthIndex = 0;
30 suffix = i + 1;
31 }
32 }
33 }
34
35 public VirtualModFunction(int max, int current){
36 this.current = current;
37 this.max = max;
38 this.init();
39 }
40
41
42 @Override
43 public Integer execute(String columnValue, Map<String, Object> extension) {
44 return bucket[((Long) (Long.valueOf(columnValue) % max)).intValue()];
45 }

這裡也可以順帶做一下讀寫分離,配置一些讀操作路由到哪個實例,寫操作路由到哪個實例,並且做到負載均衡,對應用層透明。

結果集合並

如果需要在多個物理表上執行查詢,則需要對結果集進行合併處理,此處需要注意返回是一個迭代器 resultset。

  • 統計類 針對 sum count,max,min 只需要將每個結果集的返回結果在做一個 max 和min,count 和 sum 直接相加即可,針對 avg 需要通過上面改寫的 sql 獲取 sum 和count 然後相除計算平均值。
  • 排序類大部分的排序都伴隨著 limit 限制查詢條數。例如返回結果需要查詢最近的2000條記錄,並且根據創建時間倒序排序,根據路由結果需要查詢所有的物理表,假設是4張表,如果此時4張表的數據沒有時間上的排序關係,則需要每張表都查詢2000條記錄,並且按照創建時間倒序排列,現在要做的就是從4個已經排序好的鏈表,每個鏈表最多2000條數據,重新排序,選擇2000條時間最近的,我們可以通過插入排序的算法,每次分別從每個鏈表中取出時間最大的一個,在新的結果集裡找到位置並插入,直到結果集中存在2000條記錄,但是這裡可能存在一個問題,如果某一個鏈表的數據普遍比其他鏈表數據偏大,這樣每個鏈表取500條數據肯定排序不準確,所以我們還需要保證當前所有鏈表中剩下的數據的最大值比新結果集中的數據小。 而實際上業務層的需求可能並不是僅僅取出2000條數據,而是要遍歷所有的數據,這種要遍歷所有數據集的情況,建議在業務層控制一張表一張表的遍歷,如果每次都要去每張表中查詢在排序嚴重影響效率,如果在應用層控制,我們在後面在聊。
  • 聚合類 group by 應用層需要儘量避免這種操作,這些需求最好能交給搜索引擎和數據分析平臺進行,但是作為一箇中間件,對於group by 這種我們經常需要統計數據的類型還是應該儘量支持的,目前的做法是 和統計類處理類似,針對各個子集進行合併處理。

優化

以上流程基本可以實現一個簡易版本的數據庫分庫分表中間件,為了讓我們的中間件更方便開發者使用,為日常工作提供更多地遍歷性,我們還可以從以下幾點做優化。

和 spring 集成

針對哪些表需要進行分片,分片規則等,這些需要定製化的配置,我們可以在程序裡面手工編碼,但是這樣業務層又耦合了分表的邏輯,我們可以藉助 spring 的配置文件,直接將 xml 裡的內容映射成對應的 bean 實例。

  1. 我們首先要設計好對應的配置文件的格式,有哪些節點,每個節點包含哪些屬性,然後設計自己命名空間,和對應的 XSD 校驗文件,XSD 文件放在 META-INF下。
  2. 編寫 NamespaceHandlerSupport 類,註冊每個節點元素對應的解析器
 1 public class BaymaxNamespaceHandler extends NamespaceHandlerSupport {
2
3 //com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
4 @Override
5 public void init() {
6 registerBeanDefinitionParser("table", new BaymaxBeanDefinitionParser(TableConfig.class, false));
7 registerBeanDefinitionParser("context", new BaymaxBeanDefinitionParser(BaymaxSpringContext.class, false));
8 registerBeanDefinitionParser("process", new BaymaxBeanDefinitionParser(ColumnProcess.class, false));
9 }
10
11 }
  1. 在 META-INF 文件中增加配置文件 spring.handlers 中配置 spring遇到某個namespace下的節點後 通過哪個解析器解析,最終返回配置實例。
1http\://baymax.tongbanjie.com/schema/baymax-3.0=com.tongbanjie.baymax.spring.BaymaxNamespaceHandler
  1. 在 META-INF 文件中增加配置文件 spring.schema 中配置 spring遇到某個namespace下的節點後 通過哪個XSD文件進行校驗。
1http\://baymax.tongbanjie.com/schema/baymax-3.0.xsd=META-INF/baymax-3.0.xsd
  1. 可以藉助ListableBeanFactory的getBeansOfType(Class clazz) 來獲取某個class類型的所有實例,從而獲得所有的配置信息。

當然也可以通過自定義註解進行申明,這種方式我們可以藉助 BeanPostProcessor 的時候判斷類上是否包含指定的註解,但是這種方式比較笨重,而且所加註解的類必須在spring 容器管理中,也可以藉助 ClassPathScanningCandidateComponentProvider 和 AnnotationTypeFilter 實現,或者直接通過 classloader 掃描指定的包路徑。

如何支持分佈式事務

由於框架本身通過定製 JDBC 協議實現,雖然最終執行 sql 的是通過原生 JDBC,但是對上層應用透明,同時也對上層基於 JDBC 實現的事物透明,spring 的事物管理器可以直接使用。

我們考慮下以下問題,如果我們針對多張表在一個線程池內併發的區執行 sql,然後在合併結果,這是否會影響 spring的 事物管理器?

首先 spring 的聲明式事物是通過 aop 在切面做增強,事物開始先獲取 connection 並設置 setAutocommit 為 fasle,事物結束調用 connection 進行 commit 或者 rollback,通過 threadlocal 保存事物上下文和所使用的 connection 來保證事物內多個 sql共用一個 connection 操作。但是如果我們在解析 sql 後發現要執行多條 sql 語句,我們通過線程池併發執行,然後等所有的結果返回後進行合併,(這裡先不考慮,多個 sql 可能需要在不同的數據庫實例上執行),雖然通過線程池將導致 threadlocal 失效,但是我們在 threadlocal 維護的是我們自己定製的 connection,並不是原生的 JDBC 裡的 connection ,而且這裡併發執行並不會讓事物處理器沒辦法判斷是否所有的線程都已經結束,然後進行 commit 或者 rollback 。因為這裡的線程池是在我們定製的 connection 執行 sql 過程中運用的,肯定會等到所有線程處理結束後並且合併數據集才會返回。所以在本地事物層面,通過定製化 JDBC 可以做到對上層事物透明。

如果我們進行了分庫,同一個表可能在多個數據庫實例裡,這種如果要對不同實例裡的表進行更新,那麼將無法在使用本地事物,這裡我們不在討論分佈式事物的實現,由於二階段提交的各種缺點,目前很少有公司會基於二階段做分佈式事物,所以我們的中間件也可以根據自己的具體業務考慮是否要實現 XA,目前銅板街大部分分佈式事物需求都是通過基於 TCC 的事物補償做的,這種方式對業務冪等要求較高,同時要基於業務層實現回滾邏輯。

提供一個通用發號器

為什麼要提供一個發號器,我們在單表的時候,可能會用到數據庫的自增ID,但當分成多表後,每個表都進行單獨的ID自增,這樣一個邏輯表內的ID 就會出現重複。

我們可以提供一個基於邏輯表自增的主鍵ID 獲取方式,如果沒有分庫只分表,可以在數據庫中增加一個表維護每張邏輯表對應的自增ID。每次需要獲取ID 的時候都先查詢這個標當前的ID 然後加一返回,然後在寫入數據庫,為了併發獲取的情況,我們可以採用樂觀鎖,類似於CAS,update的時候傳人以前的ID。如果被人修改過則重新獲取,當然我們也可以一次性獲取一批ID例如一次獲取100個,等這100個用完了在重新獲取,為了避免這100個還沒用完,程序正常或非正常退出,在獲取這100個值的時候就將數據庫通過CAS更新為已經獲取了100個值之和的值。

不推薦用 UUID,無序,太長佔內存影響索引效果,不攜帶任何業務含義。

藉助 ZOOKEEPER 的 zone 的版本號來做序列號。

藉助 REDIS 的 INCR 命令,進行自增,每臺 redis 設置不同的初始值,但是設置相同的歩長。

1A:1,6,11,16,21
2B:2,7,12,17,22
3C:3,8,13,18,23
4D:4,9,14,19,24
5E:5,10,15,20,25

snowflake算法:其核心思想是:使用41bit作為毫秒數,10bit作為機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作為毫秒內的流水號(意味著每個節點在每毫秒可以產生 4096 個 ID)。

銅板街目前所使用的訂單號規則: - 15位時間戳,4位自增序列,2位區分訂單類型,7位機器ID,2位分庫後綴,2位分表後綴 共32位 - 7位機器ID 通過IP來獲取 - 15位時間戳精確到毫秒,4位自增序列,意味著單JVM1毫秒可以生成9999個訂單 。

最後4位可以方便的根據訂單號定位到物理表,這裡需要注意分庫分表如果是根據一致性哈希算法,這個地方最好存最大值, 例如 用戶id % 64 取餘 最多可以分64張表,而目前可能用不到這麼多,每相鄰4個數字分配到一張表,共16張表,既 userID % 64 / 4 * 4 ,而這個地方存儲 userID % 64 即可,不必存最終分表的結果,這種方式方便後續做擴容,可能分表的結果變更了,但是訂單號卻無法進行變更。

 1 @Override
2 public String routeDbByUserId(String userId) {
3 Assert.hasLength(userId, "用戶ID不能為空");
4
5 Integer userIdInteger = null;
6 try {
7 userIdInteger = Integer.parseInt(userId);
8 } catch (Exception ex) {
9 logger.error("解析用戶ID為整數失敗" + userId, ex);
10 throw new RuntimeException("解析用戶ID為整數失敗");
11 }
12
13 //根據路由規則確定,具體在哪個庫哪個表 例如根據分庫公式最終結果在0到63之間 如果要分兩個庫 mod為32 分1個庫mod為64 分16個庫 mod為4
14 //規律為 64 = mod * (最終的分庫數或分表數)
15 int mod = orderSplitConfig.getDbSegment();
16
17 Integer dbSuffixInt = userIdInteger / 64 % 64 / mod * mod ;
18
19 return StringUtils.leftPad(String.valueOf(dbSuffixInt), 2, '0');
20}
21
22
23 @Override
24 public String routeTableByUserId(String userId) {
25
26 Assert.hasLength(userId, "用戶ID不能為空");
27
28 Integer userIdInteger = null;
29 try {
30 userIdInteger = Integer.parseInt(userId);
31 } catch (Exception ex) {
32 logger.error("解析用戶ID為整數失敗" + userId, ex);
33 throw new RuntimeException("解析用戶ID為整數失敗");
34 }
35
36 //根據路由規則確定,具體在哪個庫哪個表 例如根據分表公式最終結果在0到63之間 如果要分兩個庫 mod為32 分1個庫mod為64 分16個庫 mod為4
37 //規律為 64 = mod * (最終的分庫數或分表數)
38 int mod = orderSplitConfig.getTableSegment();
39
40 Integer tableSuffixInt = userIdInteger % 64 / mod * mod;
41
42 return StringUtils.leftPad( String.valueOf(tableSuffixInt), 2, '0');
43 }

如何實現跨表遍歷

如果業務需求是遍歷所有滿足條件的數據,而不是隻是為了取某種條件下前面一批數據,這種建議在應用層實現,一張表一張表的遍歷,每次查詢結果返回下一次查詢的起始位置和物理表名,查詢的時候建議根據 大於或小於某一個 ID 進行分頁,不要 limit500,500這種,以下為銅板街的實現方式。

 1 public List<T> select(String tableName, SelectorParam selectorParam, E realQueryParam) {
2
3 List<T> list = new ArrayList<T>();
4
5 // 定位到某張表
6 String suffix = partitionManager.getCurrentSuffix(tableName, selectorParam.getLocationNo());
7
8 int originalSize = selectorParam.getLimit();
9
10 while (true) {
11
12 List<T> ts = this.queryByParam(realQueryParam, selectorParam, suffix);
13
14 if (!CollectionUtils.isEmpty(ts)) {
15 list.addAll(ts);
16 }
17
18 if (list.size() == originalSize) {
19 break;
20 }
21
22 suffix = partitionManager.getNextSuffix(tableName, suffix);
23
24 if (StringUtils.isEmpty(suffix)) {
25 break;
26 }
27
28 // 查詢下一張表 不需要定位單號 而且也只需要查剩下的size即可
29 selectorParam.setLimit(originalSize - list.size());
30 selectorParam.setLocationNo(null);
31 }
32
33 return list;
34 }

提供一個擴容工具和管理控制檯做配置可視化和監控

  1. 監控可以藉助 druid,也可以在定製的 JDBC 層自己做埋點,將數據以報表的形式進行展示,也可以針對特定的監控指標進行配置,例如執行次數,執行時間大於某個指定時間。
  2. 管理控制檯,由於目前配置是在應用層,當然也可以把配置獨立出來放在獨立的服務器上,由於分片配置基本上無法在線修改,每次修改可能都伴隨著數據遷移,所以基本上只能做展示,但是分表後我們在測試環境執行 sql 去進行邏輯查詢的時候,傳統的 sql 工具無法幫忙做到自動路由,這樣我們每次查詢可能都需要手工計算下分片結果,或者要連續寫好幾個 sql 之後在聚合,通過這個管理控制檯我們就可以直接根據邏輯表名寫 sql,這樣我們在測試環境或者在線上核對數據的時候,就提高了效率。
  3. 擴容工具,笨辦法只能先從老表查詢在 insert 到新表,等到新表數據完全同步完後,在切換到新的切片規則,所以我們設計分片算法的時候,需要考慮到後面擴容,例如一致性哈希就需要遷移一半的數據(擴容一倍的話) 數據遷移如果出現故障,那將是個災難,如果我們要在不停機的情況下完成擴容,可以通過配置文件按以下流程來。
  • 準備階段.將截至到某一刻的歷史表數據同步到新表 例如截至2017年10月1日之前的歷史數據,這些歷史數據最好不會在被修改;
  • 階段一.訪問老表,寫入老表;
  • 階段二.訪問老表,寫入老表同時寫入新表 (插入和修改);
  • 階段三.將10月1日到首次寫入新表之間的數據同步到新表 需要保證此時被遷移的數據全部都是終態;
  • 階段四.訪問新表,寫入老表和新表;
  • 階段五.訪問新表,寫入新表。

以上流程適用於,訂單這種歷史數據在達到終態後將不會在被修改,如果歷史數據也可能被修改,則可能需要停機,或者通過 canel 進行數據同步。

關注我:私信回覆“555”獲取往期Java高級架構資料、源碼、筆記、視頻Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術往期架構視頻

相關推薦

推薦中...