圖1:開發(fā)語(yǔ)言
由于PHP的單線程模型,我們把耗時(shí)較久的運(yùn)算和I/O操作從HTTP請(qǐng)求周期中分離出來, 交給由Python實(shí)現(xiàn)的任務(wù)進(jìn)程來完成,以保證請(qǐng)求響應(yīng)速度。這些任務(wù)主要包括:郵件發(fā)送、數(shù)據(jù)索引、數(shù)據(jù)聚合和好友動(dòng)態(tài)推送(稍候會(huì)有介紹)等等。通常這些任務(wù)由用戶觸發(fā),并且,用戶的一個(gè)行為可能會(huì)觸發(fā)多種任務(wù)的執(zhí)行。 比如,用戶上傳了一張新的照片,我們需要更新索引,也需要向他的朋友推送一條新的動(dòng)態(tài)。PHP通過消息隊(duì)列(我們用的是RabbitMQ)來觸發(fā)任務(wù)執(zhí)行。
圖2:PHP和Python的協(xié)作
數(shù)據(jù)庫(kù)一向是網(wǎng)站架構(gòu)中最具挑戰(zhàn)性的,瓶頸通常出現(xiàn)在這里。又拍網(wǎng)的照片數(shù)據(jù)量很大,數(shù)據(jù)庫(kù)也幾度出現(xiàn)嚴(yán)重的壓力問題。 因此,這里我主要介紹一下又拍網(wǎng)在分庫(kù)設(shè)計(jì)這方面的一些嘗試。
和很多使用MySQL的2.0站點(diǎn)一樣,又拍網(wǎng)的MySQL集群經(jīng)歷了從最初的一個(gè)主庫(kù)一個(gè)從庫(kù)、到一個(gè)主庫(kù)多個(gè)從庫(kù)、 然后到多個(gè)主庫(kù)多個(gè)從庫(kù)的一個(gè)發(fā)展過程。
最初是由一臺(tái)主庫(kù)和一臺(tái)從庫(kù)組成,當(dāng)時(shí)從庫(kù)只用作備份和容災(zāi),當(dāng)主庫(kù)出現(xiàn)故障時(shí),從庫(kù)就手動(dòng)變成主庫(kù),一般情況下,從庫(kù)不作讀寫操作(同步除外)。隨著壓力的增加,我們加上了memcached,當(dāng)時(shí)只用其緩存單行數(shù)據(jù)。 但是,單行數(shù)據(jù)的緩存并不能很好地解決壓力問題,因?yàn)閱涡袛?shù)據(jù)的查詢通常很快。所以我們把一些實(shí)時(shí)性要求不高的Query放到從庫(kù)去執(zhí)行。后面又通過添加多個(gè)從庫(kù)來分流查詢壓力,不過隨著數(shù)據(jù)量的增加,主庫(kù)的寫壓力也越來越大。
在參考了一些相關(guān)產(chǎn)品和其它網(wǎng)站的做法后,我們決定進(jìn)行數(shù)據(jù)庫(kù)拆分。也就是將數(shù)據(jù)存放到不同的數(shù)據(jù)庫(kù)服務(wù)器中,一般可以按兩個(gè)緯度來拆分?jǐn)?shù)據(jù):
垂直拆分:是指按功能模塊拆分,比如可以將群組相關(guān)表和照片相關(guān)表存放在不同的數(shù)據(jù)庫(kù)中,這種方式多個(gè)數(shù)據(jù)庫(kù)之間的表結(jié)構(gòu)不同。
水平拆分:而水平拆分是將同一個(gè)表的數(shù)據(jù)進(jìn)行分塊保存到不同的數(shù)據(jù)庫(kù)中,這些數(shù)據(jù)庫(kù)中的表結(jié)構(gòu)完全相同。
一般都會(huì)先進(jìn)行垂直拆分,因?yàn)檫@種方式拆分方式實(shí)現(xiàn)起來比較簡(jiǎn)單,根據(jù)表名訪問不同的數(shù)據(jù)庫(kù)就可以了。但是垂直拆分方式并不能徹底解決所有壓力問題,另外,也要看應(yīng)用類型是否合適這種拆分方式。如果合適的話,也能很好的起到分散數(shù)據(jù)庫(kù)壓力的作用。比如對(duì)于豆瓣我覺得比較適合采用垂直拆分, 因?yàn)?a >豆瓣的各核心業(yè)務(wù)/模塊(書籍、電影、音樂)相對(duì)獨(dú)立,數(shù)據(jù)的增加速度也比較平穩(wěn)。不同的是,又拍網(wǎng)的核心業(yè)務(wù)對(duì)象是用戶上傳的照片,而照片數(shù)據(jù)的增加速度隨著用戶量的增加越來越快。壓力基本上都在照片表上,顯然垂直拆分并不能從根本上解決我們的問題,所以,我們采用水平拆分的方式。
水平拆分實(shí)現(xiàn)起來相對(duì)復(fù)雜,我們要先確定一個(gè)拆分規(guī)則,也就是按什么條件將數(shù)據(jù)進(jìn)行切分。 一般2.0網(wǎng)站都以用戶為中心,數(shù)據(jù)基本都跟隨用戶,比如用戶的照片、朋友和評(píng)論等等。因此一個(gè)比較自然的選擇是根據(jù)用戶來切分。每個(gè)用戶都對(duì)應(yīng)一個(gè)數(shù)據(jù)庫(kù),訪問某個(gè)用戶的數(shù)據(jù)時(shí), 我們要先確定他/她所對(duì)應(yīng)的數(shù)據(jù)庫(kù),然后連接到該數(shù)據(jù)庫(kù)進(jìn)行實(shí)際的數(shù)據(jù)讀寫。
那么,怎么樣對(duì)應(yīng)用戶和數(shù)據(jù)庫(kù)呢?我們有這些選擇:
按算法對(duì)應(yīng)
最簡(jiǎn)單的算法是按用戶ID的奇偶性來對(duì)應(yīng),將奇數(shù)ID的用戶對(duì)應(yīng)到數(shù)據(jù)庫(kù)A,而偶數(shù)ID的用戶則對(duì)應(yīng)到數(shù)據(jù)庫(kù)B。這個(gè)方法的最大問題是,只能分成兩個(gè)庫(kù)。另一個(gè)算法是按用戶ID所在區(qū)間對(duì)應(yīng),比如ID在0-10000之間的用戶對(duì)應(yīng)到數(shù)據(jù)庫(kù)A, ID在10000-20000這個(gè)范圍的對(duì)應(yīng)到數(shù)據(jù)庫(kù)B,以此類推。按算法分實(shí)現(xiàn)起來比較方便,也比較高效,但是不能滿足后續(xù)的伸縮性要求,如果需要增加數(shù)據(jù)庫(kù)節(jié)點(diǎn),必需調(diào)整算法或移動(dòng)很大的數(shù)據(jù)集, 比較難做到在不停止服務(wù)的前提下進(jìn)行擴(kuò)充數(shù)據(jù)庫(kù)節(jié)點(diǎn)。
按索引/映射表對(duì)應(yīng)
這種方法是指建立一個(gè)索引表,保存每個(gè)用戶的ID和數(shù)據(jù)庫(kù)ID的對(duì)應(yīng)關(guān)系,每次讀寫用戶數(shù)據(jù)時(shí)先從這個(gè)表獲取對(duì)應(yīng)數(shù)據(jù)庫(kù)。新用戶注冊(cè)后,在所有可用的數(shù)據(jù)庫(kù)中隨機(jī)挑選一個(gè)為其建立索引。這種方法比較靈活,有很好的伸縮性。一個(gè)缺點(diǎn)是增加了一次數(shù)據(jù)庫(kù)訪問,所以性能上沒有按算法對(duì)應(yīng)好。
比較之后,我們采用的是索引表的方式,我們?cè)敢鉃槠潇`活性損失一些性能,更何況我們還有memcached, 因?yàn)樗饕龜?shù)據(jù)基本不會(huì)改變的緣故,緩存命中率非常高。所以能很大程度上減少了性能損失。
圖4:數(shù)據(jù)訪問過程
索引表的方式能夠比較方便地添加數(shù)據(jù)庫(kù)節(jié)點(diǎn),在增加節(jié)點(diǎn)時(shí),只要將其添加到可用數(shù)據(jù)庫(kù)列表里即可。 當(dāng)然如果需要平衡各個(gè)節(jié)點(diǎn)的壓力的話,還是需要進(jìn)行數(shù)據(jù)的遷移,但是這個(gè)時(shí)候的遷移是少量的,可以逐步進(jìn)行。要遷移用戶A的數(shù)據(jù),首先要將其狀態(tài)置為遷移數(shù)據(jù)中,這個(gè)狀態(tài)的用戶不能進(jìn)行寫操作,并在頁(yè)面上進(jìn)行提示。 然后將用戶A的數(shù)據(jù)全部復(fù)制到新增加的節(jié)點(diǎn)上后,更新映射表,然后將用戶A的狀態(tài)置為正常,最后將原來對(duì)應(yīng)的數(shù)據(jù)庫(kù)上的數(shù)據(jù)刪除。這個(gè)過程通常會(huì)在臨晨進(jìn)行,所以,所以很少會(huì)有用戶碰到遷移數(shù)據(jù)中的情況。
當(dāng)然,有些數(shù)據(jù)是不屬于某個(gè)用戶的,比如系統(tǒng)消息、配置等等,我們把這些數(shù)據(jù)保存在一個(gè)全局庫(kù)中。
分庫(kù)會(huì)給你在應(yīng)用的開發(fā)和部署上都帶來很多麻煩。
不能執(zhí)行跨庫(kù)的關(guān)聯(lián)查詢
如果我們需要查詢的數(shù)據(jù)分布于不同的數(shù)據(jù)庫(kù),我們沒辦法通過JOIN的方式查詢獲得。比如要獲得好友的最新照片,你不能保證所有好友的數(shù)據(jù)都在同一個(gè)數(shù)據(jù)庫(kù)里。一個(gè)解決辦法是通過多次查詢,再進(jìn)行聚合的方式。我們需要盡量避免類似的需求。有些需求可以通過保存多份數(shù)據(jù)來解決,比如User-A和 User-B的數(shù)據(jù)庫(kù)分別是DB-1和DB-2, 當(dāng)User-A評(píng)論了User-B的照片時(shí),我們會(huì)同時(shí)在DB-1和DB-2中保存這條評(píng)論信息,我們首先在DB-2中的photo_comments表中插入一條新的記錄,然后在DB-1中的user_comments表中插入一條新的記錄。這兩個(gè)表的結(jié)構(gòu)如下圖所示。這樣我們可以通過查詢 photo_comments表得到User-B的某張照片的所有評(píng)論, 也可以通過查詢user_comments表獲得User-A的所有評(píng)論。另外可以考慮使用全文檢索工具來解決某些需求, 我們使用Solr來提供全站標(biāo)簽檢索和照片搜索服務(wù)。
圖5:評(píng)論表結(jié)構(gòu)
不能保證數(shù)據(jù)的一致/完整性
跨庫(kù)的數(shù)據(jù)沒有外鍵約束,也沒有事務(wù)保證。比如上面的評(píng)論照片的例子, 很可能出現(xiàn)成功插入photo_comments表,但是插入user_comments表時(shí)卻出錯(cuò)了。一個(gè)辦法是在兩個(gè)庫(kù)上都開啟事務(wù),然后先插入 photo_comments,再插入user_comments, 然后提交兩個(gè)事務(wù)。這個(gè)辦法也不能完全保證這個(gè)操作的原子性。
所有查詢必須提供數(shù)據(jù)庫(kù)線索
比如要查看一張照片,僅憑一個(gè)照片ID是不夠的,還必須提供上傳這張照片的用戶的ID(也就是數(shù)據(jù)庫(kù)線索),才能找到它實(shí)際的存放位置。因此,我們必須重新設(shè)計(jì)很多URL地址,而有些老的地址我們又必須保證其仍然有效。我們把照片地址改成/photos/{username}/{photo_id} /的形式,然后對(duì)于系統(tǒng)升級(jí)前上傳的照片ID, 我們又增加一張映射表,保存photo_id和user_id的對(duì)應(yīng)關(guān)系。當(dāng)訪問老的照片地址時(shí),我們通過查詢這張表獲得用戶信息, 然后再重定向到新的地址。
自增ID
如果要在節(jié)點(diǎn)數(shù)據(jù)庫(kù)上使用自增字段,那么我們就不能保證全局唯一。這倒不是很嚴(yán)重的問題,但是當(dāng)節(jié)點(diǎn)之間的數(shù)據(jù)發(fā)生關(guān)系時(shí),就會(huì)使得問題變得比較麻煩。我們可以再來看看上面提到的評(píng)論的例子。如果photo_comments表中的comment_id的自增字段,當(dāng)我們?cè)贒B- 2.photo_comments表插入新的評(píng)論時(shí), 得到一個(gè)新的comment_id,假如值為101,而User-A的ID為1,那么我們還需要在DB-1.user_comments表中插入(1, 101 ...)。 User-A是個(gè)很活躍的用戶,他又評(píng)論了User-C的照片,而User-C的數(shù)據(jù)庫(kù)是DB-3。 很巧的是這條新評(píng)論的ID也是101,這種情況很用可能發(fā)生。那么我們又在DB-1.user_comments表中插入一行像這樣(1, 101 ...)的數(shù)據(jù)。 那么我們要怎么設(shè)置user_comments表的主鍵呢(標(biāo)識(shí)一行數(shù)據(jù))?可以不設(shè)啊,不幸的是有的時(shí)候(框架、緩存等原因)必需設(shè)置。那么可以以 user_id、 comment_id和photo_id為組合主鍵,但是photo_id也有可能一樣(的確很巧)。看來只能再加上photo_owner_id了, 但是這個(gè)結(jié)果又讓我們實(shí)在有點(diǎn)無法接受,太復(fù)雜的組合鍵在寫入時(shí)會(huì)帶來一定的性能影響,這樣的自然鍵看起來也很不自然。所以,我們放棄了在節(jié)點(diǎn)上使用自增字段,想辦法讓這些ID變成全局唯一。為此增加了一個(gè)專門用來生成ID的數(shù)據(jù)庫(kù),這個(gè)庫(kù)中的表結(jié)構(gòu)都很簡(jiǎn)單,只有一個(gè)自增字段id。 當(dāng)我們要插入新的評(píng)論時(shí),我們先在ID庫(kù)的photo_comments表里插入一條空的記錄,以獲得一個(gè)唯一的評(píng)論ID。 當(dāng)然這些邏輯都已經(jīng)封裝在我們的框架里了,對(duì)于開發(fā)人員是透明的。 為什么不用其它方案呢,比如一些支持incr操作的Key-Value數(shù)據(jù)庫(kù)。我們還是比較放心把數(shù)據(jù)放在MySQL里。 另外,我們會(huì)定期清理ID庫(kù)的數(shù)據(jù),以保證獲取新ID的效率。
我們稱前面提到的一個(gè)數(shù)據(jù)庫(kù)節(jié)點(diǎn)為Shard,一個(gè)Shard由兩個(gè)臺(tái)物理服務(wù)器組成, 我們稱它們?yōu)镹ode-A和Node-B,Node-A和Node-B之間是配置成Master-Master相互復(fù)制的。 雖然是Master-Master的部署方式,但是同一時(shí)間我們還是只使用其中一個(gè),原因是復(fù)制的延遲問題, 當(dāng)然在Web應(yīng)用里,我們可以在用戶會(huì)話里放置一個(gè)A或B來保證同一用戶一次會(huì)話里只訪問一個(gè)數(shù)據(jù)庫(kù), 這樣可以避免一些延遲問題。但是我們的Python任務(wù)是沒有任何狀態(tài)的,不能保證和PHP應(yīng)用讀寫相同的數(shù)據(jù)庫(kù)。那么為什么不配置成Master-Slave呢?我們覺得只用一臺(tái)太浪費(fèi)了,所以我們?cè)诿颗_(tái)服務(wù)器上都創(chuàng)建多個(gè)邏輯數(shù)據(jù)庫(kù)。 如下圖所示,在Node-A和Node-B上我們都建立了shard_001和shard_002兩個(gè)邏輯數(shù)據(jù)庫(kù), Node-A上的shard_001和Node-B上的shard_001組成一個(gè)Shard,而同一時(shí)間只有一個(gè)邏輯數(shù)據(jù)庫(kù)處于Active狀態(tài)。 這個(gè)時(shí)候如果需要訪問Shard-001的數(shù)據(jù)時(shí),我們連接的是Node-A上的shard_001, 而訪問Shard-002的數(shù)據(jù)則是連接Node-B上的shard_002。以這種交叉的方式將壓力分散到每臺(tái)物理服務(wù)器上。 以Master-Master方式部署的另一個(gè)好處是,我們可以不停止服務(wù)的情況下進(jìn)行表結(jié)構(gòu)升級(jí), 升級(jí)前先停止復(fù)制,升級(jí)Inactive的庫(kù),然后升級(jí)應(yīng)用,再將已經(jīng)升級(jí)好的數(shù)據(jù)庫(kù)切換成Active狀態(tài), 原來的Active數(shù)據(jù)庫(kù)切換成Inactive狀態(tài),然后升級(jí)它的表結(jié)構(gòu),最后恢復(fù)復(fù)制。 當(dāng)然這個(gè)步驟不一定適合所有升級(jí)過程,如果表結(jié)構(gòu)的更改會(huì)導(dǎo)致數(shù)據(jù)復(fù)制失敗,那么還是需要停止服務(wù)再升級(jí)的。
圖6:數(shù)據(jù)庫(kù)布局
前面提到過添加服務(wù)器時(shí),為了保證負(fù)載的平衡,我們需要遷移一部分?jǐn)?shù)據(jù)到新的服務(wù)器上。為了避免短期內(nèi)遷移的必要,我們?cè)趯?shí)際部署的時(shí)候,每臺(tái)機(jī)器上部署了8個(gè)邏輯數(shù)據(jù)庫(kù), 添加服務(wù)器后,我們只要將這些邏輯數(shù)據(jù)庫(kù)遷移到新服務(wù)器就可以了。最好是每次添加一倍的服務(wù)器, 然后將每臺(tái)的1/2邏輯數(shù)據(jù)遷移到一臺(tái)新服務(wù)器上,這樣能很好的平衡負(fù)載。當(dāng)然,最后到了每臺(tái)上只有一個(gè)邏輯庫(kù)時(shí),遷移就無法避免了,不過那應(yīng)該是比較久遠(yuǎn)的事情了。
我們把分庫(kù)邏輯都封裝在我們的PHP框架里了,開發(fā)人員基本上不需要被這些繁瑣的事情困擾。下面是使用我們的框架進(jìn)行照片數(shù)據(jù)的讀寫的一些例子:
<?php
$Photos = new ShardedDBTable('Photos', 'yp_photos', 'user_id', array(
'photo_id' => array('type' => 'long', 'primary' => true, 'global_auto_increment' => true),
'user_id' => array('type' => 'long'),
'title' => array('type' => 'string'),
'posted_date' => array('type' => 'date'),
));
$photo = $Photos->new_object(array('user_id' => 1, 'title' => 'Workforme'));
$photo->insert();
// 加載ID為10001的照片,注意第一個(gè)參數(shù)為用戶ID
$photo = $Photos->load(1, 10001);
// 更改照片屬性
$photo->title = 'Database Sharding';
$photo->update();
// 刪除照片
$photo->delete();
// 獲取ID為1的用戶在2010-06-01之后上傳的照片
$photos = $Photos->fetch(array('user_id' => 1, 'posted_date__gt' => '2010-06-01'));
?>
首先要定義一個(gè)ShardedDBTable對(duì)象,所有的API都是通過這個(gè)對(duì)象開放。第一個(gè)參數(shù)是對(duì)象類型名稱, 如果這個(gè)名稱已經(jīng)存在,那么將返回之前定義的對(duì)象。你也可以通過get_table('Photos')這個(gè)函數(shù)來獲取之前定義的Table對(duì)象。 第二個(gè)參數(shù)是對(duì)應(yīng)的數(shù)據(jù)庫(kù)表名,而第三個(gè)參數(shù)是數(shù)據(jù)庫(kù)線索字段,你會(huì)發(fā)現(xiàn)在后面的所有API中全部需要指定這個(gè)字段的值。 第四個(gè)參數(shù)是字段定義,其中photo_id字段的global_auto_increment屬性被置為true,這就是前面所說的全局自增ID, 只要指定了這個(gè)屬性,框架會(huì)處理好ID的事情。
如果我們要訪問全局庫(kù)中的數(shù)據(jù),我們需要定義一個(gè)DBTable對(duì)象。
<?php
$Users = new DBTable('Users', 'yp_users', array(
'user_id' => array('type' => 'long', 'primary' => true, 'auto_increment' => true),
'username' => array('type' => 'string'),
));
?>
DBTable是ShardedDBTable的父類,除了定義時(shí)參數(shù)有些不同(DBTable不需要指定數(shù)據(jù)庫(kù)線索字段),它們提供一樣的 API。
我們的框架提供了緩存功能,對(duì)開發(fā)人員是透明的。
<?php
$photo = $Photos->load(1, 10001);
?>
比如上面的方法調(diào)用,框架先嘗試以Photos-1-10001為Key在緩存中查找,未找到的話再執(zhí)行數(shù)據(jù)庫(kù)查詢并放入緩存。當(dāng)更改照片屬性或刪除照片時(shí),框架負(fù)責(zé)從緩存中刪除該照片。這種單個(gè)對(duì)象的緩存實(shí)現(xiàn)起來比較簡(jiǎn)單。稍微麻煩的是像下面這樣的列表查詢結(jié)果的緩存。
<?php
$photos = $Photos->fetch(array('user_id' => 1, 'posted_date__gt' => '2010-06-01'));
?>
我們把這個(gè)查詢分成兩步,第一步先查出符合條件的照片ID,然后再根據(jù)照片ID分別查找具體的照片信息。 這么做可以更好的利用緩存。第一個(gè)查詢的緩存Key為Photos-list-{shard_key}-{md5(查詢條件SQL語(yǔ)句)}, Value是照片ID列表(逗號(hào)間隔)。其中shard_key為user_id的值1。目前來看,列表緩存也不麻煩。 但是如果用戶修改了某張照片的上傳時(shí)間呢,這個(gè)時(shí)候緩存中的數(shù)據(jù)就不一定符合條件了。所以,我們需要一個(gè)機(jī)制來保證我們不會(huì)從緩存中得到過期的列表數(shù)據(jù)。我們?yōu)槊繌埍碓O(shè)置了一個(gè)revision,當(dāng)該表的數(shù)據(jù)發(fā)生變化時(shí)(調(diào)用insert/update/delete方法), 我們就更新它的revision,所以我們把列表的緩存Key改為Photos-list-{shard_key}-{md5(查詢條件SQL語(yǔ)句)}-{revision}, 這樣我們就不會(huì)再得到過期列表了。
revision信息也是存放在緩存里的,Key為Photos-revision。這樣做看起來不錯(cuò),但是好像列表緩存的利用率不會(huì)太高。因?yàn)槲覀兪且哉麄€(gè)數(shù)據(jù)類型的revision為緩存Key的后綴,顯然這個(gè)revision更新的非常頻繁,任何一個(gè)用戶修改或上傳了照片都會(huì)導(dǎo)致它的更新,哪怕那個(gè)用戶根本不在我們要查詢的Shard里。要隔離用戶的動(dòng)作對(duì)其他用戶的影響,我們可以通過縮小revision的作用范圍來達(dá)到這個(gè)目的。 所以revision的緩存Key變成Photos-{shard_key}-revision,這樣的話當(dāng)ID為1的用戶修改了他的照片信息時(shí), 只會(huì)更新Photos-1-revision這個(gè)Key所對(duì)應(yīng)的revision。
因?yàn)槿謳?kù)沒有shard_key,所以修改了全局庫(kù)中的表的一行數(shù)據(jù),還是會(huì)導(dǎo)致整個(gè)表的緩存失效。 但是大部分情況下,數(shù)據(jù)都是有區(qū)域范圍的,比如我們的幫助論壇的主題帖子, 帖子屬于主題。修改了其中一個(gè)主題的一個(gè)帖子,沒必要使所有主題的帖子緩存都失效。 所以我們?cè)贒BTable上增加了一個(gè)叫isolate_key的屬性。
<?php
$GLOBALS['Posts'] = new DBTable('Posts', 'yp_posts', array(
'topic_id' => array('type' => 'long', 'primary' => true),
'post_id' => array('type' => 'long', 'primary' => true, 'auto_increment' => true),
'author_id' => array('type' => 'long'),
'content' => array('type' => 'string'),
'posted_at' => array('type' => 'datetime'),
'modified_at' => array('type' => 'datetime'),
'modified_by' => array('type' => 'long'),
), 'topic_id');
?>
注意構(gòu)造函數(shù)的最后一個(gè)參數(shù)topic_id就是指以字段topic_id作為isolate_key,它的作用和shard_key一樣用于隔離 revision的作用范圍。
ShardedDBTable繼承自DBTable,所以也可以指定isolate_key。 ShardedDBTable指定了isolate_key的話,能夠更大幅度縮小revision的作用范圍。 比如相冊(cè)和照片的關(guān)聯(lián)表yp_album_photos,當(dāng)用戶往他的其中一個(gè)相冊(cè)里添加了新的照片時(shí), 會(huì)導(dǎo)致其它相冊(cè)的照片列表緩存也失效。如果我指定這張表的isolate_key為album_id的話, 我們就把這種影響限制在了本相冊(cè)內(nèi)。
我們的緩存分為兩級(jí),第一級(jí)只是一個(gè)PHP數(shù)組,有效范圍是Request。而第二級(jí)是memcached。這么做的原因是,很多數(shù)據(jù)在一個(gè) Request周期內(nèi)需要加載多次,這樣可以減少memcached的網(wǎng)絡(luò)請(qǐng)求。另外我們的框架也會(huì)盡可能的發(fā)送memcached的gets命令來獲取數(shù)據(jù), 從而減少網(wǎng)絡(luò)請(qǐng)求。
這個(gè)架構(gòu)使得我們?cè)诤荛L(zhǎng)一段時(shí)間內(nèi)都不必再為數(shù)據(jù)庫(kù)壓力所困擾。我們的設(shè)計(jì)很多地方參考了netlog和flickr的實(shí)現(xiàn),因此非常感謝他們將一些實(shí)現(xiàn)細(xì)節(jié)發(fā)布出來。
聯(lián)系客服