天天看點

001---mysql分庫分表

mysql分庫分表

一、整體的切分方式

1、分庫分表:即資料的切分就是通過某種特定的條件,将我們存放在同一個資料庫中的資料分散存放到多個資料庫(主機)中,以達到分散單台裝置負載的效果

2、資料的切分根據其切分規則的類型,可以分為如下兩種切分模式

【1】垂直(縱向)切分:把單一的表拆分成多個表 / 将不相關的表,分散到不同的資料庫(主機)上。

        如:使用者表、商品SKU表、交易Pay表,根據業務不同進行切分,将表切分到不同資料庫上。

        優點:

         (1)。拆分後業務清晰,拆分規則明确

         (2)。系統之間進行整合或擴充很容易

         (3)。按照成本、應用的等級、應用的類型等将表放到不同的機器上,便于管理

         (4)。便于實作動靜分離、冷熱分離的資料庫表的設計模式

                     冷熱分離:冷資料(查詢多,變化少),熱資料/活躍資料(變化多)。将冷熱資料分開存放即冷熱分離。

         (5)。資料維護簡單

       缺點:

         (1)。部分業務表無法關聯(Join),隻能通過接口方式解決,提高了系統的複雜度

         (2)。受每種業務的不同限制,存在單庫性能瓶頸,不易進行資料擴充和提升性能

         (3)。事務處理複雜

設計資料庫表結構時考慮因素:

(1)。冷資料适合MyISAM存儲引擎(查詢快) ,熱資料适合InnoDB存儲引擎(更新快)。

(2)。主從分離:主庫用于寫入操作,從庫(可配置更多從庫)用于查詢操作,分擔請求壓力。

(3)。針對活躍/熱資料,可采用Memcache、Redis等緩存,待累計一定量時再更新DB。

【2】水準(橫向)切分:根據表中資料的邏輯關系,将同一個表中的資料按照某種條件拆分到多台資料庫(主機)上。

         如:使用者表可根據ID被水準切分為使用者1表 & 使用者2表 (表結構一緻),即使用者表資料=使用者1表資料+使用者2表資料

         優點:

         (1)。單庫單表的資料保持在一定的量級,有助于性能的提高。

         (2)。切分的表的結構相同,應用層改造較少,隻需要增加路由規則即可。

         (3)。提高了系統的穩定性和負載能力。

        缺點:

         (1)。切分後,資料是分散的,很難利用資料庫的Join操作,跨庫Join性能較差

         (2)。拆分規則難以抽象。

         (3)。分片事務的一緻性難以解決。

         (4)。資料擴容的難度和維護量極大。

     以上,水準/垂直切分共同點:

  • 存在分布式事務的問題。
  • 存在跨節點Join的問題。
  • 存在跨節點合并排序、分頁的問題。
  • 存在多資料源管理的問題。

二、水準切分方式的路由過程和分片次元

1. 水準切分的路由過程

我們在設計表時需要确定對表按照什麼樣的規則進行分庫分表。

例如,當有新使用者時,程式得确定

          (1)。将此使用者的資訊添加到哪個表中;

           (2)。在登入時我們需要通過使用者的賬号找到資料庫中對應的記錄。

         所有這些都需要按照某一規則進行路由請求,因為請求所需要的資料分布在不同的分片表中。

以上:針對輸入的請求,通過分庫分表規則查找到對應的表和庫的過程叫作路由。

例如:

分庫分表的規則是user_id % 4,當使用者新注冊了一個賬号時,假設使用者的ID是123,我們就可以通過123 % 4 = 3确定此賬号應該被儲存在User3表中。

當ID為123的使用者登入時,我們可通過123 % 4 = 3計算後,确定其被記錄在User3中。

2. 水準切分的分片次元(可參考Myca提供的切片方式)

【1】按照哈希切片

         哈希分片:對資料的某個字段求哈希,再除以分片總數後取模,取模後相同的資料為一個分片。

         按照哈希分片常常應用于資料沒有時效性的情況,比如所有資料無論是在什麼時間産生的,都需要進行處理或者查詢。

         優點:資料切片比較均勻,對資料壓力分散的效果較好。

         缺點:資料分散後,對于查詢需求需要進行聚合處理。

【2】按照時間切片

         時間分片:按照時間的範圍将資料分布到不同的分片上。(這種切片方式适用于有明顯時間特點的資料)

三、分片後的事務處理機制

(一)、分布式事務

由于我們将單表的資料切片後存儲在多個資料庫甚至多個資料庫執行個體中,是以依靠資料庫本身的事務機制不能滿足所有場景的需要。

但是,我們推薦在一個資料庫執行個體中的操作盡可能使用本地事務來保證一緻性,跨資料庫執行個體的一系列更新操作需要根據事務路由在不同的資料源中完成,各個資料源之間的更新操作需要通過分布式事務處理。

主流的分布式事務解決方案有三種:兩階段送出協定、最大努力保證模式和事務補償機制。

1、兩階段送出協定

兩階段送出協定将分布式事務分為兩個階段,一個是準備階段,一個是送出階段,兩個階段都由事務管理器發起。

基于兩階段送出協定,事務管理器能夠最大限度地保證跨資料庫操作的事務的原子性,是分布式系統環境下最嚴格的事務實作方法。

符合J2EE規範的AppServer(例如:Websphere、Weblogic、Jboss等)對關系型資料庫資料源和消息隊列都實作了兩階段送出協定,隻需在使用時配置即可

如圖所示:

001---mysql分庫分表

但是,兩階段送出協定也帶來了性能方面的問題:

(1)。難于進行水準伸縮,因為在送出事務的過程中,事務管理器需要和每個參與者進行準備和送出的操作的協調,在準備階段鎖定資源,在送出階段消費資源。

(2)。由于參與者較多,鎖定資源和消費資源之間的時間差被拉長,導緻響應速度較慢,在此期間産生死鎖或者不确定結果的可能性較大。

是以,在網際網路行業裡,為了追求性能的提升,很少使用兩階段送出協定。

另:由于兩階段送出協定是阻塞協定,在極端情況下不能快速響應請求方,是以有人提出了三階段送出協定。

三階段送出協定:解決了兩階段送出協定的阻塞問題,但仍然需要事務管理器在參與者之間協調,才能完成一個分布式事務。

2、最大努力保證模式

最大努力保證模式:是一種非常通用的保證分布式一緻性的模式。(很多開發人員一直在使用,但是并未意識到這是一種模式)

最大努力保證模式适用于對一緻性要求并不十分嚴格但是對性能要求較高的場景。

具體的實作方法:

在更新多個資源時,将多個資源的送出盡量延後到最後一刻處理,這樣,若業務流程出現問題,則所有的資源更新都可以復原,事務仍保持一緻。

唯一可能出現問題的情況是在送出多個資源時發生了系統問題。

如:網絡問題等,但是這種情況是非常罕見的,一旦出現這種情況,就需要進行實時補償,将已送出的事務進行復原,這和我們常說的TCC模式有些類似。

下面是使用最大努力保證模式的一個樣例,在該樣例中涉及兩個操作,一個是從消息隊列消費消息,一個是更新資料庫,需要保證分布式的一緻性。

(1)開始消息事務。

(2)開始資料庫事務。

(3)接收消息。

(4)更新資料庫。

(5)送出資料庫事務。

(6)送出消息事務。

注:從第1步到第4步并不是很關鍵,關鍵的是第5步和第6步,需要将其放在最後一起送出,盡最大努力保證前面的業務處理的一緻性。

到了第5步和第6步,業務邏輯處理完成,這時隻可能發生系統錯誤。

若第5步失敗,則可以将消息隊列和資料庫事務全部復原,保持一緻。

若第5步成功,第6步遇到了網絡逾時等問題,則這是唯一可能産生問題的情況。

在這種情況下,消息的消費過程并沒有被送出到消息隊列,消息隊列可能會重新發送消息給其他消息處理服務,

這會導緻消息被重複消費,但是可以通過幂等處理來保證消除重複消息帶來的影響。

當然,在使用這種模式時,我們要充分考慮每個資源的送出順序。

我們在生産實踐中遇到的一種反模式,就是在資料庫事務中嵌套遠端調用,而且遠端調用是耗時任務,導緻資料庫事務被拉長,最後拖垮資料庫。

是以,上面的案例涉及的是消息事務嵌套資料庫事務,在這裡必須進行充分評估和設計,才可以規避事務風險。

3、事務補償機制

顯然,在對性能要求很高的場景中:

兩階段送出協定并不是一種好方案。

最大努力保證模式也會使多個分布式操作互相嵌套,有可能互相影響。

這裡,我們給出事務補償機制,其性能很高,并且能夠盡最大可能地保證事務的最終一緻性。

在資料庫分庫分表後,如果涉及的多個更新操作在某一個資料庫範圍内完成,則可以使用資料庫内的本地事務保證一緻性;

對于跨庫的多個操作,可通過補償和重試,使其在一定的時間視窗内完成操作,這樣就可以實作事務的最終一緻性,突破事務遇到問題就滾回的傳統思路。

如果采用事務補償機制,則在遇到問題時,我們需要記錄遇到問題的環境、資訊、步驟、狀态等,後續通過重試機制使其達到最終一緻性,

詳細内容可以參考《分布式服務架構:原理、設計與實戰》第2章,徹底了解ACID原理、CAP理論、BASE原理、最終一緻性模式等内容。

(二)、事務路由

無論使用上面哪種方法實作分布式事務,都需要對分庫分表的多個資料源路由事務,一般通過對Spring環境的配置,為不同的資料源配置不同的事務管理器(TransactionManager)。

這樣,如果更新操作在一個資料庫執行個體内發生,便可以使用資料源的事務來處理。對于跨資料源的事務,可通過在應用層使用最大努力保證模式和事務補償機制來達成事務的一緻性。

當然,有時我們需要通過編寫程式來選擇資料庫的事務管理器,根據實作方式的不同,可将事務路由具體分為以下三種。

1、自動送出事務路由

自動送出事務通過依賴JDBC資料源的自動送出事務特性,對任何資料庫進行更新操作後會自動送出事務,不需要開發人員手工操作事務,也不需要配置事務,實作起來很簡單,但是隻能滿足簡單的業務邏輯需求。

在通常情況下,JDBC在連接配接建立後預設設定自動送出為true,當然,也可以在擷取連接配接後手工修改這個屬性,代碼如下:

connnection conn = null;
try{
conn = getConnnection();
conn.setAutoCommit(true);
// 資料庫操作
……………………………
conn.commit();
}catch(Throwable e){
if(conn!=null){
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
throw new RuntimeException(e);
}finally{
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();}}
}       

我們基本不需要使用原始的JDBC API來改變這些屬性,這些操作一般都會被封裝在我們使用的架構中

2、可程式設計事務路由

我們在應用中通常采用Spring的聲明式的事務來管理資料庫事務。在分庫分表時,事務處理是個問題,在一個需要開啟事務的方法中,需要動态地确定開啟哪個資料庫執行個體的事務,也就是說在每個開啟事務的方法調用前就必須确定開啟哪個資料源的事務。下面使用僞代碼來說明如何實作一個可程式設計事務路由的小架構。

首先,通過Spring配置檔案展示可程式設計事務小架構是怎麼使用的:

<?xml version="1.0?>
<beans>
   <bean id="sharding-db-trx0"class="org.springframework.jdbc.datasource.Data SourceTransactionManager">
       <property name="dataSource">
           <ref bean="sharding-db0" />
       </property>
   </bean>
   <bean id="sharding-db-trx1" class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger">
       <property name="dataSource">
           <ref bean="sharding-db1" />
       </property>
   </bean>
   <bean id="sharding-db-trx2" class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger">
       <property name="dataSource">
         <ref bean="sharding-db2" />
       </property>
    </bean>
    <bean id="sharding-db-trx3" class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger">
       <property name="dataSource">
          <ref bean="sharding-db3" />
       </property>
    </bean>
    <bean id="shardingTransactionManager" class="com.robert.dbsplit.core. ShardingTransactionManager">
       <property name="proxyTransactionManagers">
         <map value-type="org.springframework.transaction.PlatformTran sactionManager">
            <entry key="sharding0" value-ref="sharding-db-trx0" />
            <entry key="sharding1" value-ref="sharding-db-trx1" />
           <entry key="sharding2" value-ref="sharding-db-trx2" />
           <entry key="sharding3" value-ref="sharding-db-trx3" />
         </map>
    </property>
  </bean>
  <aop:config>
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*insert(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*update(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*delete(..))"/>
  </aop:config>
  <tx:advice id="txAdvice" transaction-manager="shardingTransactionManager">
    <tx:attributes>
      <tx:method name="*" rollback-for="java.lang.Exception"/>
    </tx:attributes>
  </tx:advice>
</beans>      

這裡使用Spring環境的aop和tx标簽來攔截com.robert.biz包下的所有插入、更新和删除的方法,

當指定的包的方法被調用時,就會使用Spring提供的事務Advice,Spring的事務Advice(tx:advice)會使用事務管理器來控制事務,

如果某個方法發生了異常,那麼Spring的事務Advice就會使shardingTransactionManager復原相應的事務。

我們看到shardingTransactionManager的類型是ShardingTransactionManager,這個類型是我們開發的一個組合的事務管理器,

ShardingTransactionManager事務管理器聚合了所有分片資料庫的事務管理器對象,然後根據某個标記來路由到不同的事務管理器中,

這些事務管理器用來控制各個分片的資料源的事務。

這裡的标記是什麼呢?我們在調用方法時,會提前把分片的标記放進ThreadLocal中,

然後在ShardingTransactionManager的getTransaction方法被調用時,取得ThreadLocal中存的标記,

最後根據标記來判斷使用哪個分片資料庫的事務管理器對象。

為了通過标記路由到不同的事務管理器,我們設計了一個專門的ShardingContextHolder類,

在該類的内部使用了一個ThreadLocal類來指定分片資料庫的關鍵字,

在ShardingTransaction Manager中通過取得這個标記來選擇具體的分片資料庫的事務管理器對象。

是以,這個類提供了setShard和getShard的方法:

setShard用于使用者程式設計指定使用哪個分片資料庫的事務管理器,

getShard用于ShardingTransactionManager擷取标記并取得分片資料庫的事務管理器對象。

相關代碼如下:

public class ShardingContextHolder<T> {
private static final ThreadLocal shardHolder = new ThreadLocal();
public static <T> void setShard(T shard) {
  Validate.notNull(shard, "請指定某個分片資料庫!");
  shardHolder.set(shard);
}
public static <T> T getShard() {
return (T) shardHolder.get();
  }
}      

有了ShardingContextHolder類後,我們就可以在ShardingTransactionManager中根據給定的分片配置将事務操控權路由到不同分片的資料庫的事務管理器上,實作很簡單,如果在ThreadLocal中存儲了某個分片資料庫的事務管理器的關鍵字,就使用那個分片的資料庫的事務管理器:

public class ShardingTransactionManager implements PlatformTransactionManager {

private Map<Object, PlatformTransactionManager> proxyTransactionManagers =

new HashMap<Object, PlatformTransactionManager>();

protected PlatformTransactionManager getTargetTransactionManager() {

Object shard = ShardingContextHolder.getShard();

Validate.notNull(shard, "必須指定一個路由的shard!");

return targetTransactionManagers.get(shard);

}

public void setProxyTransactionManagers(Map<Object, PlatformTransaction Manager> targetTransactionManagers) {

this.targetTransactionManagers = targetTransactionManagers;

}

public void commit(TransactionStatus status) throws TransactionException {

getProxyTransactionManager().commit(status);

}

public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {

return getProxyTransactionManager().getTransaction(definition);

}

public void rollback(TransactionStatus status) throws TransactionException

{

getProxyTransactionManager().rollback(status);

  }
}      

有了這些使用類,我們的可程式設計事務路由小架構就實作了,這樣在某個具體的服務開始之前,我們就可以使用如下代碼來控制使用某個分片的資料庫的事務管理器了:

RoutingContextHolder.setShard("sharding0");

return userService.create(user);      

3、聲明式事務路由

在上一小節實作了可程式設計事務路由的小架構,這個小架構通過讓開發人員在ThreadLocal中指定資料庫分片并程式設計實作

大多數分庫分表架構會實作聲明式事務路由,也就是在實作的服務方法上直接聲明事務的處理注解,

注解包含使用哪個資料庫分片的事務管理器的資訊,這樣,開發人員就可以專注于業務邏輯的實作,把事務處理交給架構來實作。

下面是實際的線上項目中實作的聲明式事務路由的一個使用執行個體:

@TransactionHint(table = "INVOICE", keyPath = "0.accountId")

public void persistInvoice(Invoice invoice) {

// Save invoice to DB

this.createInvoice(invoice);

for (InvoiceItem invoiceItem : invoice.getItems()) {

invoiceItem.setInvId(invoice.getId());

invoiceItemService.createInvoiceItem(invoice.getAccountId(), invoiceItem);

}

// Save invoice to cache

invoiceCacheService.set(invoice.getAccountId(), invoice.getInvPeriodStart().getTime(), invoice.getInvPeriodEnd().getTime(),

invoice);

// Update last invoice date to Account

Account account = new Account();

account.setId(invoice.getAccountId());

account.setLstInvDate(invoice.getInvPeriodEnd());

accountService.updateAccount(account);

}      

在這個執行個體中,我們開發了一個持久發票的服務方法。持久發票的服務方法用來儲存發票資訊和發票項的詳情資訊。

這裡,發票與發票項這兩個領域對象具有父子結構關系。

由于在設計過程中通過賬戶ID對這個父子表進行分庫分表,是以在進行事務路由時,也需要通過賬戶ID控制使用哪個資料庫分片的事務管理器。

在這個執行個體中,我們配置了 TransactionHint,TransactionHint的聲明如下:

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface TransactionHint {

String table() default "";

String keyPath() default "";

}      

TransactionHint包含了兩個屬性:

第1個屬性table:指定這次操作涉及分片的資料庫表

第2個屬性keypath:指定這次操作根據哪個參數的哪個字段進行分片路由。

該執行個體通過table指定了INVOICE表,并通過keyPath指定了使用第1個參數的字段accountId作為路由的關鍵字。

這裡的實作與可程式設計事務路由的小架構實作類似,在方法persistInvoice被調用時,根據TransactionHint提供的操作的資料庫表名稱,在Spring環境的配置中找到這個表的分庫分表的配置資訊,例如:一共分了多少個資料庫執行個體、資料庫和表。

下面是在Spring環境中配置的INVOICE表和INVOICE_ITEM表的具體資訊,我們看到它們一共使用了兩個資料庫執行個體,每個執行個體有兩個庫,每個庫有8個表,使用水準下标政策。配置如下:

<bean name="billingInvSplitTable" class="com.robert.dbsplit.core.Split Table"init-method="init">

<property name="dbNamePrefix" value="billing_inv"/>

<property name="tableNamePrefix" value="INVOICE"/>

<property name="dbNum" value="2"/>

<property name="tableNum" value="8"/>

<property name="splitStrategyType" value="HORIZONTAL"/>

<property name="splitNodes">

<list>

<ref bean="splitNode0"/>

<ref bean="splitNode1"/>

</list>

</property>

<property name="readWriteSeparate" value="true"/>

</bean>

<bean name="billingInvItemSplitTable" class="com.robert.dbsplit.core.SplitTable" init-method="init">

<property name="dbNamePrefix" value="billing_inv"/>

<property name="tableNamePrefix" value="INVOICE_ITEM"/>

<property name="dbNum" value="2"/>

<property name="tableNum" value="8"/>

<property name="splitStrategyType" value="HORIZONTAL"/>

<property name="splitNodes">

<list>

<ref bean="splitNode0"/>

<ref bean="splitNode1"/>

</list>

</property>

<property name="readWriteSeparate" value="true"/>

</bean>      

然後,在方法被調用時通過AOP進行攔截,根據TransactionHint配置的路由的主鍵資訊keyPath = "0.accountId",得知這次根據第0個參數Invoice的accountID字段來路由,根據Invoice的accountID的值來計算這次持久發票表具體涉及哪個資料庫分片,然後把這個資料庫分片的資訊儲存到ThreadLocal中。具體的實作代碼如下:

SimpleSplitJdbcTemplate simpleSplitJdbcTemplate =

(SimpleSplitJdbcTemplate) ReflectionUtil.getFieldValue(field SimpleSplitJdbcTemplate, invocation.getThis());

Method method = invocation.getMethod();

// Convert to th method of implementation class

method = targetClass.getMethod(method.getName(), method.getParameter Types());

TransactionHint[] transactionHints = method.getAnnotationsByType (TransactionHint.class);

if (transactionHints == null || transactionHints.length < 1)

throw new IllegalArgumentException("The method " + method + " includes illegal transaction hint.");

TransactionHint transactionHint = transactionHints[0];

String tableName = transactionHint.table();

String keyPath = transactionHint.keyPath();

String[] parts = keyPath.split("\.");

int paramIndex = Integer.valueOf(parts[0]);

Object[] params = invocation.getArguments();

Object splitKey = params[paramIndex];

if (parts.length > 1) {

String[] paths = Arrays.copyOfRange(parts, 1, parts.length);

splitKey = ReflectionUtil.getFieldValueByPath(splitKey, paths);

}

SplitNode splitNode = simpleSplitJdbcTemplate.decideSplitNode(tableName, splitKey);

ThreadContextHolder.INST.setContext(splitNode);

ThreadContextHolder是一個單例的對象,在該對象裡封裝了一個ThreadLocal,用來存儲某個方法在某個線程下關聯的分片資訊:

public class ThreadContextHolder<T> {

public static final ThreadContextHolder<SplitNode> INST = new ThreadContextHolder<SplitNode>();

private ThreadLocal<T> contextHolder = new ThreadLocal<T>();

public T getContext() {

return contextHolder.get();

}

public void setContext(T context) {

contextHolder.set(context);

 }
}      

接下來與可程式設計式事務路由類似,實作一個定制化的事務管理器,在擷取目标事務管理器時,通過我們在ThreadLocal中儲存的資料庫分片資訊,獲得這個分片資料庫的事務管理器,然後傳回:

public class RoutingTransactionManager implements PlatformTransactionManager {

protected PlatformTransactionManager getTargetTransactionManager() {

SplitNode splitNode = ThreadContextHolder.INST.getContext();

return splitNode.getPlatformTransactionManager();

}

public void commit(TransactionStatus status) throws TransactionException {

getTargetTransactionManager().commit(status);

}

public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {

return getTargetTransactionManager().getTransaction(definition);

}

public void rollback(TransactionStatus status) throws TransactionException

{

getTargetTransactionManager().rollback(status);

}

}      

 四、讀寫分離

MySQL提供了讀寫分離的機制,所有寫操作必須對應到主庫(Master),讀操作可以在主庫(Master)和從庫(Slave)機器上進行。

主庫與從庫的結構完全一樣,一個主庫可以有多個從庫,甚至在從庫下還可以挂從庫,這種一主多從的方式可以有效地提高資料庫叢集的吞吐量。

在DBA領域一般配置主-主-從或者主-從-從兩種部署模型

所有寫操作都先在主庫上進行,然後異步更新到從庫上,是以從主庫同步到從庫機器有一定的延遲,當系統很繁忙時,延遲問題會更加嚴重,從庫機器數量的增加也會使這個問題更嚴重。

此外,主庫是叢集的瓶頸,當寫操作過多時會嚴重影響主庫的穩定性,如果主庫挂掉,則整個叢集都将不能正常工作。

根據以上特點,我們總結一些最佳實踐如下:

1、當讀操作壓力很大時,可以考慮添加從庫機器來分解大量讀操作帶來的壓力,但是當從庫機器達到一定的數量時,就需要考慮分庫來緩解壓力了。

2、當寫壓力很大時,就必須進行分庫操作了。

可能會因為種種原因,叢集中的資料庫硬體配置等會不一樣,某些性能高,某些性能低,這時可以通過程式控制每台機器讀寫的比重來達到負載均衡,這需要更加複雜的讀寫分離的路由規則。

五、分庫分表引起的問題

分庫分表按照某種規則将資料的集合拆分成多個子集合,資料的完整性被打破,是以在某種場景下會産生多種問題。

1、擴容與遷移

在分庫分表後,如果涉及的分片已經達到了承載資料的最大值,就需要對叢集進行擴容。擴容是很麻煩的,一般會成倍地擴容。

通用的擴容方法包括如下5個步驟:

(1)。按照新舊分片規則,對新舊資料庫進行雙寫

(2)。将雙寫前按照舊分片規則寫入的曆史資料,根據新分片規則遷移寫入新的資料庫

(3)。将按照舊的分片規則查詢改為按照新的分片規則查詢

(4)。将雙寫資料庫邏輯從代碼中下線,隻按照新的分片規則寫入資料

(5)。删除按照舊分片規則寫入的曆史資料

在(2)遷移曆史資料時,由于資料量很大,通常會導緻不一緻,是以,先清洗舊的資料,洗完後再遷移到新規則的新資料庫下,再做全量對比,對比後評估在遷移的過程中是否有資料的更新,如果有的話就再清洗、遷移,最後以對比沒有差距為準。

如果是金融交易資料,則最好将動靜資料分離,随着時間的流逝,某個時間點之前的資料是不會被更新的,我們就可以拉長雙寫的時間視窗,這樣在足夠長的時間流逝後,隻需遷移那些不再被更新的曆史資料即可,就不會在遷移的過程中由于曆史資料被更新而導緻代理不一緻。

在資料量巨大時,如果資料遷移後沒法進行全量對比,就需要進行抽樣對比,在進行抽樣對比時要根據業務的特點選取一些具有某類特征性的資料進行對比。

在遷移的過程中,資料的更新會導緻不一緻,可以線上上記錄遷移過程中的更新操作的日志,遷移後根據更新日志與曆史資料共同決定資料的最新狀态,來達到遷移資料的最終一緻性。

2. 分庫分表次元導緻的查詢問題

在分庫分表以後,如果查詢的标準是分片的主鍵,則可以通過分片規則再次路由并查詢;

但是對于其他主鍵的查詢、範圍查詢、關聯查詢、查詢結果排序等,并不是按照分庫分表次元來查詢的。

例如,使用者購買了商品,需要将交易記錄儲存下來,那麼如果按照買家的緯度分表,則每個買家的交易記錄都被儲存在同一表中,我們可以很快、很友善地查到某個買家的購買情況,但是某個商品被購買的交易資料很有可能分布在多張表中,查找起來比較麻煩。

反之,按照商品次元分表,則可以很友善地查找到該商品的購買情況,但若要查找到買家的交易記錄,則會比較麻煩。

是以常見的解決方式如下:

(1)。在多個分片表查詢後合并資料集,這種方式的效率很低

(2)。記錄兩份資料,一份按照買家緯度分表,一份按照商品次元分表

(3)。通過搜尋引擎解決,但如果實時性要求很高,就需要實作實時搜尋

實際上,在高并發的服務平台下,交易系統是專門做交易的,因為交易是核心服務,SLA的級别比較高,是以需要和查詢系統分離,查詢一般通過其他系統進行,資料也可能是備援存儲的。

如:在某電商交易平台下,可能有買家查詢自己在某一時間段的訂單,也可能有賣家查詢自己在某一時間段的訂單,如果使用了分庫分表方案,則這兩個需求是難以滿足的。

是以,通用的解決方案是,在交易生成時生成一份按照買家分片的資料副本和一份按照賣家分片的資料副本,查詢時分别滿足之前的兩個需求,是以,查詢的資料和交易的資料可能是分别存儲的,并從不同的系統提供接口。

另外,在電商系統中,在一個交易訂單生成後,一般需要引用到訂單中交易的商品實體,如果簡單地引用,若商品的金額等資訊發生變化,則會導緻原訂單上的商品資訊也會發生變化,這樣買家會很疑惑。

是以,通用的解決方案是在交易系統中存儲商品的快照,在查詢交易時使用交易的快照,因為快照是個靜态資料,永遠都不會更新,是以解決了這個問題。

可見查詢的問題最好在單獨的系統中使用其他技術來解決,而不是在交易系統中實作各類查詢功能;當然,也可以通過對商品的變更實施版本化,在交易訂單中引用商品的版本資訊,在版本更新時保留商品的舊版本,這也是一種不錯的解決方案。

最後,關聯的表有可能不在同一資料庫中,是以基本不可能進行聯合查詢,需要借助大資料技術來實作,也就是上面所說(3)方法,即通過大資料技術統一聚合和處理關系型資料庫的資料,然後對外提供查詢操作。

3. 跨庫事務難以實作

要避免在一個事務中同時修改資料庫db0和資料庫db1中的表,因為操作起來很複雜,對效率也會有一定的影響。

4. 同組資料跨庫問題

要盡量把同一組資料放到同一台資料庫伺服器上,不但在某些場景下可以利用本地事務的強一緻性,還可以使這組資料自治。

以電商為例,我們的應用有兩個資料庫db0和db1,分庫分表後,按照id次元,将賣家A的交易資訊存放到db0中。當資料庫db1挂掉時,賣家A的交易資訊不受影響,依然可以正常使用。也就是說,要避免資料庫中的資料依賴另一資料庫中的資料

 本文來自于:

https://www.toutiao.com/a6545626478447428103/?tt_from=weixin&utm_campaign=client_share&article_category=stock&timestamp=1524215325&app=news_article&utm_source=weixin&iid=28070358035&utm_medium=toutiao_android&wxshare_count=1