說說數據庫架構,ORM緩存和路由

爲何在ORM層作緩存,而不是DB層

ORM能有效地提升程序員的開發效率,程序員更喜歡操做對象而不是數據庫,他們不關心也不想手寫一堆SQL語句,畢竟一個公司裏普通程序員要佔多數,他們並非很是熟悉數據庫,寫出來的SQL執行效率也確定會有這樣那樣的問題。php

若是讓程序員去操做對象,這就是他們的強項了:定義關係、使用ORM的方法和屬性、獲取/遍歷結果等等。同時ORM又能夠在內部對SQL語句及對象之間的關係進行優化,儘可能保證SQL高效地執行,甚至能夠透明地加個緩存。這樣一個共贏的結果,何樂而不爲呢。程序員

若是是一些比較複雜的查詢語句,只能經過寫SQL語句來實現,這樣的話,能夠在語句的執行段外面套一層緩存判斷,如:web

<?php
$result = $memcache->get('isobamapresident'); // fetch
if ($result === false)
{
	// do some database heavy stuff
	$db = DB::getInstance();
	$votes = $db->prepare( "SELECT COUNT(*) FROM VOTES WHERE vote = 'OBAMA'" )->execute();
	$result = ($votes > (USA_CITIZEN_COUNT / 2)) ? 'Sure is!' : 'Nope.'; // well, ideally
	$memcache->set('isobamapresident', $result, 0);
}

透明地插入緩存

所謂透明緩存,就是用戶正常使用ORM,獲取ORM的查詢結果。而事實上ORM的結果集極可能是來自緩存而不是數據庫。算法

<?php
//獲取1小時前發佈的文章
$time = time() - 86400;
ORM::factory('article')->where('created', '>', $time)->findAll();

//正常的結果是經過執行如下SQL語句返回的
//SELECT * FROM article WHERE created > $time

//但實際上多是從Memcache中讀取的結果
$memcache = new Memcache();
$memcache->connect('memcache_host', 11211);
$memcache->get('some_key');

這樣一來,php代碼不用改變,但由於是從緩存中讀取,因此數據的獲取速度有保障,同時也減輕了數據庫的壓力,又是一個共贏的局面。sql

固然願望是美好的,現實是殘酷的,若是要達到上面所說的效果,須要費很多周折。數據庫

數據庫架構

在設計ORM的緩存前,先了解如下數據庫的大體架構。以netlog的數據庫架構變遷爲例:緩存

單數據庫 服務器

主庫+從庫 架構

保持主庫+從庫的架構,把讀寫最頻繁的幾個表分到單獨的數據庫服務器 分佈式

把那幾個讀寫最頻繁的表也分紅主從

出現了1040 too many connections

Sharding(水平分區)

數據庫服務器/數據庫/分區

這樣基本上就能夠應付正常的訪問了,若是哪一個表數據量過大或鏈接過多,就Sharding一下。但隨之而來的問題也很明顯,好比:

<?php
//沒有分區以前,能夠經過下面幾段代碼來獲取數據
$db = DB::getInstance();
$db->prepare("SELECT title, message FROM BLOG_MESSAGES WHERE userid = {userID}");
$db->assignInt('userID', $userID);
$db->execute(); 
$results = $db->getResults();

//假設將BLOG_MESSAGES按照用戶id分配到了不一樣的分區上,上面的代碼就須要作一些改動
//最簡單的就是在getInstance時把用戶的id傳過去,讓ORM內部去找分區,至關於路由
$db = DB::getInstance($userID);

如何對數據進行分片

當要對數據進行分片時,應該考慮這兩個問題:使用表的哪一列(sharding key)做爲分割的依據;使用怎樣的分割算法(sharding scheme)。使用哪一個key要看具體的應用。以博客爲例,若是想要現實每一個用戶的博客,那麼userID就能夠做爲sharding key。如何根據sharding key來找到對應的分區通常有三種方法:取模(求餘)、數據量、映射表。假設採用映射表的方法,若是要獲取用戶的博客,先要到映射表裏找到該userID對應的分區,再從分區中找到userID對應的博客列表。隨之而來的問題是:

不能執行跨分區查詢

若是要從不一樣的分區獲取數據,就不能經過JOIN/GROUP BY/ORDER BY/LIMIT來實現了。好比:

//獲取最新的10條博客
SELECT * FROM BLOG_MESSAGES ORDER BY created DESC LIMIT 0, 10;
//若是數據在多個分區中,上面這條查詢就失效了

要解決這個問題,最好從設計上就避免這些查詢語句。也能夠經過冗餘來實現。

數據一致性得不到保障

由於會在多個數據庫之間更新數據,若是要保證數據一致性,就要實現分佈式事務。

也能夠經過一個小技巧來模擬分佈式事務,好比有兩臺數據庫服務器,這時能夠先開啓一個事務,但只在保證兩臺服務器都正常的狀況下才一一提交事務。固然兩次事務的提交也會有延遲,但相對來講更加靠譜。

保持分區平衡

若是基於用戶ID進行分區,可能會出現分區之間的不平衡,好比一些活躍的用戶都被分到了同一分區,而沉默用戶被分到了另外一個分區,這時量販額分區的壓力明顯不同。因此分區的算法很重要。

備份策略

由於數據在不一樣的分區中,備份策略就不想之前那麼簡單了。

ORM的緩存實現

先聲明一下,ORM的緩存不能解決JOIN或者複雜的SQL查詢,其實若是考慮到未來會有分區的可能,就應該在設計表時避免JOIN語句。由於複雜的SQL相對來講佔的少數,甚至能夠對這些SQL單獨制定緩存策略。

先不考慮分區,假設有一個用戶表和博客表,要達到如下目標:

  • 緩存每一條博客記錄,更新博客時,更新緩存
  • 緩存每一個用戶的博客列表,用戶更新博客時,更新該列表
  • 程序員使用ORM時不須要考慮緩存

緩存行實現

緩存行仍是比較簡單的,用戶查詢某個id時,緩存該行內容,下次就能夠直接讀取緩存了。

若是內容被更新/刪除了,緩存也同時更新/刪除。

緩存列實現

<?php
//若是在find/findAll裏傳入了參數,則該參數即爲key
ORM::factory('article')->where('user_id', '=', '2')->and_where('created', '>', time() - 86400)->findAll(2);

//上面的代碼會在Model內部生成一個結構化的字符串,該字符串及對應的值將被放入緩存中
{table_name}-{key}-{md5(sql)}
//相似這樣
article-2-c81e728d9d4c2f636f067f89cc14862c
//若是沒有傳參數,{key}就不會被替代
article-{key}-c81e728d9d4c2f636f067f89cc14862c

//首次執行此代碼時,ORM內部會先去緩存中找上面的結構化字符串,沒有找到,就會執行SQL語句,而後把返回的結果的id放到緩存中
//這就是要放到緩存中的數據,下次若是再執行此SQL,直接從緩存中獲取id(1,43,50),而後再從緩存中獲取這些id對應的行內容
//注意到這裏有個revision,這是未來要判斷該緩存是否已過時的關鍵。
'article-2-c81e728d9d4c2f636f067f89cc14862c' => array(
	'revision' => 1294476790,
	'data' => [1, 43, 50],
);

//同時還會生成另外一組數據,就是revision
'article-2-revision' => 1294476777,

//若是做者又更新了一篇博客,則上面的查詢語句結果就發生了變化。
ORM::factory('article')->values(array(...))->save(2);

//ORM會找到緩存中的一組revision數據,同時更新它
'article-2-revision' => 1294476888,

//若是沒有提供key,那就是
'article-{key}-revision' => 1294476888,

//下次再執行上面的ORM查詢代碼時,會先去查找'article-2-revision'的版本,而後跟'article-2-c81e728d9d4c2f636f067f89cc14862c'的版本號比較,若是前一個版本號>後一個版本號,表示數據有改變,緩存已過時,這時就須要從新執行SQL語句,並更新'article-2-c81e728d9d4c2f636f067f89cc14862c'這個字符串的版本號。若是比較結果是前一個版本號<=後一個版本號,那就直接從緩存中讀取。

ORM的路由

上面說的是數據沒有分區的狀況,若是數據被分區了的話,還要在ORM內部實現路由功能。

<?php
ORM::factory('articles')->where('created', '>', time()-86400)->findAll();

假設文章經過某種算法,被分在了不一樣的分區上,上面這個ORM編譯出來的SQL是沒法運行的。但又不能讓程序員來關心分庫分表的事,這時就能夠在ORM內部實現路由機制,在具體的Model層實現路由算法。

<?php
class Model_Article extends ORM 
{
	protected function _route()
	{
		//這裏能夠實現具體算法,改變ORM的一些屬性,從而影響SQL的編譯
	}
}

參考:


--EOF--

相關文章
相關標籤/搜索