偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

關(guān)于分庫(kù)分表,這里有一套大而全的輕量級(jí)架構(gòu)設(shè)計(jì)思路

數(shù)據(jù)庫(kù)
這里介紹設(shè)計(jì)分庫(kù)分表框架時(shí)應(yīng)該考慮的設(shè)計(jì)要點(diǎn),并給出相應(yīng)的解決方案,為后面實(shí)現(xiàn)分庫(kù)分表框架dbsplit提供理論支撐。

本文節(jié)選自《可伸縮服務(wù)架構(gòu):框架與中間件》一書(shū),作者:李艷鵬、楊彪、李海亮、賈博巖、劉淏

這里介紹設(shè)計(jì)分庫(kù)分表框架時(shí)應(yīng)該考慮的設(shè)計(jì)要點(diǎn),并給出相應(yīng)的解決方案,為后面實(shí)現(xiàn)分庫(kù)分表框架dbsplit提供理論支撐。

一、整體的切分方式

簡(jiǎn)單來(lái)說(shuō),數(shù)據(jù)的切分就是通過(guò)某種特定的條件,將我們存放在同一個(gè)數(shù)據(jù)庫(kù)中的數(shù)據(jù)分散存放到多個(gè)數(shù)據(jù)庫(kù)(主機(jī))中,以達(dá)到分散單臺(tái)設(shè)備負(fù)載的效果,即分庫(kù)分表。

數(shù)據(jù)的切分根據(jù)其切分規(guī)則的類型,可以分為如下兩種切分模式。

  • 垂直(縱向)切分:把單一的表拆分成多個(gè)表,并分散到不同的數(shù)據(jù)庫(kù)(主機(jī))上。

  • 水平(橫向)切分:根據(jù)表中數(shù)據(jù)的邏輯關(guān)系,將同一個(gè)表中的數(shù)據(jù)按照某種條件拆分到多臺(tái)數(shù)據(jù)庫(kù)(主機(jī))上。

1. 垂直切分

一個(gè)數(shù)據(jù)庫(kù)由多個(gè)表構(gòu)成,每個(gè)表對(duì)應(yīng)不同的業(yè)務(wù),垂直切分是指按照業(yè)務(wù)將表進(jìn)行分類,將其分布到不同的數(shù)據(jù)庫(kù)上,這樣就將數(shù)據(jù)分擔(dān)到了不同的庫(kù)上(專庫(kù)專用)。

案例如下:

#有如下幾張表

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

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

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

針對(duì)以上案例,垂直切分就是根據(jù)每個(gè)表的不同業(yè)務(wù)進(jìn)行切分,比如User表、Pay表和Commodity表,將每個(gè)表切分到不同的數(shù)據(jù)庫(kù)上。

垂直切分的優(yōu)點(diǎn)如下:

  • 拆分后業(yè)務(wù)清晰,拆分規(guī)則明確。

  • 系統(tǒng)之間進(jìn)行整合或擴(kuò)展很容易。

  • 按照成本、應(yīng)用的等級(jí)、應(yīng)用的類型等將表放到不同的機(jī)器上,便于管理。

  • 便于實(shí)現(xiàn)動(dòng)靜分離、冷熱分離的數(shù)據(jù)庫(kù)表的設(shè)計(jì)模式。

  • 數(shù)據(jù)維護(hù)簡(jiǎn)單。

垂直切分的缺點(diǎn)如下:

  • 部分業(yè)務(wù)表無(wú)法關(guān)聯(lián)(Join),只能通過(guò)接口方式解決,提高了系統(tǒng)的復(fù)雜度。

  • 受每種業(yè)務(wù)的不同限制,存在單庫(kù)性能瓶頸,不易進(jìn)行數(shù)據(jù)擴(kuò)展和提升性能。

  • 事務(wù)處理復(fù)雜。

垂直切分除了用于分解單庫(kù)單表的壓力,也用于實(shí)現(xiàn)冷熱分離,也就是根據(jù)數(shù)據(jù)的活躍度進(jìn)行拆分,因?yàn)閷?duì)擁有不同活躍度的數(shù)據(jù)的處理方式不同。

我們可將本來(lái)可以在同一個(gè)表中的內(nèi)容人為地劃分為多個(gè)表。所謂“本來(lái)”,是指按照關(guān)系型數(shù)據(jù)庫(kù)第三范式的要求,應(yīng)該在同一個(gè)表中,將其拆分開(kāi)就叫作反范化(Denormalize)。

例如,對(duì)配置表的某些字段很少進(jìn)行修改時(shí),將其放到一個(gè)查詢性能較高的數(shù)據(jù)庫(kù)硬件上;對(duì)配置表的其他字段更新頻繁時(shí),則將其放到另一個(gè)更新性能較高的數(shù)據(jù)庫(kù)硬件上。

這里我們?cè)倥e一個(gè)例子:在微博系統(tǒng)的設(shè)計(jì)中,一個(gè)微博對(duì)象包括文章標(biāo)題、作者、分類、創(chuàng)建時(shí)間等屬性字段,這些字段的變化頻率低,查詢次數(shù)多,叫作冷數(shù)據(jù)。而博客的瀏覽量、回復(fù)數(shù)、點(diǎn)贊數(shù)等類似的統(tǒng)計(jì)信息,或者別的變化頻率比較高的數(shù)據(jù),叫作活躍數(shù)據(jù)或者熱數(shù)據(jù)。

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

我們推薦在設(shè)計(jì)數(shù)據(jù)庫(kù)表結(jié)構(gòu)時(shí),就考慮垂直拆分,根據(jù)冷熱分離、動(dòng)靜分離的原則,再根據(jù)使用的存儲(chǔ)引擎的特點(diǎn),對(duì)冷數(shù)據(jù)可以使用MyISAM,能更好地進(jìn)行數(shù)據(jù)查詢;對(duì)熱數(shù)據(jù)可以使用InnoDB,有更快的更新速度,這樣能夠有效提升性能。

其次,對(duì)讀多寫(xiě)少的冷數(shù)據(jù)可配置更多的從庫(kù)來(lái)化解大量查詢請(qǐng)求的壓力;對(duì)于熱數(shù)據(jù),可以使用多個(gè)主庫(kù)構(gòu)建分庫(kù)分表的結(jié)構(gòu),請(qǐng)參考下面關(guān)于水平切分的內(nèi)容,后續(xù)的三四五章提供了不同的分庫(kù)分表的具體實(shí)施方案。

注意,對(duì)于一些特殊的活躍數(shù)據(jù)或者熱點(diǎn)數(shù)據(jù),也可以考慮使用Memcache、Redis之類的緩存,等累計(jì)到一定的量后再更新數(shù)據(jù)庫(kù),例如,在記錄微博點(diǎn)贊數(shù)量的業(yè)務(wù)中,點(diǎn)贊數(shù)量被存儲(chǔ)在緩存中,每增加1000個(gè)點(diǎn)贊,才寫(xiě)一次數(shù)據(jù)。

2. 水平切分

與垂直切分對(duì)比,水平切分不是將表進(jìn)行分類,而是將其按照某個(gè)字段的某種規(guī)則分散到多個(gè)庫(kù)中,在每個(gè)表中包含一部分?jǐn)?shù)據(jù),所有表加起來(lái)就是全量的數(shù)據(jù)。

簡(jiǎn)單來(lái)說(shuō),我們可以將對(duì)數(shù)據(jù)的水平切分理解為按照數(shù)據(jù)行進(jìn)行切分,就是將表中的某些行切分到一個(gè)數(shù)據(jù)庫(kù)表中,而將其他行切分到其他數(shù)據(jù)庫(kù)表中。

這種切分方式根據(jù)單表的數(shù)據(jù)量的規(guī)模來(lái)切分,保證單表的容量不會(huì)太大,從而保證了單表的查詢等處理能力,例如將用戶的信息表拆分成User1、User2等,表結(jié)構(gòu)是完全一樣的。我們通常根據(jù)某些特定的規(guī)則來(lái)劃分表,比如根據(jù)用戶的ID來(lái)取模劃分。

例如,在博客系統(tǒng)中,當(dāng)讀取博客的量很大時(shí),就應(yīng)該采取水平切分來(lái)減少每個(gè)單表的壓力,并提升性能。

以微博表為例,當(dāng)同時(shí)有100萬(wàn)個(gè)用戶在瀏覽時(shí),如果是單表,則單表會(huì)進(jìn)行100萬(wàn)次請(qǐng)求,假如是單庫(kù),數(shù)據(jù)庫(kù)就會(huì)承受100萬(wàn)次的請(qǐng)求壓力;假如將其分為100個(gè)表,并且分布在10個(gè)數(shù)據(jù)庫(kù)中,每個(gè)表進(jìn)行1萬(wàn)次請(qǐng)求,則每個(gè)數(shù)據(jù)庫(kù)會(huì)承受10萬(wàn)次的請(qǐng)求壓力,雖然這不可能絕對(duì)平均,但是可以說(shuō)明問(wèn)題,這樣壓力就減少了很多,并且是成倍減少的。

水平切分的優(yōu)點(diǎn)如下:

  • 單庫(kù)單表的數(shù)據(jù)保持在一定的量級(jí),有助于性能的提高。

  • 切分的表的結(jié)構(gòu)相同,應(yīng)用層改造較少,只需要增加路由規(guī)則即可。

  • 提高了系統(tǒng)的穩(wěn)定性和負(fù)載能力。

水平切分的缺點(diǎn)如下:

  • 切分后,數(shù)據(jù)是分散的,很難利用數(shù)據(jù)庫(kù)的Join操作,跨庫(kù)Join性能較差。

  • 拆分規(guī)則難以抽象。

  • 分片事務(wù)的一致性難以解決。

  • 數(shù)據(jù)擴(kuò)容的難度和維護(hù)量極大。

綜上所述,垂直切分和水平切分的共同點(diǎn)如下:

  • 存在分布式事務(wù)的問(wèn)題。

  • 存在跨節(jié)點(diǎn)Join的問(wèn)題。

  • 存在跨節(jié)點(diǎn)合并排序、分頁(yè)的問(wèn)題。

  • 存在多數(shù)據(jù)源管理的問(wèn)題。

在了解這兩種切分方式的特點(diǎn)后,我們就可以根據(jù)自己的業(yè)務(wù)需求來(lái)選擇,通常會(huì)同時(shí)使用這兩種切分方式,垂直切分更偏向于業(yè)務(wù)拆分的過(guò)程,在技術(shù)上我們更關(guān)注水平切分的方案。

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

這里講解水平切分的路由過(guò)程和分片維度。

1. 水平切分的路由過(guò)程

我們?cè)谠O(shè)計(jì)表時(shí)需要確定對(duì)表按照什么樣的規(guī)則進(jìn)行分庫(kù)分表。例如,當(dāng)有新用戶時(shí),程序得確定將此用戶的信息添加到哪個(gè)表中;同理,在登錄時(shí)我們需要通過(guò)用戶的賬號(hào)找到數(shù)據(jù)庫(kù)中對(duì)應(yīng)的記錄,所有這些都需要按照某一規(guī)則進(jìn)行路由請(qǐng)求,因?yàn)檎?qǐng)求所需要的數(shù)據(jù)分布在不同的分片表中。

針對(duì)輸入的請(qǐng)求,通過(guò)分庫(kù)分表規(guī)則查找到對(duì)應(yīng)的表和庫(kù)的過(guò)程叫作路由。例如,分庫(kù)分表的規(guī)則是user_id % 4,當(dāng)用戶新注冊(cè)了一個(gè)賬號(hào)時(shí),假設(shè)用戶的ID是123,我們就可以通過(guò)123 % 4 = 3確定此賬號(hào)應(yīng)該被保存在User3表中。當(dāng)ID為123的用戶登錄時(shí),我們可通過(guò)123 % 4 = 3計(jì)算后,確定其被記錄在User3中。

2. 水平切分的分片維度

對(duì)數(shù)據(jù)切片有不同的切片維度,可以參考Mycat提供的切片方式(見(jiàn)本書(shū)3.4節(jié)),這里只介紹兩種最常用的切片維度。

1)按照哈希切片

對(duì)數(shù)據(jù)的某個(gè)字段求哈希,再除以分片總數(shù)后取模,取模后相同的數(shù)據(jù)為一個(gè)分片,這樣的將數(shù)據(jù)分成多個(gè)分片的方法叫作哈希分片。

按照哈希分片常常應(yīng)用于數(shù)據(jù)沒(méi)有時(shí)效性的情況,比如所有數(shù)據(jù)無(wú)論是在什么時(shí)間產(chǎn)生的,都需要進(jìn)行處理或者查詢,例如支付行業(yè)的客戶要求可以對(duì)至少1年以內(nèi)的交易進(jìn)行查詢和退款,那么1年以內(nèi)的所有交易數(shù)據(jù)都必須停留在交易數(shù)據(jù)庫(kù)中,否則就無(wú)法查詢和退款。

如果這家公司在一年內(nèi)能做10億條交易,假設(shè)每個(gè)數(shù)據(jù)庫(kù)分片能夠容納5000萬(wàn)條數(shù)據(jù),則至少需要20個(gè)表才能容納10億條交易。在路由時(shí),我們根據(jù)交易ID進(jìn)行哈希取模來(lái)找到數(shù)據(jù)屬于哪個(gè)分片,因此,在設(shè)計(jì)系統(tǒng)時(shí)要充分考慮如何設(shè)計(jì)數(shù)據(jù)庫(kù)的分庫(kù)分表的路由規(guī)則。

這種切片方式的好處是數(shù)據(jù)切片比較均勻,對(duì)數(shù)據(jù)壓力分散的效果較好,缺點(diǎn)是數(shù)據(jù)分散后,對(duì)于查詢需求需要進(jìn)行聚合處理。

2)按照時(shí)間切片

與按照哈希切片不同,這種方式是按照時(shí)間的范圍將數(shù)據(jù)分布到不同的分片上的,例如,我們可以將交易數(shù)據(jù)按照月進(jìn)行切片,或者按照季度進(jìn)行切片,由交易數(shù)據(jù)的多少來(lái)決定按照什么樣的時(shí)間周期對(duì)數(shù)據(jù)進(jìn)行切片。

這種切片方式適用于有明顯時(shí)間特點(diǎn)的數(shù)據(jù),例如,距離現(xiàn)在1個(gè)季度的數(shù)據(jù)訪問(wèn)頻繁,距離現(xiàn)在兩個(gè)季度的數(shù)據(jù)可能沒(méi)有更新,距離現(xiàn)在3個(gè)季度的數(shù)據(jù)沒(méi)有查詢需求。

針對(duì)這種情況,可以通過(guò)按照時(shí)間進(jìn)行切片,針對(duì)不同的訪問(wèn)頻率使用不同檔次的硬件資源來(lái)節(jié)省成本:假設(shè)距離現(xiàn)在1個(gè)季度的數(shù)據(jù)訪問(wèn)頻率***,我們就用更好的硬件來(lái)運(yùn)行這個(gè)分片;假設(shè)距離現(xiàn)在3個(gè)季度的數(shù)據(jù)沒(méi)有任何訪問(wèn)需求,我們就可以將其整體歸檔,以方便DBA操作。

在實(shí)際的生產(chǎn)實(shí)踐中,按照哈希切片和按照時(shí)間切片都是常用的分庫(kù)分表方式,并被廣泛使用,有時(shí)可以結(jié)合使用這兩種方式,例如:對(duì)交易數(shù)據(jù)先按照季度進(jìn)行切片,然后對(duì)于某一季度的數(shù)據(jù)按照主鍵哈希進(jìn)行切片。

三、分片后的事務(wù)處理機(jī)制

本節(jié)講解分片后的事務(wù)處理機(jī)制。

1. 分布式事務(wù)

由于我們將單表的數(shù)據(jù)切片后存儲(chǔ)在多個(gè)數(shù)據(jù)庫(kù)甚至多個(gè)數(shù)據(jù)庫(kù)實(shí)例中,所以依靠數(shù)據(jù)庫(kù)本身的事務(wù)機(jī)制不能滿足所有場(chǎng)景的需要。

但是,我們推薦在一個(gè)數(shù)據(jù)庫(kù)實(shí)例中的操作盡可能使用本地事務(wù)來(lái)保證一致性,跨數(shù)據(jù)庫(kù)實(shí)例的一系列更新操作需要根據(jù)事務(wù)路由在不同的數(shù)據(jù)源中完成,各個(gè)數(shù)據(jù)源之間的更新操作需要通過(guò)分布式事務(wù)處理。

這里只介紹實(shí)現(xiàn)分布式操作一致性的幾個(gè)主流思路,保證分布式事務(wù)一致性的具體方法請(qǐng)參考《分布式服務(wù)架構(gòu):原理、設(shè)計(jì)與實(shí)戰(zhàn)》中第2章的內(nèi)容。

主流的分布式事務(wù)解決方案有三種:兩階段提交協(xié)議、***努力保證模式和事務(wù)補(bǔ)償機(jī)制。

1)兩階段提交協(xié)議

兩階段提交協(xié)議將分布式事務(wù)分為兩個(gè)階段,一個(gè)是準(zhǔn)備階段,一個(gè)是提交階段,兩個(gè)階段都由事務(wù)管理器發(fā)起。

基于兩階段提交協(xié)議,事務(wù)管理器能夠***限度地保證跨數(shù)據(jù)庫(kù)操作的事務(wù)的原子性,是分布式系統(tǒng)環(huán)境下最嚴(yán)格的事務(wù)實(shí)現(xiàn)方法。符合J2EE規(guī)范的AppServer(例如:Websphere、Weblogic、Jboss等)對(duì)關(guān)系型數(shù)據(jù)庫(kù)數(shù)據(jù)源和消息隊(duì)列都實(shí)現(xiàn)了兩階段提交協(xié)議,只需在使用時(shí)配置即可。如圖3-9所示。

關(guān)于分庫(kù)分表,這有一套大而全的輕量級(jí)架構(gòu)設(shè)計(jì)思路

圖3-9

但是,兩階段提交協(xié)議也帶來(lái)了性能方面的問(wèn)題,難于進(jìn)行水平伸縮,因?yàn)樵谔峤皇聞?wù)的過(guò)程中,事務(wù)管理器需要和每個(gè)參與者進(jìn)行準(zhǔn)備和提交的操作的協(xié)調(diào),在準(zhǔn)備階段鎖定資源,在提交階段消費(fèi)資源。

但是由于參與者較多,鎖定資源和消費(fèi)資源之間的時(shí)間差被拉長(zhǎng),導(dǎo)致響應(yīng)速度較慢,在此期間產(chǎn)生死鎖或者不確定結(jié)果的可能性較大。因此,在互聯(lián)網(wǎng)行業(yè)里,為了追求性能的提升,很少使用兩階段提交協(xié)議。

另外,由于兩階段提交協(xié)議是阻塞協(xié)議,在極端情況下不能快速響應(yīng)請(qǐng)求方,因此有人提出了三階段提交協(xié)議,解決了兩階段提交協(xié)議的阻塞問(wèn)題,但仍然需要事務(wù)管理器在參與者之間協(xié)調(diào),才能完成一個(gè)分布式事務(wù)。

2)***努力保證模式

這是一種非常通用的保證分布式一致性的模式,很多開(kāi)發(fā)人員一直在使用,但是并未意識(shí)到這是一種模式。***努力保證模式適用于對(duì)一致性要求并不十分嚴(yán)格但是對(duì)性能要求較高的場(chǎng)景。

具體的實(shí)現(xiàn)方法是,在更新多個(gè)資源時(shí),將多個(gè)資源的提交盡量延后到***一刻處理,這樣的話,如果業(yè)務(wù)流程出現(xiàn)問(wèn)題,則所有的資源更新都可以回滾,事務(wù)仍然保持一致。

唯一可能出現(xiàn)問(wèn)題的情況是在提交多個(gè)資源時(shí)發(fā)生了系統(tǒng)問(wèn)題,比如網(wǎng)絡(luò)問(wèn)題等,但是這種情況是非常罕見(jiàn)的,一旦出現(xiàn)這種情況,就需要進(jìn)行實(shí)時(shí)補(bǔ)償,將已提交的事務(wù)進(jìn)行回滾,這和我們常說(shuō)的TCC模式有些類似。

下面是使用***努力保證模式的一個(gè)樣例,在該樣例中涉及兩個(gè)操作,一個(gè)是從消息隊(duì)列消費(fèi)消息,一個(gè)是更新數(shù)據(jù)庫(kù),需要保證分布式的一致性。

(1)開(kāi)始消息事務(wù)。

(2)開(kāi)始數(shù)據(jù)庫(kù)事務(wù)。

(3)接收消息。

(4)更新數(shù)據(jù)庫(kù)。

(5)提交數(shù)據(jù)庫(kù)事務(wù)。

(6)提交消息事務(wù)。

這時(shí),從第1步到第4步并不是很關(guān)鍵,關(guān)鍵的是第5步和第6步,需要將其放在***一起提交,盡***努力保證前面的業(yè)務(wù)處理的一致性。

到了第5步和第6步,業(yè)務(wù)邏輯處理完成,這時(shí)只可能發(fā)生系統(tǒng)錯(cuò)誤,如果第5步失敗,則可以將消息隊(duì)列和數(shù)據(jù)庫(kù)事務(wù)全部回滾,保持一致。如果第5步成功,第6步遇到了網(wǎng)絡(luò)超時(shí)等問(wèn)題,則這是唯一可能產(chǎn)生問(wèn)題的情況。

在這種情況下,消息的消費(fèi)過(guò)程并沒(méi)有被提交到消息隊(duì)列,消息隊(duì)列可能會(huì)重新發(fā)送消息給其他消息處理服務(wù),這會(huì)導(dǎo)致消息被重復(fù)消費(fèi),但是可以通過(guò)冪等處理來(lái)保證消除重復(fù)消息帶來(lái)的影響。

當(dāng)然,在使用這種模式時(shí),我們要充分考慮每個(gè)資源的提交順序。我們?cè)谏a(chǎn)實(shí)踐中遇到的一種反模式,就是在數(shù)據(jù)庫(kù)事務(wù)中嵌套遠(yuǎn)程調(diào)用,而且遠(yuǎn)程調(diào)用是耗時(shí)任務(wù),導(dǎo)致數(shù)據(jù)庫(kù)事務(wù)被拉長(zhǎng),***拖垮數(shù)據(jù)庫(kù)。

因此,上面的案例涉及的是消息事務(wù)嵌套數(shù)據(jù)庫(kù)事務(wù),在這里必須進(jìn)行充分評(píng)估和設(shè)計(jì),才可以規(guī)避事務(wù)風(fēng)險(xiǎn)。

3)事務(wù)補(bǔ)償機(jī)制

顯然,在對(duì)性能要求很高的場(chǎng)景中,兩階段提交協(xié)議并不是一種好方案,***努力保證模式也會(huì)使多個(gè)分布式操作互相嵌套,有可能互相影響。這里,我們給出事務(wù)補(bǔ)償機(jī)制,其性能很高,并且能夠盡***可能地保證事務(wù)的最終一致性。

在數(shù)據(jù)庫(kù)分庫(kù)分表后,如果涉及的多個(gè)更新操作在某一個(gè)數(shù)據(jù)庫(kù)范圍內(nèi)完成,則可以使用數(shù)據(jù)庫(kù)內(nèi)的本地事務(wù)保證一致性;對(duì)于跨庫(kù)的多個(gè)操作,可通過(guò)補(bǔ)償和重試,使其在一定的時(shí)間窗口內(nèi)完成操作,這樣就可以實(shí)現(xiàn)事務(wù)的最終一致性,突破事務(wù)遇到問(wèn)題就滾回的傳統(tǒng)思路。

如果采用事務(wù)補(bǔ)償機(jī)制,則在遇到問(wèn)題時(shí),我們需要記錄遇到問(wèn)題的環(huán)境、信息、步驟、狀態(tài)等,后續(xù)通過(guò)重試機(jī)制使其達(dá)到最終一致性,詳細(xì)內(nèi)容可以參考《分布式服務(wù)架構(gòu):原理、設(shè)計(jì)與實(shí)戰(zhàn)》第2章,徹底理解ACID原理、CAP理論、BASE原理、最終一致性模式等內(nèi)容。

2. 事務(wù)路由

無(wú)論使用上面哪種方法實(shí)現(xiàn)分布式事務(wù),都需要對(duì)分庫(kù)分表的多個(gè)數(shù)據(jù)源路由事務(wù),一般通過(guò)對(duì)Spring環(huán)境的配置,為不同的數(shù)據(jù)源配置不同的事務(wù)管理器(TransactionManager)。

這樣,如果更新操作在一個(gè)數(shù)據(jù)庫(kù)實(shí)例內(nèi)發(fā)生,便可以使用數(shù)據(jù)源的事務(wù)來(lái)處理。對(duì)于跨數(shù)據(jù)源的事務(wù),可通過(guò)在應(yīng)用層使用***努力保證模式和事務(wù)補(bǔ)償機(jī)制來(lái)達(dá)成事務(wù)的一致性。

當(dāng)然,有時(shí)我們需要通過(guò)編寫(xiě)程序來(lái)選擇數(shù)據(jù)庫(kù)的事務(wù)管理器,根據(jù)實(shí)現(xiàn)方式的不同,可將事務(wù)路由具體分為以下三種。

1)自動(dòng)提交事務(wù)路由

自動(dòng)提交事務(wù)通過(guò)依賴JDBC數(shù)據(jù)源的自動(dòng)提交事務(wù)特性,對(duì)任何數(shù)據(jù)庫(kù)進(jìn)行更新操作后會(huì)自動(dòng)提交事務(wù),不需要開(kāi)發(fā)人員手工操作事務(wù),也不需要配置事務(wù),實(shí)現(xiàn)起來(lái)很簡(jiǎn)單,但是只能滿足簡(jiǎn)單的業(yè)務(wù)邏輯需求。

在通常情況下,JDBC在連接創(chuàng)建后默認(rèn)設(shè)置自動(dòng)提交為true,當(dāng)然,也可以在獲取連接后手工修改這個(gè)屬性,代碼如下:

 

  1. connnection conn = null 
  2. try{  
  3. conn = getConnnection();  
  4. conn.setAutoCommit(true);  
  5. // 數(shù)據(jù)庫(kù)操作  
  6. ……………………………  
  7. conn.commit();  
  8. }catch(Throwable e){  
  9. if(conn!=null){  
  10. try {  
  11. conn.rollback();  
  12. } catch (SQLException e1) {  
  13. e1.printStackTrace();  
  14.  
  15.  
  16. throw new RuntimeException(e);  
  17. }finally{  
  18. if(conn!=null){  
  19. try {  
  20. conn.close();  
  21. } catch (SQLException e) {  
  22. e.printStackTrace();  
  23.  
  24.  

我們基本不需要使用原始的JDBC API來(lái)改變這些屬性,這些操作一般都會(huì)被封裝在我們使用的框架中。本書(shū)3.6節(jié)介紹的開(kāi)源數(shù)據(jù)庫(kù)分庫(kù)分表框架dbsplit默認(rèn)使用的就是這種模式。

2)可編程事務(wù)路由

我們?cè)趹?yīng)用中通常采用Spring的聲明式的事務(wù)來(lái)管理數(shù)據(jù)庫(kù)事務(wù),在分庫(kù)分表時(shí),事務(wù)處理是個(gè)問(wèn)題,在一個(gè)需要開(kāi)啟事務(wù)的方法中,需要?jiǎng)討B(tài)地確定開(kāi)啟哪個(gè)數(shù)據(jù)庫(kù)實(shí)例的事務(wù),也就是說(shuō)在每個(gè)開(kāi)啟事務(wù)的方法調(diào)用前就必須確定開(kāi)啟哪個(gè)數(shù)據(jù)源的事務(wù)。下面使用偽代碼來(lái)說(shuō)明如何實(shí)現(xiàn)一個(gè)可編程事務(wù)路由的小框架。

首先,通過(guò)Spring配置文件展示可編程事務(wù)小框架是怎么使用的:

 

  1. <?xml version="1.0?>  
  2. <beans>  
  3. <bean id="sharding-db-trx0"class="org.springframework.jdbc.datasource.Data SourceTransactionManager" 
  4. <property name="dataSource" 
  5. <ref bean="sharding-db0" />  
  6. </property>  
  7. </bean>  
  8. <bean id="sharding-db-trx1"  
  9. class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger" 
  10. <property name="dataSource" 
  11. <ref bean="sharding-db1" />  
  12. </property>  
  13. </bean>  
  14. <bean id="sharding-db-trx2"  
  15. class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger" 
  16. <property name="dataSource" 
  17. <ref bean="sharding-db2" />  
  18. </property>  
  19. </bean>  
  20. <bean id="sharding-db-trx3"  
  21. class="org.springframework.jdbc.datasource.DataSourceTransactionMana ger" 
  22. <property name="dataSource" 
  23. <ref bean="sharding-db3" />  
  24. </property>  
  25. </bean>  
  26. <bean id="shardingTransactionManager" class="com.robert.dbsplit.core. ShardingTransactionManager" 
  27. <property name="proxyTransactionManagers" 
  28. <map value-type="org.springframework.transaction.PlatformTran sactionManager" 
  29. <entry key="sharding0" value-ref="sharding-db-trx0" />  
  30. <entry key="sharding1" value-ref="sharding-db-trx1" />  
  31. <entry key="sharding2" value-ref="sharding-db-trx2" />  
  32. <entry key="sharding3" value-ref="sharding-db-trx3" />  
  33. </map>  
  34. </property>  
  35. </bean>  
  36. <aop:config>  
  37. <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*insert(..))"/>  
  38. <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*update(..))"/>  
  39. <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.robert.biz.*delete(..))"/>  
  40. </aop:config>  
  41. <tx:advice id="txAdvice" transaction-manager="shardingTransactionManager" 
  42. <tx:attributes>  
  43. <tx:method name="*" rollback-for="java.lang.Exception"/>  
  44. </tx:attributes>  
  45. </tx:advice>  
  46. </beans> 

這里使用Spring環(huán)境的aop和tx標(biāo)簽來(lái)攔截com.robert.biz包下的所有插入、更新和刪除的方法,當(dāng)指定的包的方法被調(diào)用時(shí),就會(huì)使用Spring提供的事務(wù)Advice,Spring的事務(wù)Advice(tx:advice)會(huì)使用事務(wù)管理器來(lái)控制事務(wù),如果某個(gè)方法發(fā)生了異常,那么Spring的事務(wù)Advice就會(huì)使shardingTransactionManager回滾相應(yīng)的事務(wù)。

我們看到shardingTransactionManager的類型是ShardingTransactionManager,這個(gè)類型是我們開(kāi)發(fā)的一個(gè)組合的事務(wù)管理器,這個(gè)事務(wù)管理器聚合了所有分片數(shù)據(jù)庫(kù)的事務(wù)管理器對(duì)象,然后根據(jù)某個(gè)標(biāo)記來(lái)路由到不同的事務(wù)管理器中,這些事務(wù)管理器用來(lái)控制各個(gè)分片的數(shù)據(jù)源的事務(wù)。

這里的標(biāo)記是什么呢?我們?cè)谡{(diào)用方法時(shí),會(huì)提前把分片的標(biāo)記放進(jìn)ThreadLocal中,然后在ShardingTransactionManager的getTransaction方法被調(diào)用時(shí),取得ThreadLocal中存的標(biāo)記,***根據(jù)標(biāo)記來(lái)判斷使用哪個(gè)分片數(shù)據(jù)庫(kù)的事務(wù)管理器對(duì)象。

為了通過(guò)標(biāo)記路由到不同的事務(wù)管理器,我們?cè)O(shè)計(jì)了一個(gè)專門(mén)的ShardingContextHolder類,在該類的內(nèi)部使用了一個(gè)ThreadLocal類來(lái)指定分片數(shù)據(jù)庫(kù)的關(guān)鍵字,在ShardingTransaction Manager中通過(guò)取得這個(gè)標(biāo)記來(lái)選擇具體的分片數(shù)據(jù)庫(kù)的事務(wù)管理器對(duì)象。

因此,這個(gè)類提供了setShard和getShard的方法,setShard用于使用者編程指定使用哪個(gè)分片數(shù)據(jù)庫(kù)的事務(wù)管理器,而getShard用于ShardingTransactionManager獲取標(biāo)記并取得分片數(shù)據(jù)庫(kù)的事務(wù)管理器對(duì)象。相關(guān)代碼如下:

 

  1. public class ShardingContextHolder<T> {  
  2. private static final ThreadLocal shardHolder = new ThreadLocal();  
  3. public static <T> void setShard(T shard) {  
  4. Validate.notNull(shard, "請(qǐng)指定某個(gè)分片數(shù)據(jù)庫(kù)!");  
  5. shardHolder.set(shard);  
  6.  
  7. public static <T> T getShard() {  
  8. return (T) shardHolder.get();  
  9.  

 

有了ShardingContextHolder類后,我們就可以在ShardingTransactionManager中根據(jù)給定的分片配置將事務(wù)操控權(quán)路由到不同分片的數(shù)據(jù)庫(kù)的事務(wù)管理器上,實(shí)現(xiàn)很簡(jiǎn)單,如果在ThreadLocal中存儲(chǔ)了某個(gè)分片數(shù)據(jù)庫(kù)的事務(wù)管理器的關(guān)鍵字,就使用那個(gè)分片的數(shù)據(jù)庫(kù)的事務(wù)管理器:

 

  1. public class ShardingTransactionManager implements PlatformTransactionManager {  
  2. private Map<Object, PlatformTransactionManager> proxyTransactionManagers = new HashMap<Object, PlatformTransactionManager>();  
  3. protected PlatformTransactionManager getTargetTransactionManager() {  
  4. Object shard = ShardingContextHolder.getShard();  
  5. Validate.notNull(shard, "必須指定一個(gè)路由的shard!");  
  6. return targetTransactionManagers.get(shard);  
  7.  
  8. public void setProxyTransactionManagers(Map<Object, PlatformTransaction Manager> targetTransactionManagers) {  
  9. this.targetTransactionManagers = targetTransactionManagers;  
  10.  
  11. public void commit(TransactionStatus status) throws TransactionException {  
  12. getProxyTransactionManager().commit(status);  
  13.  
  14. public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {  
  15. return getProxyTransactionManager().getTransaction(definition);  
  16.  
  17. public void rollback(TransactionStatus status) throws TransactionException  
  18.  
  19. getProxyTransactionManager().rollback(status);  

 

有了這些使用類,我們的可編程事務(wù)路由小框架就實(shí)現(xiàn)了,這樣在某個(gè)具體的服務(wù)開(kāi)始之前,我們就可以使用如下代碼來(lái)控制使用某個(gè)分片的數(shù)據(jù)庫(kù)的事務(wù)管理器了:

 

  1. RoutingContextHolder.setShard("sharding0");  
  2. return userService.create(user); 

 

3)聲明式事務(wù)路由

在上一小節(jié)實(shí)現(xiàn)了可編程事務(wù)路由的小框架,這個(gè)小框架通過(guò)讓開(kāi)發(fā)人員在ThreadLocal中指定數(shù)據(jù)庫(kù)分片并編程實(shí)現(xiàn)。

大多數(shù)分庫(kù)分表框架會(huì)實(shí)現(xiàn)聲明式事務(wù)路由,也就是在實(shí)現(xiàn)的服務(wù)方法上直接聲明事務(wù)的處理注解,注解包含使用哪個(gè)數(shù)據(jù)庫(kù)分片的事務(wù)管理器的信息,這樣,開(kāi)發(fā)人員就可以專注于業(yè)務(wù)邏輯的實(shí)現(xiàn),把事務(wù)處理交給框架來(lái)實(shí)現(xiàn)。

下面是筆者在實(shí)際的線上項(xiàng)目中實(shí)現(xiàn)的聲明式事務(wù)路由的一個(gè)使用實(shí)例:

 

  1. @TransactionHint(table = "INVOICE", keyPath = "0.accountId" 
  2. public void persistInvoice(Invoice invoice) {  
  3. // Save invoice to DB  
  4. this.createInvoice(invoice);  
  5. for (InvoiceItem invoiceItem : invoice.getItems()) {  
  6. invoiceItem.setInvId(invoice.getId());  
  7. invoiceItemService.createInvoiceItem(invoice.getAccountId(), invoiceItem);  
  8.  
  9. // Save invoice to cache  
  10. invoiceCacheService.set(invoice.getAccountId(), invoice.getInvPeriodStart().getTime(), invoice.getInvPeriodEnd().getTime(), invoice);  
  11. // Update last invoice date to Account  
  12. Account account = new Account();  
  13. account.setId(invoice.getAccountId());  
  14. account.setLstInvDate(invoice.getInvPeriodEnd()); 
  15. accountService.updateAccount(account);  

 

在這個(gè)實(shí)例中,我們開(kāi)發(fā)了一個(gè)持久發(fā)票的服務(wù)方法。持久發(fā)票的服務(wù)方法用來(lái)保存發(fā)票信息和發(fā)票項(xiàng)的詳情信息,這里,發(fā)票與發(fā)票項(xiàng)這兩個(gè)領(lǐng)域?qū)ο缶哂懈缸咏Y(jié)構(gòu)關(guān)系。

由于在設(shè)計(jì)過(guò)程中通過(guò)賬戶ID對(duì)這個(gè)父子表進(jìn)行分庫(kù)分表,因此在進(jìn)行事務(wù)路由時(shí),也需要通過(guò)賬戶ID控制使用哪個(gè)數(shù)據(jù)庫(kù)分片的事務(wù)管理器。在這個(gè)實(shí)例中,我們配置了 TransactionHint,TransactionHint的聲明如下:

 

  1. @Target({ElementType.METHOD})  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. public @interface TransactionHint {  
  5. String table() default "" 
  6. String keyPath() default "" 

 

可以看到,TransactionHint包含了兩個(gè)屬性,第1個(gè)屬性table指定這次操作涉及分片的數(shù)據(jù)庫(kù)表,第2個(gè)屬性指定這次操作根據(jù)哪個(gè)參數(shù)的哪個(gè)字段進(jìn)行分片路由。該實(shí)例通過(guò)table指定了INVOICE表,并通過(guò)keyPath指定了使用第1個(gè)參數(shù)的字段accountId作為路由的關(guān)鍵字。

這里的實(shí)現(xiàn)與可編程事務(wù)路由的小框架實(shí)現(xiàn)類似,在方法persistInvoice被調(diào)用時(shí),根據(jù)TransactionHint提供的操作的數(shù)據(jù)庫(kù)表名稱,在Spring環(huán)境的配置中找到這個(gè)表的分庫(kù)分表的配置信息,例如:一共分了多少個(gè)數(shù)據(jù)庫(kù)實(shí)例、數(shù)據(jù)庫(kù)和表。

下面是在Spring環(huán)境中配置的INVOICE表和INVOICE_ITEM表的具體信息,我們看到它們一共使用了兩個(gè)數(shù)據(jù)庫(kù)實(shí)例,每個(gè)實(shí)例有兩個(gè)庫(kù),每個(gè)庫(kù)有8個(gè)表,使用水平下標(biāo)策略。配置如下:

 

  1. <bean name="billingInvSplitTable" class="com.robert.dbsplit.core.Split Table"init-method="init" 
  2. <property name="dbNamePrefix" value="billing_inv"/>  
  3. <property name="tableNamePrefix" value="INVOICE"/>  
  4. <property name="dbNum" value="2"/>  
  5. <property name="tableNum" value="8"/>  
  6. <property name="splitStrategyType" value="HORIZONTAL"/>  
  7. <property name="splitNodes" 
  8. <list>  
  9. <ref bean="splitNode0"/>  
  10. <ref bean="splitNode1"/>  
  11. </list>  
  12. </property>  
  13. <property name="readWriteSeparate" value="true"/>  
  14. </bean>  
  15. <bean name="billingInvItemSplitTable" class="com.robert.dbsplit.core.SplitTable"  init-method="init" 
  16. <property name="dbNamePrefix" value="billing_inv"/>  
  17. <property name="tableNamePrefix" value="INVOICE_ITEM"/>  
  18. <property name="dbNum" value="2"/>  
  19. <property name="tableNum" value="8"/>  
  20. <property name="splitStrategyType" value="HORIZONTAL"/>  
  21. <property name="splitNodes" 
  22. <list>  
  23. <ref bean="splitNode0"/>  
  24. <ref bean="splitNode1"/>  
  25. </list>  
  26. </property>  
  27. <property name="readWriteSeparate" value="true"/>  
  28. </bean> 

 

然后,在方法被調(diào)用時(shí)通過(guò)AOP進(jìn)行攔截,根據(jù)TransactionHint配置的路由的主鍵信息keyPath = "0.accountId",得知這次根據(jù)第0個(gè)參數(shù)Invoice的accountID字段來(lái)路由,根據(jù)Invoice的accountID的值來(lái)計(jì)算這次持久發(fā)票表具體涉及哪個(gè)數(shù)據(jù)庫(kù)分片,然后把這個(gè)數(shù)據(jù)庫(kù)分片的信息保存到ThreadLocal中。具體的實(shí)現(xiàn)代碼如下:

 

  1. SimpleSplitJdbcTemplate simpleSplitJdbcTemplate = (SimpleSplitJdbcTemplate) ReflectionUtil.getFieldValue(field SimpleSplitJdbcTemplate, invocation.getThis());  
  2. Method method = invocation.getMethod();  
  3. // Convert to th method of implementation class  
  4. method = targetClass.getMethod(method.getName(), method.getParameter Types());  
  5. TransactionHint[] transactionHints = method.getAnnotationsByType (TransactionHint.class);  
  6. if (transactionHints == null || transactionHints.length < 1)  
  7. throw new IllegalArgumentException("The method " + method + " includes illegal transaction hint.");  
  8. TransactionHint transactionHint = transactionHints[0];  
  9. String tableName = transactionHint.table();  
  10. String keyPath = transactionHint.keyPath();  
  11. String[] parts = keyPath.split("\.");  
  12. int paramIndex = Integer.valueOf(parts[0]);  
  13. Object[] params = invocation.getArguments();  
  14. Object splitKey = params[paramIndex];  
  15. if (parts.length > 1) {  
  16. String[] paths = Arrays.copyOfRange(parts, 1, parts.length);  
  17. splitKey = ReflectionUtil.getFieldValueByPath(splitKey, paths);  
  18.  
  19. SplitNode splitNode = simpleSplitJdbcTemplate.decideSplitNode(tableName, splitKey);  
  20. ThreadContextHolder.INST.setContext(splitNode);  
  21. ThreadContextHolder是一個(gè)單例的對(duì)象,在該對(duì)象里封裝了一個(gè)ThreadLocal,用來(lái)存儲(chǔ)某個(gè)方法在某個(gè)線程下關(guān)聯(lián)的分片信息:  
  22. public class ThreadContextHolder<T> {  
  23. public static final ThreadContextHolder<SplitNode> INST = new ThreadContextHolder<SplitNode>();  
  24. private ThreadLocal<T> contextHolder = new ThreadLocal<T>();  
  25. public T getContext() {  
  26. return contextHolder.get();  
  27.  
  28. public void setContext(T context) {  
  29. contextHolder.set(context);  
  30.  

 

接下來(lái)與可編程式事務(wù)路由類似,實(shí)現(xiàn)一個(gè)定制化的事務(wù)管理器,在獲取目標(biāo)事務(wù)管理器時(shí),通過(guò)我們?cè)赥hreadLocal中保存的數(shù)據(jù)庫(kù)分片信息,獲得這個(gè)分片數(shù)據(jù)庫(kù)的事務(wù)管理器,然后返回:

 

  1. public class RoutingTransactionManager implements PlatformTransactionManager {  
  2. protected PlatformTransactionManager getTargetTransactionManager() {  
  3. SplitNode splitNode = ThreadContextHolder.INST.getContext(); 
  4. return splitNode.getPlatformTransactionManager(); 
  5.  
  6. public void commit(TransactionStatus status) throws TransactionException {  
  7. getTargetTransactionManager().commit(status);  
  8.  
  9. public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {  
  10. return getTargetTransactionManager().getTransaction(definition);  
  11. public void rollback(TransactionStatus status) throws TransactionException  
  12.  
  13. getTargetTransactionManager().rollback(status);  
  14.  

 

本書(shū)3.6節(jié)介紹的開(kāi)源數(shù)據(jù)庫(kù)分庫(kù)分表框架dbsplit是一個(gè)分庫(kù)分表的簡(jiǎn)單示例實(shí)現(xiàn),在筆者所工作的公司內(nèi)部有內(nèi)部版本,在內(nèi)部版本中實(shí)現(xiàn)了聲明式事務(wù)路由,但是這部分功能并沒(méi)有開(kāi)源到dbsplit項(xiàng)目,原因是有些與業(yè)務(wù)結(jié)合的邏輯無(wú)法分離。如果感興趣,則可以加入我們的開(kāi)源項(xiàng)目開(kāi)發(fā)中。

四、讀寫(xiě)分離

在實(shí)際應(yīng)用中的絕大多數(shù)情況下讀操作遠(yuǎn)大于寫(xiě)操作。MySQL提供了讀寫(xiě)分離的機(jī)制,所有寫(xiě)操作必須對(duì)應(yīng)到主庫(kù)(Master),讀操作可以在主庫(kù)(Master)和從庫(kù)(Slave)機(jī)器上進(jìn)行。

主庫(kù)與從庫(kù)的結(jié)構(gòu)完全一樣,一個(gè)主庫(kù)可以有多個(gè)從庫(kù),甚至在從庫(kù)下還可以掛從庫(kù),這種一主多從的方式可以有效地提高數(shù)據(jù)庫(kù)集群的吞吐量。

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

所有寫(xiě)操作都先在主庫(kù)上進(jìn)行,然后異步更新到從庫(kù)上,所以從主庫(kù)同步到從庫(kù)機(jī)器有一定的延遲,當(dāng)系統(tǒng)很繁忙時(shí),延遲問(wèn)題會(huì)更加嚴(yán)重,從庫(kù)機(jī)器數(shù)量的增加也會(huì)使這個(gè)問(wèn)題更嚴(yán)重。

此外,主庫(kù)是集群的瓶頸,當(dāng)寫(xiě)操作過(guò)多時(shí)會(huì)嚴(yán)重影響主庫(kù)的穩(wěn)定性,如果主庫(kù)掛掉,則整個(gè)集群都將不能正常工作。

根據(jù)以上特點(diǎn),我們總結(jié)一些***實(shí)踐如下。

  • 當(dāng)讀操作壓力很大時(shí),可以考慮添加從庫(kù)機(jī)器來(lái)分解大量讀操作帶來(lái)的壓力,但是當(dāng)從庫(kù)機(jī)器達(dá)到一定的數(shù)量時(shí),就需要考慮分庫(kù)來(lái)緩解壓力了。

  • 當(dāng)寫(xiě)壓力很大時(shí),就必須進(jìn)行分庫(kù)操作了。

可能會(huì)因?yàn)榉N種原因,集群中的數(shù)據(jù)庫(kù)硬件配置等會(huì)不一樣,某些性能高,某些性能低,這時(shí)可以通過(guò)程序控制每臺(tái)機(jī)器讀寫(xiě)的比重來(lái)達(dá)到負(fù)載均衡,這需要更加復(fù)雜的讀寫(xiě)分離的路由規(guī)則。

五、分庫(kù)分表引起的問(wèn)題

分庫(kù)分表按照某種規(guī)則將數(shù)據(jù)的集合拆分成多個(gè)子集合,數(shù)據(jù)的完整性被打破,因此在某種場(chǎng)景下會(huì)產(chǎn)生多種問(wèn)題。

1. 擴(kuò)容與遷移

在分庫(kù)分表后,如果涉及的分片已經(jīng)達(dá)到了承載數(shù)據(jù)的***值,就需要對(duì)集群進(jìn)行擴(kuò)容。擴(kuò)容是很麻煩的,一般會(huì)成倍地?cái)U(kuò)容。

通用的擴(kuò)容方法包括如下5個(gè)步驟:

Step1:按照新舊分片規(guī)則,對(duì)新舊數(shù)據(jù)庫(kù)進(jìn)行雙寫(xiě)。

Step2:將雙寫(xiě)前按照舊分片規(guī)則寫(xiě)入的歷史數(shù)據(jù),根據(jù)新分片規(guī)則遷移寫(xiě)入新的數(shù)據(jù)庫(kù)。

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

Step4:將雙寫(xiě)數(shù)據(jù)庫(kù)邏輯從代碼中下線,只按照新的分片規(guī)則寫(xiě)入數(shù)據(jù)。

Step5:刪除按照舊分片規(guī)則寫(xiě)入的歷史數(shù)據(jù)。

這里,在第2步遷移歷史數(shù)據(jù)時(shí),由于數(shù)據(jù)量很大,通常會(huì)導(dǎo)致不一致,因此,先清洗舊的數(shù)據(jù),洗完后再遷移到新規(guī)則的新數(shù)據(jù)庫(kù)下,再做全量對(duì)比,對(duì)比后評(píng)估在遷移的過(guò)程中是否有數(shù)據(jù)的更新,如果有的話就再清洗、遷移,***以對(duì)比沒(méi)有差距為準(zhǔn)。

如果是金融交易數(shù)據(jù),則***將動(dòng)靜數(shù)據(jù)分離,隨著時(shí)間的流逝,某個(gè)時(shí)間點(diǎn)之前的數(shù)據(jù)是不會(huì)被更新的,我們就可以拉長(zhǎng)雙寫(xiě)的時(shí)間窗口,這樣在足夠長(zhǎng)的時(shí)間流逝后,只需遷移那些不再被更新的歷史數(shù)據(jù)即可,就不會(huì)在遷移的過(guò)程中由于歷史數(shù)據(jù)被更新而導(dǎo)致代理不一致。

在數(shù)據(jù)量巨大時(shí),如果數(shù)據(jù)遷移后沒(méi)法進(jìn)行全量對(duì)比,就需要進(jìn)行抽樣對(duì)比,在進(jìn)行抽樣對(duì)比時(shí)要根據(jù)業(yè)務(wù)的特點(diǎn)選取一些具有某類特征性的數(shù)據(jù)進(jìn)行對(duì)比。

在遷移的過(guò)程中,數(shù)據(jù)的更新會(huì)導(dǎo)致不一致,可以在線上記錄遷移過(guò)程中的更新操作的日志,遷移后根據(jù)更新日志與歷史數(shù)據(jù)共同決定數(shù)據(jù)的***狀態(tài),來(lái)達(dá)到遷移數(shù)據(jù)的最終一致性。

2. 分庫(kù)分表維度導(dǎo)致的查詢問(wèn)題

在分庫(kù)分表以后,如果查詢的標(biāo)準(zhǔn)是分片的主鍵,則可以通過(guò)分片規(guī)則再次路由并查詢;但是對(duì)于其他主鍵的查詢、范圍查詢、關(guān)聯(lián)查詢、查詢結(jié)果排序等,并不是按照分庫(kù)分表維度來(lái)查詢的。

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

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

所以常見(jiàn)的解決方式如下:

  • 在多個(gè)分片表查詢后合并數(shù)據(jù)集,這種方式的效率很低。

  • 記錄兩份數(shù)據(jù),一份按照買(mǎi)家緯度分表,一份按照商品維度分表。

  • 通過(guò)搜索引擎解決,但如果實(shí)時(shí)性要求很高,就需要實(shí)現(xiàn)實(shí)時(shí)搜索。

實(shí)際上,在高并發(fā)的服務(wù)平臺(tái)下,交易系統(tǒng)是專門(mén)做交易的,因?yàn)榻灰资呛诵姆?wù),SLA的級(jí)別比較高,所以需要和查詢系統(tǒng)分離,查詢一般通過(guò)其他系統(tǒng)進(jìn)行,數(shù)據(jù)也可能是冗余存儲(chǔ)的。

這里再舉個(gè)例子,在某電商交易平臺(tái)下,可能有買(mǎi)家查詢自己在某一時(shí)間段的訂單,也可能有賣(mài)家查詢自己在某一時(shí)間段的訂單,如果使用了分庫(kù)分表方案,則這兩個(gè)需求是難以滿足的。

因此,通用的解決方案是,在交易生成時(shí)生成一份按照買(mǎi)家分片的數(shù)據(jù)副本和一份按照賣(mài)家分片的數(shù)據(jù)副本,查詢時(shí)分別滿足之前的兩個(gè)需求,因此,查詢的數(shù)據(jù)和交易的數(shù)據(jù)可能是分別存儲(chǔ)的,并從不同的系統(tǒng)提供接口。

另外,在電商系統(tǒng)中,在一個(gè)交易訂單生成后,一般需要引用到訂單中交易的商品實(shí)體,如果簡(jiǎn)單地引用,若商品的金額等信息發(fā)生變化,則會(huì)導(dǎo)致原訂單上的商品信息也會(huì)發(fā)生變化,這樣買(mǎi)家會(huì)很疑惑。

因此,通用的解決方案是在交易系統(tǒng)中存儲(chǔ)商品的快照,在查詢交易時(shí)使用交易的快照,因?yàn)榭煺帐莻€(gè)靜態(tài)數(shù)據(jù),永遠(yuǎn)都不會(huì)更新,所以解決了這個(gè)問(wèn)題。

可見(jiàn)查詢的問(wèn)題***在單獨(dú)的系統(tǒng)中使用其他技術(shù)來(lái)解決,而不是在交易系統(tǒng)中實(shí)現(xiàn)各類查詢功能;當(dāng)然,也可以通過(guò)對(duì)商品的變更實(shí)施版本化,在交易訂單中引用商品的版本信息,在版本更新時(shí)保留商品的舊版本,這也是一種不錯(cuò)的解決方案。

***,關(guān)聯(lián)的表有可能不在同一數(shù)據(jù)庫(kù)中,所以基本不可能進(jìn)行聯(lián)合查詢,需要借助大數(shù)據(jù)技術(shù)來(lái)實(shí)現(xiàn),也就是上面所說(shuō)的第3種方法,即通過(guò)大數(shù)據(jù)技術(shù)統(tǒng)一聚合和處理關(guān)系型數(shù)據(jù)庫(kù)的數(shù)據(jù),然后對(duì)外提供查詢操作,請(qǐng)參考第5章的內(nèi)容。

3. 跨庫(kù)事務(wù)難以實(shí)現(xiàn)

要避免在一個(gè)事務(wù)中同時(shí)修改數(shù)據(jù)庫(kù)db0和數(shù)據(jù)庫(kù)db1中的表,因?yàn)椴僮髌饋?lái)很復(fù)雜,對(duì)效率也會(huì)有一定的影響。請(qǐng)參考第三章的內(nèi)容。

4. 同組數(shù)據(jù)跨庫(kù)問(wèn)題

要盡量把同一組數(shù)據(jù)放到同一臺(tái)數(shù)據(jù)庫(kù)服務(wù)器上,不但在某些場(chǎng)景下可以利用本地事務(wù)的強(qiáng)一致性,還可以使這組數(shù)據(jù)自治。

以電商為例,我們的應(yīng)用有兩個(gè)數(shù)據(jù)庫(kù)db0和db1,分庫(kù)分表后,按照id維度,將賣(mài)家A的交易信息存放到db0中。當(dāng)數(shù)據(jù)庫(kù)db1掛掉時(shí),賣(mài)家A的交易信息不受影響,依然可以正常使用。也就是說(shuō),要避免數(shù)據(jù)庫(kù)中的數(shù)據(jù)依賴另一數(shù)據(jù)庫(kù)中的數(shù)據(jù)。 

責(zé)任編輯:龐桂玉 來(lái)源: 今日頭條
相關(guān)推薦

2022-12-25 18:58:53

架構(gòu)RabbitMQ

2021-10-29 07:25:32

分庫(kù)分表技巧

2018-07-30 09:33:21

2018-03-25 09:11:31

大數(shù)據(jù)機(jī)器學(xué)習(xí)分析軟件

2020-06-12 07:36:33

Redis

2015-09-20 15:50:46

2016-08-23 00:39:25

2024-08-13 17:09:00

架構(gòu)分庫(kù)分表開(kāi)發(fā)

2025-04-01 08:45:00

2020-12-30 05:36:59

分庫(kù)分表存儲(chǔ)

2017-04-24 13:51:16

設(shè)計(jì)師分析

2021-05-27 07:12:19

單點(diǎn)登錄系統(tǒng)

2024-01-10 08:36:10

延時(shí)關(guān)閉訂單

2019-05-21 14:37:41

數(shù)據(jù)科學(xué)可視化企業(yè)

2024-10-31 08:50:14

2022-12-29 09:49:06

輕量級(jí)架構(gòu)決策

2020-10-30 09:33:01

分庫(kù)分表數(shù)據(jù)庫(kù)

2016-03-25 09:57:09

統(tǒng)一監(jiān)控報(bào)警平臺(tái)運(yùn)維

2020-05-22 13:32:24

可視化詞云圖數(shù)據(jù)

2010-07-14 09:01:07

架構(gòu)設(shè)計(jì)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)