1. 程式人生 > >分散式快取:memcached(寫操作快取)

分散式快取:memcached(寫操作快取)

資料庫的前端快取區

檔案系統核心緩衝區,位於實體記憶體的核心地址空間,所有對磁碟檔案的讀寫操作都要經過它,也可以把它看作是磁碟的前端裝置。
這塊核心緩衝區實際上包括2個部分:讀快取區、寫快取區。

讀快取區中儲存著最近系統從磁碟上讀取的資料,一旦下次需要讀取這些資料的時候,核心將直接從這裡獲得,而不需訪問磁碟。
寫快取區的目的主要是為了減少磁碟的物理寫操作,核心緩區可以將多次寫操作指令累計起來,通過一次物理磁頭的移動來完成。當然,寫快取區導致資料真正寫入磁碟會產生幾秒的延遲,在實際寫入之前,這些資料被稱為Dirtry Page。

我們可以在資料庫動態內容之間建立一層快取區,它可以部署在獨立的伺服器上,用於加速資料庫讀寫的操作,這個快取區實際上的由動態內容來控制的。

memcached

key-value

為了實現快取記憶體,我們不會把快取內容放在磁碟上,memcached使用實體記憶體來作為快取區,當我們啟動memcached的時候,需要指定分配給快取區的記憶體大小。
比如我們分配4GB的記憶體:

memcached -d -m 4086 -l 192.168.88.88 -p 11211

如果說memcached最許需要什麼,毫無疑問,那就是記憶體。

memcached使用key-value的方式來儲存資料,我們將每個key以及對應的value合起來稱為資料項。
memecached使用了高效的基於key的hash演算法來設計儲存資料結構,並且使用了精心設計的記憶體分配器,這意味著不論你儲存了多少資料項,查詢任何資料項所花費的時間都不變。

資料項過期時間

由於緩衝區空間是有限的,一旦快取區沒有足夠的控制元件儲存新的資料項,memcached便會想辦法淘汰一些資料項來騰出空間,把最近不常訪問的資料項淘汰掉。

當然,我們更原因為資料項設定過期時間。如果你在使用PHP來編寫動態內容:

$mem = memcache_connect("192.168.88.88",11211);
$mem->add("item_key","item_value",false,30);

把item_key這個資料項的過期時間設定為30秒。

網路併發模型

memcached是分散式快取系統,可以執行在獨立的伺服器上,動態內容通過TCP Socket來訪問它。memcached使用libevent函式庫來實現網路併發模型,你可以在較大併發使用者數的環境下仍然方向使用memcached。

物件序列化

我們在網路中傳輸的是二進位制資料,那麼陣列或者物件這樣的抽象資料型別,是否可以存入memcached中呢?
答案是肯定的。因為基於序列化(Serialize)機制,我們可以把抽象資料型別轉換為二進位制字串,以便通過網路進入快取伺服器,同時在讀取這些資料的時候,二進位制字串又可以轉換回原有的資料型別。
這個“轉換”的過程比較複雜,但是在具有動態特性的指令碼語言中(比如PHP),這個過程不需要你去實現。

下面是一個簡單的把物件寫入memcached伺服器的例子:

<?php
class  Person
{
    public $name;
    public function setName($name){
        $this->name = $name;
    }
}

// 例項化
$p = new Person();
$p->setName("jack");// 給物件屬性賦值

// 連線memcached
$mem = memcache_connect("192.168.88.88",11211);
$mem->add("p_jack",$p,false,0); // 寫入
$obj = $mem->get("p_jack"); // 讀取

echo $obj->name; // 輸出:jack。

寫操作快取

讀操作快取大家一定很熟悉,可寫操作快取也至關重要。

假設有這樣一個需求:就拿我們站點訪問量統計功能來說,我們需要記錄每個URL的累計訪問量,所以每次頁面重新整理都會伴隨著一次訪問量的增加。

$page = "artical20180120.html";
$sql = "update page_view set view_count=view_count+1 where page='{$page}'";
$conn = mysqli_connect("localhost","root","root","db_page");
mysqli_select_db("db_main",$conn);
mysqli_query($sql,$conn);

沒有快取的方法就想上面程式碼那樣,我們稱之為“直接更新”。

執行緒安全和鎖競爭

我們先來看一個分散式加運算,下面的程式碼片段,先從快取伺服器取回一個數值,然而在本地+1,接下來再寫回快取伺服器。

$count = $mem->get($key);
$count++;
$mem->set($key,$count,false,0);

嗯,看起來似乎沒有什麼問題。但是,你可以別忘了可能會有多個使用者同時觸發這樣的計算,你一定能想象到有什麼糟糕的後果,最後累計訪問量總是小於實際訪問量。

事實上,這並不涉及memcacahed本身執行緒安全問題,而是上面這種加運算的方式不是執行緒安全的。如果要保證這種加運算同時進行,就要考慮一定的事務隔離機制。
最簡單的辦法是使用鎖競爭,並且把鎖儲存在memcached中,存在競爭關係的動態內容可以爭奪這個鎖,一單某個會話搶到鎖,那麼其他的會話就必須等待。
但我們不併鼓勵這樣做,因為鎖競爭帶來的等待時間是無法容忍的。

原子加法

memcached提供了原子遞增操作,也正是因為這個特性,我們在訪問量遞增更新的應用中引入寫快取
我們來修改程式碼:

$page = "artical20180120.html";

$mem = memcache_connect("192.168.88.88",11211);
$count = $mem->increment($page,1); // 累加1
if ($count === false){
    $mem->add($page,1);
    exit(1);
}
if ($count == 1000){
    $mem->set($page,0,false,0);
    $sql = "update page_view set view_count=view_count+{$count} where page='{$page}'";
    $conn = mysqli_connect("localhost","root","root","db_page");
    mysqli_select_db("db_main",$conn);
    mysqli_query($sql,$conn);
}

上面程式碼完全改變了“直接更新”方式,它做了以下工作:
1、為memcached快取中對應資料項加1,如果該資料項不存在,則建立該資料項,並且賦值為1,代表這個頁面是第一次被訪問;
2、如果memcached快取中存在對應資料項,並且累加後的數值為1000,則把這個資料項設定為0,並且更新資料庫,將資料庫對應數值加1000。

也就是火,改造後的程式每經歷1000次遞增後才寫一次資料庫。這就大大減少了直接對資料庫的操作。如果你的資料庫因為大量的寫操作而繁忙,那麼應該仔細考慮,哪些寫操作可以快取到memcached中?