輕松構建微服務之分庫分表


微信公眾號:內核小王子
關注可了解更多關於數據庫,JVM內核相關的知識;
如果你有任何疑問也可以加我pigpdong[^1]

前言

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

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

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

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

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

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

1.在應用層直接分片

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

@Override
public OrderDO findByPrimaryKey(String orderNo) {

    Assert.hasLength(orderNo, "訂單號不能為空");

    Map<String, Object> map = new HashMap<String, Object>(3);
    map.put("tableSuffix", orderRouter.routeTableByOrderNo(orderNo));
    map.put("dbSuffix", orderRouter.routeDbByOrderNo(orderNo));
    map.put("orderNo", orderNo);

    Object obj = getSqlSession().selectOne("NEW_ORDER.FIND_BY_PRIMARYKEY", map);
    if (obj != null && obj instanceof OrderDO) {
        return (OrderDO) obj;
    }
    return null;
}

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的實現

@Test
public void testQ() throws SQLException,NamingException{
    Context context = new InitialContext();
    DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/myDataSource");
    Connection connection = dataSource.getConnection();
    PreparedStatement preparedStatement = connection.prepareStatement("select * from busi_order where id = ?");
    preparedStatement.setString(1,"1");
    ResultSet resultSet = preparedStatement.executeQuery();
    while(resultSet.next()){
       String orderNo =  resultSet.getString("order_no");
       System.out.println(orderNo);
    }

    preparedStatement.close();
    connection.close();
}

  • 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或者注解中配置即可,以下為一致性哈希在銅板街的實現

/**
 * 最大真實節點數
 */
private int max;

/**
 * 真實節點的數量
 */
private int current;

private int[] bucket;

private Set suffixSet;

public void init()  {
    bucket = new int[max];
    suffixSet = new TreeSet();

    int length = max / current;
    int lengthIndex = 0;

    int suffix = 0;

    for (int i = 0; i < max; i++) {
        bucket[i] = suffix;
        lengthIndex ++;
        suffixSet.add(suffix);
        if (lengthIndex == length){
            lengthIndex = 0;
            suffix = i + 1;
        }
    }
}

public VirtualModFunction(int max, int current){
    this.current = current;
    this.max = max;
    this.init();
}


@Override
public Integer execute(String columnValue, Map<String, Object> extension) {
    return bucket[((Long) (Long.valueOf(columnValue) % max)).intValue()];
}

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

結果集合並

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

1.統計類 針對sum count ,max,min 只需要將每個結果集的返回結果在做一個max和min,count和sum直接相加即可,針對avg需要通過上面改寫的sql獲取sum和count 然后相除計算平均值

2.排序類 大部分的排序都伴隨着limit限制查詢條數,例如返回結果需要查詢最近的2000條記錄,並且根據創建時間倒序排序,根據路由結果需要查詢所有的物理表,假設是4張表,如果此時4張表的數據沒有時間上的排序關系,則需要每張表都查詢2000條記錄,並且按照創建時間倒序排列,現在要做的就是從4個已經排序好的鏈表,每個鏈表最多2000條數據,重新排序,選擇2000條時間最近的,我們可以通過插入排序的算法,每次分別從每個鏈表中取出時間最大的一個,在新的結果集里找到位置並插入,直到結果集中存在2000條記錄,但是這里可能存在一個問題,如果某一個鏈表的數據普遍比其他鏈表數據偏大,這樣每個鏈表取500條數據肯定排序不准確,所以我們還需要保證當前所有鏈表中剩下的數據的最大值比新結果集中的數據小。 而實際上業務層的需求可能並不是僅僅取出2000條數據,而是要遍歷所有的數據,這種要遍歷所有數據集的情況,建議在業務層控制一張表一張表的遍歷,如果每次都要去每張表中查詢在排序嚴重影響效率,如果在應用層控制,我們在后面在聊。

3.聚合類 group by 應用層需要盡量避免這種操作,這些需求最好能交給搜索引擎和數據分析平台進行,但是作為一個中間件,對於group by 這種我們經常需要統計數據的類型還是應該盡量支持的,目前的做法是 和統計類處理類似,針對各個子集進行合並處理。

優化

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

和spring集成

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

1.我們首先要設計好對應的配置文件的格式,有哪些節點,每個節點包含哪些屬性,然后設計自己命名空間,和對應的XSD校驗文件,XSD文件放在META-INF下.

2.編寫 NamespaceHandlerSupport 類,注冊每個節點元素對應的解析器

public class BaymaxNamespaceHandler extends NamespaceHandlerSupport {

	//com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
	@Override
	public void init() {
		registerBeanDefinitionParser("table", new BaymaxBeanDefinitionParser(TableConfig.class, false));
		registerBeanDefinitionParser("context", new BaymaxBeanDefinitionParser(BaymaxSpringContext.class, false));
		registerBeanDefinitionParser("process", new BaymaxBeanDefinitionParser(ColumnProcess.class, false));
	}

}

3.在META-INF文件中增加配置文件 spring.handlers 中配置 spring遇到某個namespace下的節點后 通過哪個解析器解析,最終返回配置實例

http\://baymax.tongbanjie.com/schema/baymax-3.0=com.tongbanjie.baymax.spring.BaymaxNamespaceHandler

4.在META-INF文件中增加配置文件 spring.schema 中配置 spring遇到某個namespace下的節點后 通過哪個XSD文件進行校驗

http\://baymax.tongbanjie.com/schema/baymax-3.0.xsd=META-INF/baymax-3.0.xsd

5.可以借助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設置不同的初始值,但是設置相同的歩長。

A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E: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 即可,不必存最終分表的結果,這種方式方便后續做擴容,可能分表的結果變更了,但是訂單號卻無法進行變更

@Override
public String routeDbByUserId(String userId) {
    Assert.hasLength(userId, "用戶ID不能為空");

    Integer userIdInteger = null;
    try {
        userIdInteger = Integer.parseInt(userId);
    } catch (Exception ex) {
        logger.error("解析用戶ID為整數失敗" + userId, ex);
        throw new RuntimeException("解析用戶ID為整數失敗");
    }

    //根據路由規則確定,具體在哪個庫哪個表 例如根據分庫公式最終結果在0到63之間  如果要分兩個庫 mod為32 分1個庫mod為64 分16個庫 mod為4
    //規律為 64 = mod * (最終的分庫數或分表數)
    int mod = orderSplitConfig.getDbSegment();

    Integer dbSuffixInt = userIdInteger / 64 % 64 / mod * mod ;

    return StringUtils.leftPad(String.valueOf(dbSuffixInt),  2, '0');
}


@Override
public String routeTableByUserId(String userId) {

    Assert.hasLength(userId, "用戶ID不能為空");

    Integer userIdInteger = null;
    try {
        userIdInteger = Integer.parseInt(userId);
    } catch (Exception ex) {
        logger.error("解析用戶ID為整數失敗" + userId, ex);
        throw new RuntimeException("解析用戶ID為整數失敗");
    }

    //根據路由規則確定,具體在哪個庫哪個表 例如根據分表公式最終結果在0到63之間  如果要分兩個庫 mod為32 分1個庫mod為64 分16個庫 mod為4
    //規律為 64 = mod * (最終的分庫數或分表數)
    int mod = orderSplitConfig.getTableSegment();

    Integer tableSuffixInt = userIdInteger % 64 / mod * mod;

    return StringUtils.leftPad( String.valueOf(tableSuffixInt),  2, '0');
}

如何實現跨表遍歷

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

public List<T> select(String tableName, SelectorParam selectorParam, E realQueryParam) {

    List<T> list = new ArrayList<T>();

    // 定位到某張表
    String suffix = partitionManager.getCurrentSuffix(tableName, selectorParam.getLocationNo());

    int originalSize = selectorParam.getLimit();

    while (true) {

        List<T> ts = this.queryByParam(realQueryParam, selectorParam, suffix);

        if (!CollectionUtils.isEmpty(ts)) {
            list.addAll(ts);
        }

        if (list.size() == originalSize) {
            break;
        }

        suffix = partitionManager.getNextSuffix(tableName, suffix);

        if (StringUtils.isEmpty(suffix)) {
            break;
        }

        // 查詢下一張表 不需要定位單號 而且也只需要查剩下的size即可
        selectorParam.setLimit(originalSize - list.size());
        selectorParam.setLocationNo(null);
    }

    return list;
}

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

1.監控可以借助druid,也可以在定制的jdbc層自己做埋點,將數據以報表的形式進行展示,也可以針對特定的監控指標進行配置,例如執行次數,執行時間大於某個指定時間

2.管理控制台,由於目前配置是在應用層,當然也可以把配置獨立出來放在獨立的服務器上,由於分片配置基本上無法在線修改,每次修改可能都伴隨着數據遷移,所以基本上只能做展示,但是分表后我們在測試環境執行sql去進行邏輯查詢的時候,傳統的sql工具無法幫忙做到自動路由,這樣我們每次查詢可能都需要手工計算下分片結果,或者要連續寫好幾個sql之后在聚合,通過這個管理控制台我們就可以直接根據邏輯表名寫sql,這樣我們在測試環境或者在線上核對數據的時候,就提高了效率

3.擴容工具,笨辦法只能先從老表查詢在insert到新表,等到新表數據完全同步完后,在切換到新的切片規則,所以我們設計分片算法的時候,需要考慮到后面擴容,例如一致性哈希就需要遷移一半的數據(擴容一倍的話) 數據遷移如果出現故障,那將是個災難,如果我們要在不停機的情況下完成擴容,可以通過配置文件按以下流程來

  • 准備階段.將截至到某一刻的歷史表數據同步到新表 例如截至2017年10月1日之前的歷史數據,這些歷史數據最好不會在被修改
  • 階段一.訪問老表,寫入老表
  • 階段二.訪問老表,寫入老表同時寫入新表 (插入和修改)
  • 階段三.將10月1日到首次寫入新表之間的數據同步到新表 需要保證此時被遷移的數據全部都是終態
  • 階段四.訪問新表,寫入老表和新表
  • 階段五.訪問新表,寫入新表

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

mycat


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM