如何定製分表中間件

前言java

通常來講,影響數據庫最大的性能問題有兩個,一個是對數據庫的操做,一個是數據庫中的數據太大。對於前者咱們能夠藉助緩存來減小一部分讀操做,針對一些複雜的報表分析和搜索能夠交給 HadoopElasticSearch 。對於後者,咱們就只能分庫分表,讀寫分離。redis

互聯網行業隨着業務的複雜化,大多數應用都會經歷數據的垂直分區。一個複雜的流程會按照領域拆分紅不一樣的服務,每一個服務中心都擁有本身獨立的數據庫。拆分後服務共享,業務更清晰,系統也更容易擴展,同時減小了單庫數據庫鏈接數的壓力,也在必定程度上提升了單表大數據量下索引查詢的效率。固然業務隔離,也能夠避免一個業務把數據庫拖死致使全部業務都死掉,咱們將這種按照業務維度,把一個庫拆分爲多個不一樣的庫的方式叫作垂直拆分算法

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

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


數據拆分也有不少缺點,數據分散,數據庫的 Join 操做變得更加複雜,分片後數據的事務一致性很難保證,同時數據的擴容和維護難度增長,拆分規則也可能致使某個業務須要同時查詢全部的表而後進行聚合。若是須要排序和函數計算則更加複雜,因此不到萬不得已能夠先沒必要拆分。
數據庫

根據分庫分表方案中實施切片邏輯的層次不一樣,咱們將分庫分表的實現方案分紅如下4種:緩存

1. 在應用層直接分片bash

這種方式將分片規則直接放在應用層,雖然侵入了業務,開發人員不只既須要實現業務邏輯也須要實現分庫分表的配置的開發,可是實現起來簡單,適合快速上線,經過編碼方式也更容易實現跨表遍歷的狀況。後期故障也更容易定位,大多數公司都會在業務早期採用此種方式過渡,後期分表需求增多,則會尋求中間件來解決,如下代碼爲銅板街早期訂單表在 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。


切片算法

選取分片字段

分片後,若是查詢的標準是根據分片的字段,則根據切片算法,能夠路由到對應的表進行查詢。若是查詢條件中不包含分片的字段,則須要將全部的表都掃描一遍而後在進行合併。因此在設計分片的時候咱們通常會選擇一個查詢頻率較高的字段做爲分片的依據,後續的分片算法會基於該字段的值進行。例如根據建立時間字段取對應的年份,每一年一張表,取電話號碼裏面的最後一位進行分表等,這個分片的字段咱們通常會根據查詢頻率來選擇。例如在互金行業,用戶的持倉數據,咱們通常選擇用戶 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 若是沒有傳分片字段建議直接拋出異常。


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 類,註冊每一個節點元素對應的解析器

  3. 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 }
    複製代碼
  4. 在 META-INF 文件中增長配置文件 spring.handlers 中配置 spring遇到某個namespace下的節點後 經過哪一個解析器解析,最終返回配置實例。

    1http\://baymax.tongbanjie.com/schema/baymax-3.0=com.tongbanjie.baymax.spring.BaymaxNamespaceHandler
    複製代碼
  5. 在 META-INF 文件中增長配置文件 spring.schema 中配置 spring遇到某個namespace下的節點後 經過哪一個XSD文件進行校驗。

  6. 1http\://baymax.tongbanjie.com/schema/baymax-3.0.xsd=META-INF/baymax-3.0.xsd
    複製代碼
  7. 能夠藉助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 進行數據同步。


做者簡介

小強,銅板街資金端後臺開發工程師,2015年6月加入銅板街。目前負責銅板街資金端清結算相關的開發。

                                        


 本文經過案例重點介紹了若是定製分表中間件,想了解更多關於Java方面內容,請掃碼關注 「銅板街科技」 微信公衆號,並在後臺回覆 」分佈式調度、「反射」等關鍵詞獲取更多精彩內容。 

相關文章
相關標籤/搜索