當前位置:
首頁 > 最新 > 關於分庫分表,這有一套大而全的輕量級架構設計思路

關於分庫分表,這有一套大而全的輕量級架構設計思路

作者介紹

楊彪,螞蟻金服技術專家,《分散式服務架構:原理、設計與實戰》和《可伸縮服務架構:框架與中間件》作者。近10年互聯網和遊戲行業工作經驗,曾在酷我音樂盒、人人遊戲和掌趣科技等上市公司擔任核心研發職位,做過日活躍用戶量達千萬的項目,也做過多款月流水千萬以上的遊戲。

本文節選自《可伸縮服務架構:框架與中間件》一書,作者:李艷鵬、楊彪、李海亮、賈博岩、劉淏

這裡介紹設計分庫分表框架時應該考慮的設計要點,並給出相應的解決方案,為後面實現分庫分表框架dbsplit提供理論支撐。

一、整體的切分方式

簡單來說,數據的切分就是通過某種特定的條件,將我們存放在同一個資料庫中的數據分散存放到多個資料庫(主機)中,以達到分散單台設備負載的效果,即分庫分表。

數據的切分根據其切分規則的類型,可以分為如下兩種切分模式。

垂直(縱向)切分:把單一的表拆分成多個表,並分散到不同的資料庫(主機)上。

水平(橫向)切分:根據表中數據的邏輯關係,將同一個表中的數據按照某種條件拆分到多台資料庫(主機)上。

1. 垂直切分

一個資料庫由多個表構成,每個表對應不同的業務,垂直切分是指按照業務將表進行分類,將其分布到不同的資料庫上,這樣就將數據分擔到了不同的庫上(專庫專用)。

案例如下:

#有如下幾張表

--------------+--------------+------------------

用戶信息(User)+ 交易記錄(Pay)+ 商品(Commodity)|

--------------+--------------+------------------

針對以上案例,垂直切分就是根據每個表的不同業務進行切分,比如User表、Pay表和Commodity表,將每個表切分到不同的資料庫上。

垂直切分的優點如下:

拆分後業務清晰,拆分規則明確。

系統之間進行整合或擴展很容易。

按照成本、應用的等級、應用的類型等將表放到不同的機器上,便於管理。

便於實現動靜分離、冷熱分離的資料庫表的設計模式。

數據維護簡單。

垂直切分的缺點如下:

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

受每種業務的不同限制,存在單庫性能瓶頸,不易進行數據擴展和提升性能。

事務處理複雜。

垂直切分除了用於分解單庫單表的壓力,也用於實現冷熱分離,也就是根據數據的活躍度進行拆分,因為對擁有不同活躍度的數據的處理方式不同。

我們可將本來可以在同一個表中的內容人為地劃分為多個表。所謂「本來」,是指按照關係型資料庫第三範式的要求,應該在同一個表中,將其拆分開就叫作反范化(Denormalize)。

例如,對配置表的某些欄位很少進行修改時,將其放到一個查詢性能較高的資料庫硬體上;對配置表的其他欄位更新頻繁時,則將其放到另一個更新性能較高的資料庫硬體上。

這裡我們再舉一個例子:在微博系統的設計中,一個微博對象包括文章標題、作者、分類、創建時間等屬性欄位,這些欄位的變化頻率低,查詢次數多,叫作冷數據。而博客的瀏覽量、回複數、點贊數等類似的統計信息,或者別的變化頻率比較高的數據,叫作活躍數據或者熱數據。

我們把冷熱數據分開存放,就叫作冷熱分離,在MySQL的資料庫中,冷數據查詢較多,更新較少,適合用MyISAM引擎,而熱數據更新比較頻繁,適合使用InnoDB存儲引擎,這也是垂直拆分的一種。

我們推薦在設計資料庫表結構時,就考慮垂直拆分,根據冷熱分離、動靜分離的原則,再根據使用的存儲引擎的特點,對冷數據可以使用MyISAM,能更好地進行數據查詢;對熱數據可以使用InnoDB,有更快的更新速度,這樣能夠有效提升性能。

其次,對讀多寫少的冷數據可配置更多的從庫來化解大量查詢請求的壓力;對於熱數據,可以使用多個主庫構建分庫分表的結構,請參考下面關於水平切分的內容,後續的三四五章提供了不同的分庫分表的具體實施方案。

注意,對於一些特殊的活躍數據或者熱點數據,也可以考慮使用Memcache、Redis之類的緩存,等累計到一定的量後再更新資料庫,例如,在記錄微博點贊數量的業務中,點贊數量被存儲在緩存中,每增加1000個點贊,才寫一次數據。

2. 水平切分

與垂直切分對比,水平切分不是將表進行分類,而是將其按照某個欄位的某種規則分散到多個庫中,在每個表中包含一部分數據,所有表加起來就是全量的數據。

簡單來說,我們可以將對數據的水平切分理解為按照數據行進行切分,就是將表中的某些行切分到一個資料庫表中,而將其他行切分到其他資料庫表中。

這種切分方式根據單表的數據量的規模來切分,保證單表的容量不會太大,從而保證了單表的查詢等處理能力,例如將用戶的信息表拆分成User1、User2等,表結構是完全一樣的。我們通常根據某些特定的規則來劃分表,比如根據用戶的ID來取模劃分。

例如,在博客系統中,當讀取博客的量很大時,就應該採取水平切分來減少每個單表的壓力,並提升性能。

以微博表為例,當同時有100萬個用戶在瀏覽時,如果是單表,則單表會進行100萬次請求,假如是單庫,資料庫就會承受100萬次的請求壓力;假如將其分為100個表,並且分布在10個資料庫中,每個表進行1萬次請求,則每個資料庫會承受10萬次的請求壓力,雖然這不可能絕對平均,但是可以說明問題,這樣壓力就減少了很多,並且是成倍減少的。

水平切分的優點如下:

單庫單表的數據保持在一定的量級,有助於性能的提高。

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

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

水平切分的缺點如下:

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

拆分規則難以抽象。

分片事務的一致性難以解決。

數據擴容的難度和維護量極大。

綜上所述,垂直切分和水平切分的共同點如下:

存在分散式事務的問題。

存在跨節點Join的問題。

存在跨節點合併排序、分頁的問題。

存在多數據源管理的問題。

在了解這兩種切分方式的特點後,我們就可以根據自己的業務需求來選擇,通常會同時使用這兩種切分方式,垂直切分更偏向於業務拆分的過程,在技術上我們更關注水平切分的方案。

二、水平切分方式的路由過程和分片維度

這裡講解水平切分的路由過程和分片維度。

1. 水平切分的路由過程

我們在設計表時需要確定對錶按照什麼樣的規則進行分庫分表。例如,當有新用戶時,程序得確定將此用戶的信息添加到哪個表中;同理,在登錄時我們需要通過用戶的賬號找到資料庫中對應的記錄,所有這些都需要按照某一規則進行路由請求,因為請求所需要的數據分布在不同的分片表中。

針對輸入的請求,通過分庫分表規則查找到對應的表和庫的過程叫作路由。例如,分庫分表的規則是user_id % 4,當用戶新註冊了一個賬號時,假設用戶的ID是123,我們就可以通過123 % 4 = 3確定此賬號應該被保存在User3表中。當ID為123的用戶登錄時,我們可通過123 % 4 = 3計算後,確定其被記錄在User3中。

2. 水平切分的分片維度

對數據切片有不同的切片維度,可以參考Mycat提供的切片方式(見本書3.4節),這裡只介紹兩種最常用的切片維度。

1)按照哈希切片

對數據的某個欄位求哈希,再除以分片總數後取模,取模後相同的數據為一個分片,這樣的將數據分成多個分片的方法叫作哈希分片。

按照哈希分片常常應用於數據沒有時效性的情況,比如所有數據無論是在什麼時間產生的,都需要進行處理或者查詢,例如支付行業的客戶要求可以對至少1年以內的交易進行查詢和退款,那麼1年以內的所有交易數據都必須停留在交易資料庫中,否則就無法查詢和退款。

如果這家公司在一年內能做10億條交易,假設每個資料庫分片能夠容納5000萬條數據,則至少需要20個表才能容納10億條交易。在路由時,我們根據交易ID進行哈希取模來找到數據屬於哪個分片,因此,在設計系統時要充分考慮如何設計資料庫的分庫分表的路由規則。

這種切片方式的好處是數據切片比較均勻,對數據壓力分散的效果較好,缺點是數據分散後,對於查詢需求需要進行聚合處理。

2)按照時間切片

與按照哈希切片不同,這種方式是按照時間的範圍將數據分布到不同的分片上的,例如,我們可以將交易數據按照月進行切片,或者按照季度進行切片,由交易數據的多少來決定按照什麼樣的時間周期對數據進行切片。

這種切片方式適用於有明顯時間特點的數據,例如,距離現在1個季度的數據訪問頻繁,距離現在兩個季度的數據可能沒有更新,距離現在3個季度的數據沒有查詢需求。

針對這種情況,可以通過按照時間進行切片,針對不同的訪問頻率使用不同檔次的硬體資源來節省成本:假設距離現在1個季度的數據訪問頻率最高,我們就用更好的硬體來運行這個分片;假設距離現在3個季度的數據沒有任何訪問需求,我們就可以將其整體歸檔,以方便DBA操作。

在實際的生產實踐中,按照哈希切片和按照時間切片都是常用的分庫分表方式,並被廣泛使用,有時可以結合使用這兩種方式,例如:對交易數據先按照季度進行切片,然後對於某一季度的數據按照主鍵哈希進行切片。

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

本節講解分片後的事務處理機制。

1. 分散式事務

由於我們將單表的數據切片後存儲在多個資料庫甚至多個資料庫實例中,所以依靠資料庫本身的事務機制不能滿足所有場景的需要。

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

這裡只介紹實現分散式操作一致性的幾個主流思路,保證分散式事務一致性的具體方法請參考《分散式服務架構:原理、設計與實戰》中第2章的內容。

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

1)兩階段提交協議

兩階段提交協議將分散式事務分為兩個階段,一個是準備階段,一個是提交階段,兩個階段都由事務管理器發起。

基於兩階段提交協議,事務管理器能夠最大限度地保證跨資料庫操作的事務的原子性,是分散式系統環境下最嚴格的事務實現方法。符合J2EE規範的AppServer(例如:Websphere、Weblogic、Jboss等)對關係型資料庫數據源和消息隊列都實現了兩階段提交協議,只需在使用時配置即可。如圖3-9所示。

圖3-9

但是,兩階段提交協議也帶來了性能方面的問題,難於進行水平伸縮,因為在提交事務的過程中,事務管理器需要和每個參與者進行準備和提交的操作的協調,在準備階段鎖定資源,在提交階段消費資源。

但是由於參與者較多,鎖定資源和消費資源之間的時間差被拉長,導致響應速度較慢,在此期間產生死鎖或者不確定結果的可能性較大。因此,在互聯網行業里,為了追求性能的提升,很少使用兩階段提交協議。

另外,由於兩階段提交協議是阻塞協議,在極端情況下不能快速響應請求方,因此有人提出了三階段提交協議,解決了兩階段提交協議的阻塞問題,但仍然需要事務管理器在參與者之間協調,才能完成一個分散式事務。

2)最大努力保證模式

這是一種非常通用的保證分散式一致性的模式,很多開發人員一直在使用,但是並未意識到這是一種模式。最大努力保證模式適用於對一致性要求並不十分嚴格但是對性能要求較高的場景。

具體的實現方法是,在更新多個資源時,將多個資源的提交盡量延後到最後一刻處理,這樣的話,如果業務流程出現問題,則所有的資源更新都可以回滾,事務仍然保持一致。

唯一可能出現問題的情況是在提交多個資源時發生了系統問題,比如網路問題等,但是這種情況是非常罕見的,一旦出現這種情況,就需要進行實時補償,將已提交的事務進行回滾,這和我們常說的TCC模式有些類似。

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

(1)開始消息事務。

(2)開始資料庫事務。

(3)接收消息。

(4)更新資料庫。

(5)提交資料庫事務。

(6)提交消息事務。

這時,從第1步到第4步並不是很關鍵,關鍵的是第5步和第6步,需要將其放在最後一起提交,盡最大努力保證前面的業務處理的一致性。

到了第5步和第6步,業務邏輯處理完成,這時只可能發生系統錯誤,如果第5步失敗,則可以將消息隊列和資料庫事務全部回滾,保持一致。如果第5步成功,第6步遇到了網路超時等問題,則這是唯一可能產生問題的情況。

在這種情況下,消息的消費過程並沒有被提交到消息隊列,消息隊列可能會重新發送消息給其他消息處理服務,這會導致消息被重複消費,但是可以通過冪等處理來保證消除重複消息帶來的影響。

當然,在使用這種模式時,我們要充分考慮每個資源的提交順序。我們在生產實踐中遇到的一種反模式,就是在資料庫事務中嵌套遠程調用,而且遠程調用是耗時任務,導致資料庫事務被拉長,最後拖垮資料庫。

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

3)事務補償機制

顯然,在對性能要求很高的場景中,兩階段提交協議並不是一種好方案,最大努力保證模式也會使多個分散式操作互相嵌套,有可能互相影響。這裡,我們給出事務補償機制,其性能很高,並且能夠盡最大可能地保證事務的最終一致性。

在資料庫分庫分表後,如果涉及的多個更新操作在某一個資料庫範圍內完成,則可以使用資料庫內的本地事務保證一致性;對於跨庫的多個操作,可通過補償和重試,使其在一定的時間窗口內完成操作,這樣就可以實現事務的最終一致性,突破事務遇到問題就滾回的傳統思路。

如果採用事務補償機制,則在遇到問題時,我們需要記錄遇到問題的環境、信息、步驟、狀態等,後續通過重試機制使其達到最終一致性,詳細內容可以參考《分散式服務架構:原理、設計與實戰》第2章,徹底理解ACID原理、CAP理論、BASE原理、最終一致性模式等內容。

2. 事務路由

無論使用上面哪種方法實現分散式事務,都需要對分庫分表的多個數據源路由事務,一般通過對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來改變這些屬性,這些操作一般都會被封裝在我們使用的框架中。本書3.6節介紹的開源資料庫分庫分表框架dbsplit默認使用的就是這種模式。

2)可編程事務路由

我們在應用中通常採用Spring的聲明式的事務來管理資料庫事務,在分庫分表時,事務處理是個問題,在一個需要開啟事務的方法中,需要動態地確定開啟哪個資料庫實例的事務,也就是說在每個開啟事務的方法調用前就必須確定開啟哪個數據源的事務。下面使用偽代碼來說明如何實現一個可編程事務路由的小框架。

首先,通過Spring配置文件展示可編程事務小框架是怎麼使用的:

class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger">

class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger">

class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger">

我們看到shardingTransactionManager的類型是ShardingTransactionManager,這個類型是我們開發的一個組合的事務管理器,這個事務管理器聚合了所有分片資料庫的事務管理器對象,然後根據某個標記來路由到不同的事務管理器中,這些事務管理器用來控制各個分片的數據源的事務。

這裡的標記是什麼呢?我們在調用方法時,會提前把分片的標記放進ThreadLocal中,然後在ShardingTransactionManager的getTransaction方法被調用時,取得ThreadLocal中存的標記,最後根據標記來判斷使用哪個分片資料庫的事務管理器對象。

為了通過標記路由到不同的事務管理器,我們設計了一個專門的ShardingContextHolder類,在該類的內部使用了一個ThreadLocal類來指定分片資料庫的關鍵字,在ShardingTransaction Manager中通過取得這個標記來選擇具體的分片資料庫的事務管理器對象。

因此,這個類提供了setShard和getShard的方法,setShard用於使用者編程指定使用哪個分片資料庫的事務管理器,而getShard用於ShardingTransactionManager獲取標記並取得分片資料庫的事務管理器對象。相關代碼如下:

public class ShardingContextHolder {

private static final ThreadLocal shardHolder = new ThreadLocal();

public static void setShard(T shard) {

Validate.notNull(shard, "請指定某個分片資料庫!");

shardHolder.set(shard);

}

public static T getShard() {

return (T) shardHolder.get();

}

}

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

public class ShardingTransactionManager implements PlatformTransactionManager {

private Map proxyTransactionManagers =

new HashMap();

protected PlatformTransactionManager getTargetTransactionManager() {

Object shard = ShardingContextHolder.getShard();

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

return targetTransactionManagers.get(shard);

}

public void setProxyTransactionManagers(Map 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()

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface TransactionHint {

String table() default "";

String keyPath() default "";

}

可以看到,TransactionHint包含了兩個屬性,第1個屬性table指定這次操作涉及分片的資料庫表,第2個屬性指定這次操作根據哪個參數的哪個欄位進行分片路由。該實例通過table指定了INVOICE表,並通過keyPath指定了使用第1個參數的欄位accountId作為路由的關鍵字。

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

下面是在Spring環境中配置的INVOICE表和INVOICE_ITEM表的具體信息,我們看到它們一共使用了兩個資料庫實例,每個實例有兩個庫,每個庫有8個表,使用水平下標策略。配置如下:

init-method="init">

然後,在方法被調用時通過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

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 {

public static final ThreadContextHolder INST = new ThreadContextHolder();

private ThreadLocal contextHolder = new ThreadLocal();

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);

}

}

本書3.6節介紹的開源資料庫分庫分表框架dbsplit是一個分庫分表的簡單示例實現,在筆者所工作的公司內部有內部版本,在內部版本中實現了聲明式事務路由,但是這部分功能並沒有開源到dbsplit項目,原因是有些與業務結合的邏輯無法分離。如果感興趣,則可以加入我們的開源項目開發中。

四、讀寫分離

在實際應用中的絕大多數情況下讀操作遠大於寫操作。MySQL提供了讀寫分離的機制,所有寫操作必須對應到主庫(Master),讀操作可以在主庫(Master)和從庫(Slave)機器上進行。

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

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

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

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

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

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

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

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

五、分庫分表引起的問題

分庫分表按照某種規則將數據的集合拆分成多個子集合,數據的完整性被打破,因此在某種場景下會產生多種問題。

1. 擴容與遷移

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

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

Step1:按照新舊分片規則,對新舊資料庫進行雙寫。

Step2:將雙寫前按照舊分片規則寫入的歷史數據,根據新分片規則遷移寫入新的資料庫。

Step3:將按照舊的分片規則查詢改為按照新的分片規則查詢。

Step4:將雙寫資料庫邏輯從代碼中下線,只按照新的分片規則寫入數據。

Step5:刪除按照舊分片規則寫入的歷史數據。

這裡,在第2步遷移歷史數據時,由於數據量很大,通常會導致不一致,因此,先清洗舊的數據,洗完後再遷移到新規則的新資料庫下,再做全量對比,對比後評估在遷移的過程中是否有數據的更新,如果有的話就再清洗、遷移,最後以對比沒有差距為準。

如果是金融交易數據,則最好將動靜數據分離,隨著時間的流逝,某個時間點之前的數據是不會被更新的,我們就可以拉長雙寫的時間窗口,這樣在足夠長的時間流逝後,只需遷移那些不再被更新的歷史數據即可,就不會在遷移的過程中由於歷史數據被更新而導致代理不一致。

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

在遷移的過程中,數據的更新會導致不一致,可以在線上記錄遷移過程中的更新操作的日誌,遷移後根據更新日誌與歷史數據共同決定數據的最新狀態,來達到遷移數據的最終一致性。

2. 分庫分表維度導致的查詢問題

在分庫分表以後,如果查詢的標準是分片的主鍵,則可以通過分片規則再次路由並查詢;但是對於其他主鍵的查詢、範圍查詢、關聯查詢、查詢結果排序等,並不是按照分庫分表維度來查詢的。

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

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

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

在多個分片表查詢後合併數據集,這種方式的效率很低。

記錄兩份數據,一份按照買家緯度分表,一份按照商品維度分表。

通過搜索引擎解決,但如果實時性要求很高,就需要實現實時搜索。

實際上,在高並發的服務平台下,交易系統是專門做交易的,因為交易是核心服務,SLA的級別比較高,所以需要和查詢系統分離,查詢一般通過其他系統進行,數據也可能是冗餘存儲的。

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

因此,通用的解決方案是,在交易生成時生成一份按照買家分片的數據副本和一份按照賣家分片的數據副本,查詢時分別滿足之前的兩個需求,因此,查詢的數據和交易的數據可能是分別存儲的,並從不同的系統提供介面。

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

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

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

最後,關聯的表有可能不在同一資料庫中,所以基本不可能進行聯合查詢,需要藉助大數據技術來實現,也就是上面所說的第3種方法,即通過大數據技術統一聚合和處理關係型資料庫的數據,然後對外提供查詢操作,請參考第5章的內容。

通過大數據方式來提供聚合查詢的方式如圖3-10所示。

圖3-10

3. 跨庫事務難以實現

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

4. 同組數據跨庫問題

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

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

新書搶先看

本文節選自《可伸縮服務架構:框架與中間件》一書,可登錄網址:https://item.jd.com/12308233.html 購入。更多新書精華內容也將由DBAplus社群陸續呈現。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 DBAplus社群 的精彩文章:

打贏數據安全攻堅戰,從Hadoop-security治理說起!
同為分散式緩存,為何Redis更勝一籌?

TAG:DBAplus社群 |