NoSQL數(shù)據(jù)庫的主主備份
Tarantool DBMS的高性能應(yīng)該很多人都聽說過,包括其豐富的工具套件和某些特定功能。比如,它擁有一個非常強大的on-disk存儲引擎Vinyl,并且知道怎樣處理JSON文檔。然而,大部分文章往往忽略了一個關(guān)鍵點:通常,Tarantool僅僅被視為存儲器,而實際上其***特點是能夠在存儲器內(nèi)部寫代碼,從而高效處理數(shù)據(jù)。如果你想知道我和igorcoding是怎樣在Tarantool內(nèi)部建立一個系統(tǒng)的,請繼續(xù)往下看。
如果你用過Mail.Ru電子郵件服務(wù),你應(yīng)該知道它可以從其他賬號收集郵件。如果支持OAuth協(xié)議,那么在收集其他賬號的郵件時,我們就不需要讓用戶提供第三方服務(wù)憑證了,而是用OAuth令牌來代替。此外,Mail.Ru Group有很多項目要求通過第三方服務(wù)授權(quán),并且需要用戶的OAuth令牌才能處理某些應(yīng)用。因此,我們決定建立一個存儲和更新令牌的服務(wù)。
我猜大家都知道OAuth令牌是什么樣的,閉上眼睛回憶一下,OAuth結(jié)構(gòu)由以下3-4個字段組成:
- {
- “token_type” : “bearer”,
- “access_token” : “XXXXXX”,
- “refresh_token” : “YYYYYY”,
- “expires_in” : 3600
- }
- 訪問令牌(access_token)——允許你執(zhí)行動作、獲取用戶數(shù)據(jù)、下載用戶的好友列表等等;
- 更新令牌(refresh_token)——讓你重新獲取新的access_token,不限次數(shù);
- 過期時間(expires_in)——令牌到期時間戳或任何其他預(yù)定義時間,如果你的access_token到期了,你就不能繼續(xù)訪問所需的資源。
現(xiàn)在我們看一下服務(wù)的簡單框架。設(shè)想有一些前端可以在我們的服務(wù)上寫入和讀出令牌,還有一個獨立的更新器,一旦令牌到期,就可以通過更新器從OAuth服務(wù)提供商獲取新的訪問令牌。
如上圖所示,數(shù)據(jù)庫的結(jié)構(gòu)也十分簡單,由兩個數(shù)據(jù)庫節(jié)點(主和從)組成,為了說明兩個數(shù)據(jù)庫節(jié)點分別位于兩個數(shù)據(jù)中心,二者之間由一條垂直的虛線隔開,其中一個數(shù)據(jù)中心包含主數(shù)據(jù)庫節(jié)點及其前端和更新器,另一個數(shù)據(jù)中心包含從數(shù)據(jù)庫節(jié)點及其前端,以及訪問主數(shù)據(jù)庫節(jié)點的更新器。
面臨的困難
我們面臨的主要問題在于令牌的使用期(一個小時)。詳細(xì)了解這個項目之后,也許有人會問“在一小時內(nèi)更新1000萬條記錄,這真的是高負(fù)載服務(wù)嗎?如果我們用一個數(shù)除一下,結(jié)果大約是3000rps”。然而,如果因為數(shù)據(jù)庫維護(hù)或故障,甚至服務(wù)器故障(一切皆有可能)導(dǎo)致一部分記錄沒有得到更新,那事情將會變得比較麻煩。比如,如果我們的服務(wù)(主數(shù)據(jù)庫)因為某些原因持續(xù)中斷15分鐘,就會導(dǎo)致25%的服務(wù)中斷(四分之一的令牌變成無效,不能再繼續(xù)使用);如果服務(wù)中斷30分鐘,將會有一半的數(shù)據(jù)不能得到更新;如果中斷1小時,那么所有的令牌都將失效。假設(shè)數(shù)據(jù)庫癱瘓一個小時,我們重啟系統(tǒng),然后整個1000萬條令牌都需要進(jìn)行快速更新。這算不算高負(fù)載服務(wù)呢?
一開始一切都還進(jìn)展地比較順利,但是兩年后,我們進(jìn)行了邏輯擴展,增加了幾個指標(biāo),并且開始執(zhí)行一些輔助邏輯…….總之,Tarantool耗盡了CPU資源。盡管所有資源都是遞耗資源,但這樣的結(jié)果確實讓我們大吃一驚。
幸運的是,系統(tǒng)管理員幫我們安裝了當(dāng)時庫存中內(nèi)存***的CPU,解決了我們隨后6個月的CPU需求。但這只是權(quán)宜之計,我們必須想出一個解決辦法。當(dāng)時,我們學(xué)習(xí)了一個新版的Tarantool(我們的系統(tǒng)是用Tarantool 1.5寫的,這個版本除了在Mail.Ru Group,其他地方基本沒用過)。Tarantool 1.6大力提倡主主備份,于是我們想:為什么不在連接主主備份的三個數(shù)據(jù)中心分別建立一個數(shù)據(jù)庫備份呢?這聽起來是個不錯的計劃。
三個主機、三個數(shù)據(jù)中心和三個更新器,都分別連接自己的主數(shù)據(jù)庫。即使一個或者兩個主機癱瘓了,系統(tǒng)仍然照常運行,對吧?那么這個方案的缺點是什么呢?缺點就是,我們將一個OAuth服務(wù)提供商的請求數(shù)量有效地增加到了三倍,也就是說,有多少個副本,我們就要更新幾乎相同數(shù)量的令牌,這樣不行。最直接的解決辦法就是,想辦法讓各個節(jié)點自己決定誰是leader,那樣就只需要更新存儲在leader上的節(jié)點了。
選擇leader節(jié)點
選擇leader節(jié)點的算法有很多,其中有一個算法叫Paxos,相當(dāng)復(fù)雜,不知道怎樣簡化,于是我們決定用Raft代替。Raft是一個非常通俗易懂的算法,誰能通信就選誰做leader,一旦通信連接失敗或者其他因素,就重新選leader。具體實施辦法如下:
Tarantool外部既沒有Raft也沒有Paxos,但是我們可以使用net.box內(nèi)置模式,讓所有節(jié)點連接成一個網(wǎng)狀網(wǎng)(即每一個節(jié)點連接剩下所有節(jié)點),然后直接在這些連接上用Raft算法選出leader節(jié)點。***,所有節(jié)點要么成為leader節(jié)點,要么成為follower節(jié)點,或者二者都不是。
如果你覺得Raft算法實施起來有困難,下面的Lua代碼可以幫到你:
- local r = self.pool.call(self.FUNC.request_vote,
- self.term, self.uuid)
- self._vote_count = self:count_votes(r)
- if self._vote_count > self._nodes_count / 2 then
- log.info(“[raft-srv] node %d won elections”, self.id)
- self:_set_state(self.S.LEADER)
- self:_set_leader({ id=self.id, uuid=self.uuid })
- self._vote_count = 0
- self:stop_election_timer()
- self:start_heartbeater()
- else
- log.info(“[raft-srv] node %d lost elections”, self.id)
- self:_set_state(self.S.IDLE)
- self:_set_leader(msgpack.NULL)
- self._vote_count = 0
- self:start_election_timer()
- end
現(xiàn)在我們給遠(yuǎn)程服務(wù)器發(fā)送請求(其他Tarantool副本)并計算來自每一個節(jié)點的票數(shù),如果我們有一個quorum,我們就選定了一個leader,然后發(fā)送heartbeats,告訴其他節(jié)點我們還活著。如果我們在選舉中失敗了,我們可以發(fā)起另一場選舉,一段時間之后,我們又可以投票或被選為leader。
只要我們有一個quorum,選中一個leader,我們就可以將更新器指派給所有節(jié)點,但是只準(zhǔn)它們?yōu)閘eader服務(wù)。
這樣我們就規(guī)范了流量,由于任務(wù)是由單一的節(jié)點派出,因此每一個更新器獲得大約三分之一的任務(wù),有了這樣的設(shè)置,我們可以失去任何一臺主機,因為如果某臺主機出故障了,我們可以發(fā)起另一個選舉,更新器也可以切換到另一個節(jié)點。然而,和其他分布式系統(tǒng)一樣,有好幾個問題與quorum有關(guān)。
“廢棄”節(jié)點
如果各個數(shù)據(jù)中心之間失去聯(lián)系了,那么我們需要有一些適當(dāng)?shù)臋C制去維持整個系統(tǒng)正常運轉(zhuǎn),還需要有一套機制能恢復(fù)系統(tǒng)的完整性。Raft成功地做到了這兩點:
假設(shè)Dataline數(shù)據(jù)中心掉線了,那么該位置的節(jié)點就變成了“廢棄”節(jié)點,也就是說該節(jié)點就看不到其他節(jié)點了,集群中的其他節(jié)點可以看到這個節(jié)點丟失了,于是引發(fā)了另一個選舉,然后新的集群節(jié)點(即上級節(jié)點)被選為leader,整個系統(tǒng)仍然保持運轉(zhuǎn),因為各個節(jié)點之間仍然保持一致性(大半部分節(jié)點仍然互相可見)。
那么問題來了,與丟失的數(shù)據(jù)中心有關(guān)的更新器怎么樣了呢?Raft說明書沒有給這樣的節(jié)點一個單獨的名字,通常,沒有quorum的節(jié)點和不能與leader聯(lián)系的節(jié)點會被閑置下來。然而,它可以自己建立網(wǎng)絡(luò)連接然后更新令牌,一般來說,令牌都是在連接模式時更新,但是,也許用一個連接“廢棄”節(jié)點的更新器也可以更新令牌。一開始我們并不確定這樣做有意義,這樣不會導(dǎo)致冗余更新嗎?
這個問題我們需要在實施系統(tǒng)的過程中搞清楚。我們的***個想法是不更新:我們有一致性、有quorum,丟失任何一個成員,我們都不應(yīng)該更新。但是后來我們有了另一個想法,我們看一下Tarantool中的主主備份,假設(shè)有兩個主節(jié)點和一個變量(key)X=1,我們同時在每一個節(jié)點上給這個變量賦一個新值,一個賦值為2,另一個賦值為3,然后,兩個節(jié)點互相交換備份日志(就是X變量的值)。在一致性上,這樣實施主主備份是很糟糕的(無意冒犯Tarantool開發(fā)者)。
如果我們需要嚴(yán)格的一致性,這樣是行不通的。然而,回憶一下我們的OAuth令牌是由以下兩個重要因素組成:
- 更新令牌,本質(zhì)上***有效;
- 訪問令牌,有效期為一個小時;
我們的更新器有一個refresh函數(shù),可以從一個更新令牌獲取任意數(shù)量的訪問令牌,一旦發(fā)布,它們都將保持一個小時內(nèi)有效。
我們考慮一下以下場景:兩個follower節(jié)點正在和一個leader節(jié)點交互,它們更新自己的令牌,接收***個訪問令牌,這個訪問令牌被復(fù)制,于是現(xiàn)在每一個節(jié)點都有這個訪問令牌,然后,連接中斷了,所以,其中一個follower節(jié)點變成了“廢棄”節(jié)點,它沒有quorum,既看不到leader也看不到其他follower,然而,我們允許我們的更新器去更新位于“廢棄”節(jié)點上的令牌,如果“廢棄”節(jié)點沒有連接網(wǎng)絡(luò),那么整個方案都將停止運行。盡管如此,如果發(fā)生簡單的網(wǎng)絡(luò)拆分,更新器還是可以維持正常運行。
一旦網(wǎng)絡(luò)拆分結(jié)束,“廢棄”節(jié)點重新加入集群,就會引發(fā)另一場選舉或者數(shù)據(jù)交換。注意,第二和第三個令牌一樣,也是“好的”。
原始的集群成員恢復(fù)之后,下一次更新將只在一個節(jié)點上發(fā)生,然后備份。換句話來說,當(dāng)集群拆分之后,被拆分的各個部分各自獨立更新,但是一旦重新整合,數(shù)據(jù)一致性也因此恢復(fù)。通常,需要N/2+1個活動節(jié)點(對于一個3節(jié)點集群,就是需要2個活動節(jié)點)去保持集群正常運轉(zhuǎn)。盡管如此,對我們而言,即使只有1個活動節(jié)點也足夠了,它會發(fā)送盡可能多的外部請求。
重申一下,我們已經(jīng)討論了請求數(shù)量逐漸增加的情況,在網(wǎng)絡(luò)拆分或節(jié)點中斷時期,我們能夠提供一個單一的活動節(jié)點,我們會像平時一樣更新這個節(jié)點,如果出現(xiàn)絕對拆分(即當(dāng)一個集群被分成***數(shù)量的節(jié)點,每一個節(jié)點有一個網(wǎng)絡(luò)連接),如上所述,OAuth服務(wù)提供商的請求數(shù)量將提升至三倍。但是,由于這個事件發(fā)生的時間相對短暫,所以情況不是太糟,我們可不希望一直工作在拆分模式。通常情況下,系統(tǒng)處于有quorum和網(wǎng)絡(luò)連接,并且所有節(jié)點都啟動運行的狀態(tài)。
分片
還有一個問題沒有解決:我們已經(jīng)達(dá)到了CPU上限,最直接的解決辦法就是分片。
假設(shè)我們有兩個數(shù)據(jù)庫分片,每一個都有備份,有一個這樣的函數(shù),給定一些key值,就可以計算出哪一個分片上有所需要的數(shù)據(jù)。如果我們通過電子郵件分片,一部分地址存儲在一個分片上,另一部分地址存儲在另一個分片上,我們很清楚我們的數(shù)據(jù)在哪里。
有兩種方法可以分片。一種是客戶端分片,我們選擇一個返回分片數(shù)量的連續(xù)的分片函數(shù),比如CRC32、Guava或Sumbur,這個函數(shù)在所有客戶端的實現(xiàn)方式都一樣。這種方法的一個明顯優(yōu)勢在于數(shù)據(jù)庫對分片一無所知,你的數(shù)據(jù)庫正常運轉(zhuǎn),然后分片就發(fā)生了。
然而,這種方法也存在一個很嚴(yán)重的缺陷。一開始,客戶端非常繁忙。如果你想要一個新的分片,你需要把分片邏輯加進(jìn)客戶端,這里的***的問題是,可能一些客戶端在使用這種模式,而另一些客戶端卻在使用另一種完全不同的模式,而數(shù)據(jù)庫本身卻不知道有兩種不同的分片模式。
我們選擇另一種方法—數(shù)據(jù)庫內(nèi)部分片,這種情況下,數(shù)據(jù)庫代碼變得更加復(fù)雜,但是為了折中我們可以使用簡單的客戶端,每一個連接數(shù)據(jù)庫的客戶端被路由到任意節(jié)點,由一個特殊函數(shù)計算出哪一個節(jié)點應(yīng)該被連接、哪一個節(jié)點應(yīng)該被控制。前面提到,由于數(shù)據(jù)庫變得更加復(fù)雜,因此為了折中,客戶端就變得更加簡單了,但是這樣的話,數(shù)據(jù)庫就要對其數(shù)據(jù)全權(quán)負(fù)責(zé)。此外,最困難的事就是重新分片,如果你有一大堆客戶端無法更新,相比之下,如果數(shù)據(jù)庫負(fù)責(zé)管理自己的數(shù)據(jù),那重新分片就會變得非常簡單。
具體怎樣實施呢?
六邊形代表Tarantool實體,有3個節(jié)點組成分片1,另一個3節(jié)點集群作為分片2,如果我們將所有節(jié)點互相連接,結(jié)果會怎樣呢?根據(jù)Raft,我們可以知道每一個集群的狀態(tài),誰是leader服務(wù)器誰是follower服務(wù)器也一目了然,由于是集群內(nèi)連接,我們還可以知道其他分片(例如它的leader分片或者follower分片)的狀態(tài)??偟膩碚f,如果訪問***個分片的用戶發(fā)現(xiàn)這并不是他需要的分片,我們很清楚地知道應(yīng)該指導(dǎo)他往哪里走。
我們來看一些簡單的例子。
假設(shè)用戶向駐留在***個分片上的key發(fā)出請求,該請求被***個分片上的某一個節(jié)點接收,這個節(jié)點知道誰是leader,于是將請求重新路由到分片leader,反過來,分片leader對這個key進(jìn)行讀或?qū)?,并且將結(jié)果反饋給用戶。
第二個場景:用戶的請求到達(dá)***個分片中的相同節(jié)點,但是被請求的key卻在第二個分片上,這種情況也可以用類似的方法處理,***個分片知道第二個分片上誰是leader,然后把請求送到第二個分片的leader進(jìn)行轉(zhuǎn)發(fā)和處理,再將結(jié)果返回給用戶。
這個方案十分簡單,但也存在一定的缺陷,其中***的問題就是連接數(shù),在二分片的例子中,每一個節(jié)點連接到其他剩下的節(jié)點,連接數(shù)是6*5=30,如果再加一個3節(jié)點分片,那么連接數(shù)就增加到72,這會不會有點多呢?
我們該如何解決這個問題呢?我們只需要增加一些Tarantool實例,我們叫它代理,而不叫分片或數(shù)據(jù)庫,用代理去解決所有的分片問題:包括計算key值和定位分片領(lǐng)導(dǎo)。另一方面,Raft集群保持自包含,只在分片內(nèi)部工作。當(dāng)用戶訪問代理時,代理計算出所需要的分片,如果需要的是leader,就對用戶作相應(yīng)的重定向,如果不是leader,就將用戶重定向至分片內(nèi)的任意節(jié)點。
由此產(chǎn)生的復(fù)雜性是線性的,取決于節(jié)點數(shù)量。現(xiàn)在一共3個節(jié)點,每個節(jié)點3個分片,連接數(shù)少了幾倍。
代理方案的設(shè)計考慮到了進(jìn)一步規(guī)模擴展(當(dāng)分片數(shù)量大于2時),當(dāng)只有2個分片時,連接數(shù)不變,但是當(dāng)分片數(shù)量增加時,連接數(shù)會劇減。分片列表存儲在Lua配置文件中,所以,如果想要獲取新列表,我們只需要重載代碼就好了。
綜上所述,首先,我們進(jìn)行主主備份,應(yīng)用Raft算法,然后加入分片和代理,***我們得到的是一個單塊,一個集群,所以說,目前這個方案看上去是比較簡單的。
剩下的就是只讀或只寫令牌的的前端了,我們有更新器可以更新令牌,獲得更新令牌后把它傳到OAuth服務(wù)提供商,然后寫一個新的訪問令牌。
前面說過我們的一些輔助邏輯耗盡了CPU資源,現(xiàn)在我們將這些輔助資源移到另一個集群上。
輔助邏輯主要和地址簿有關(guān),給定一個用戶令牌,就會有一個對應(yīng)的地址簿,地址簿上的數(shù)據(jù)量和令牌一樣,為了不耗盡一臺機器上的CPU資源,我們顯然需要一個與副本相同的集群,只需要加一堆更新地址簿的更新器就可以了(這個任務(wù)比較少見,因此地址簿不會和令牌一起更新)。
***,通過整合這兩個集群,我們得到一個相對簡單的完整結(jié)構(gòu):
令牌更新隊列
為什么我們本可以使用標(biāo)準(zhǔn)隊列卻還要用自己的隊列呢?這和我們的令牌更新模型有關(guān)。令牌一旦發(fā)布,有效期就是一個小時,當(dāng)令牌快要到期時,需要進(jìn)行更新,而令牌更新必須在某個特定的時間點之前完成。
假設(shè)系統(tǒng)中斷了,但是我們有一堆已到期的令牌,而在我們更新這些令牌的同時,又有其他令牌陸續(xù)到期,雖然我們***肯定能全部更新完,但是如果我們先更新那些即將到期的(60秒內(nèi)),再用剩下的資源去更新已經(jīng)到期的,是不是會更合理一些?(優(yōu)先級別***的是還有4-5分鐘才到期的令牌)
用第三方軟件來實現(xiàn)這個邏輯并不是件容易的事,然而,對于Tarantool來說卻不費吹灰之力??匆粋€簡單的方案:在Tarantool中有一個存儲數(shù)據(jù)的元組,這個元組的一些ID設(shè)置了基礎(chǔ)key值,為了得到我們需要的隊列,我們只需要添加兩個字段:status(隊列令牌狀態(tài))和time(到期時間或其他預(yù)定義時間)。
現(xiàn)在我們考慮一下隊列的兩個主要功能—put和take。put就是寫入新數(shù)據(jù)。給定一些負(fù)載,put時自己設(shè)置好status和time,然后寫數(shù)據(jù),這就是建立一個新的元組。
至于take,是指建立一個基于索引的迭代器,挑出那些等待解決的任務(wù)(處于就緒狀態(tài)的任務(wù)),然后核查一下是不是該接收這些任務(wù)了,或者這些任務(wù)是否已經(jīng)到期了。如果沒有任務(wù),take就切換到wait模式。除了內(nèi)置Lua,Tarantool還有一些所謂的通道,這些通道本質(zhì)上是互聯(lián)光纖同步原語。任何光纖都可以建立一個通道然后說“我在這等著”,剩下的其他光纖可以喚醒這個通道然后給它發(fā)送信息。
等待中的函數(shù)(等待發(fā)布任務(wù)、等待指定時間或其他)建立一個通道,給通道貼上適當(dāng)?shù)臉?biāo)簽,將通道放置在某個地方,然后進(jìn)行監(jiān)聽。如果我們收到一個緊急的更新令牌,put會給通道發(fā)出通知,然后take接收更新任務(wù)。
Tarantool有一個特殊的功能:如果一個令牌被意外發(fā)布,或者一個更新令牌被take接收,或者只是出現(xiàn)接收任務(wù)的現(xiàn)象,以上三種情況Tarantool都可以跟蹤到客戶端中斷。我們將每一個連接與指定給該連接的任務(wù)聯(lián)系起來,并將這些映射關(guān)系保持在會話保存中。假設(shè)由于網(wǎng)絡(luò)中斷導(dǎo)致更新過程失敗,而且我們不知道這個令牌是否會被更新并被寫回到數(shù)據(jù)庫。于是,客戶端發(fā)生中斷了,搜索與失敗過程相關(guān)的所有任務(wù)的會話保存,然后自動將它們釋放。隨后,任意已發(fā)布的任務(wù)都可以用同一個通道給另一個put發(fā)送信息,該put會快速接收和執(zhí)行任務(wù)。
實際上,具體實施方案并不需要太多代碼:
- function put(data)
- local t = box.space.queue:auto_increment({
- ‘r’, -- [[ status ]]
- util.time(), -- [[ time ]]
- data -- [[ any payload ]]
- })
- return t
- end
- function take(timeout)
- local start_time = util.time()
- local q_ind = box.space.tokens.index.queue
- local _,t
- while true do
- local it = util.iter(q_ind, {‘r’}, {iterator = box.index.GE})
- _,t = it()
- if t and t[F.tokens.status] ~= ‘t’ then
- break
- end
- local left = (start_time + timeout) — util.time()
- if left <= 0 then return end
- t = q:wait(left)
- if t then break end
- end
- t = q:taken(t)
- return t
- end
- function queue:taken(task)
- local sid = box.session.id()
- if self._consumers[sid] == nil then
- self._consumers[sid] = {}
- end
- local k = task[self.f_id]
- local t = self:set_status(k, ‘t’)
- self._consumers[sid][k] = {util.time(), box.session.peer(sid), t}
- self._taken[k] = sid
- return t
- end
- function on_disconnect()
- local sid = box.session.id
- local now = util.time()
- if self._consumers[sid] then
- local consumers = self._consumers[sid]
- for k, rec in pairs(consumers) do
- time, peer, task = unpack(rec)
- local v = box.space[self.space].index[self.index_primary]:get({k})
- if v and v[self.f_status] == ‘t’ then
- v = self:release(v[self.f_id])
- end
- end
- self._consumers[sid] = nil
- end
- end
Put只是接收用戶想要插入隊列的所有數(shù)據(jù),并將其寫入某個空間,如果是一個簡單的索引式FIFO隊列,設(shè)置好狀態(tài)和當(dāng)前時間,然后返回該任務(wù)。
接下來要和take有點關(guān)系了,但仍然比較簡單。我們建立一個迭代器,等待接收新任務(wù)。Taken函數(shù)只需要將任務(wù)標(biāo)記成“已接收”,但有一點很重要,taken函數(shù)還能記住哪個任務(wù)是由哪個進(jìn)程接收的。On_disconnect函數(shù)可以發(fā)布某個特定連接,或者發(fā)布由某個特定用戶接收的所有任務(wù)。
是否有可選方案 ?
當(dāng)然有。我們本可以使用任意數(shù)據(jù)庫,但是,不管我們選用什么數(shù)據(jù)庫,我們都要建立一個隊列用來處理外部系統(tǒng)、處理更新等等問題。我們不能僅僅按需更新令牌,因為那樣會產(chǎn)生不可預(yù)估的工作量,不管怎樣,我們需要保持我們的系統(tǒng)充滿活力,但是那樣,我們就要將延期的任務(wù)也插入隊列,并且保證數(shù)據(jù)庫和隊列之間的一致性,我們還要被迫使用一個quorum的容錯隊列。此外,如果我們把數(shù)據(jù)同時放在RAM和一個(考慮到工作量)可能要放入內(nèi)存的隊列中,那么我們就要消耗更多資源。
在我們的方案中,數(shù)據(jù)庫存儲令牌,隊列邏輯只需要占用7個字節(jié)(每個元組只需要7個額外的字節(jié),就可以搞定隊列邏輯!),如果使用其他的隊列形式,需要占用的空間就多得多了,大概是內(nèi)存容量的兩倍。
總結(jié)
首先,我們解決了連接中斷的問題,這個問題十分常見,使用上述的系統(tǒng)讓我們擺脫了這個困擾。
分片幫助我們擴展內(nèi)存,然后,我們將連接數(shù)從二次方減少到了線性,優(yōu)化了業(yè)務(wù)任務(wù)的隊列邏輯:如果發(fā)生延期,更新我們所能更新的一切令牌,這些延期并非都是我們的故障引起的,有可能是Google、Microsoft或者其他服務(wù)端對OAuth服務(wù)提供商進(jìn)行改造,然后導(dǎo)致我們這邊出現(xiàn)大量的未更新的令牌。
去數(shù)據(jù)庫內(nèi)部運算吧,走近數(shù)據(jù),你將擁有便利、高效、可擴展和靈活的運算體驗!