Memcached介紹
Memcached是什么?
Free & open source, high-performance, distributed memory object caching system(自由&開放源碼,高性能,分布式的內(nèi)存對象緩存系統(tǒng))
。由LiveJournal旗下的danga公司開發(fā)的老牌nosql應用。
什么是NoSQL?
NoSQL,指的是菲關系型的數(shù)據(jù)庫。
相對于傳統(tǒng)關系型數(shù)據(jù)庫的"行與列",NoSQL的鮮明特點為key-value存儲(memcache,redis),或基于文檔存儲(mongodb)。
注:nosql --not only sql,不僅僅是關系型數(shù)據(jù)庫
Memcached安裝
Linux下編譯Memcached
準備編譯環(huán)境
再Linux下編譯,需要gcc,make,cmake,autoconf,libtool等工具,這幾件工具,以后還要編譯redis等使用,所以需要先安裝。在Linux系統(tǒng)聯(lián)網(wǎng)后,用如下命令安裝
yum install gcc gcc-c++ make cmake autoconf libtool
編譯Memcached
Memcached依賴于libevent庫,因此我們需要先安裝libevent。分別到libevent.org和memcached.org下載最新的stable版本(穩(wěn)定版)。
先編譯libevent,再編譯memcached。
編譯Memcached時要指定libevent的路徑。
過程如下:假設源碼在/root/package下,安裝在/usr/local下
tar zxvf libevent-2.1.8-stable.tar.gz
cd libevent-2.1.8-stable
./configure --prefix=/usr/local/libevent
make && make install
安裝memcached
tar zxvf memcached-1.5.1.tar.gz
cd memcached-1.5.1
./configure --prefix=/usr/local/memcached --with-libevent=/usr/local/libevent/
make && make install
配置環(huán)境變量
vi /etc/profile
export PATH="$PATH:/usr/local/memcached/bin"
source /etc/profile
創(chuàng)建memcached用戶
useradd memcached
設置開機自動啟動:
vi /etc/rc.local
增加
/usr/local/memcached/bin/memcached -u memcached -m 64 &
PHP安裝Memcached擴展
http://pecl.php.net/package/memcache下載擴展包
wget https://pecl.php.net/get/memcache-2.2.7.tgz
tar zxvf memcache-2.2.7.tgz
cd memcache-2.2.7
phpize //執(zhí)行phpize命令,phpize是PHP的工具,用來將PHP的擴展與PHP程序建立關聯(lián)
配置編譯安裝
./configure && make && make install
修改php.ini
vi /usr/local/php/lib/php.ini
在大約928行左右加上擴展配置
;linux extension load
extension=memcache.so
重啟Apache
apachectl -k restart
在phpinfo里可以查找到memcache說明安裝成功
Memcached的啟動
memcached -m 64 -p 11211 -u nobody -d //-d表示后臺運行(也可以用&)
| 選項 | 含義說明 |
|---|---|
| -p, --port=<num> | TCP port to listen on (default: 11211)Memcached監(jiān)聽的端口,要保證該端口號未被占用 |
| -U, --udp-port=<num> | UDP port to listen on (default: 11211, 0 is off)指定監(jiān)聽UDP的端口,默認11211,0表示關閉 |
| -s, --unix-socket=<file> | 指定Memcached用于監(jiān)聽的UNIX socket文件 |
| -A, --enable-shutdown | |
| -a, --unix-mask=<mask> | 設置-s選項指定的UNIX socket文件的權限 |
| -l, --listen=<addr> | 監(jiān)聽的服務器IP地址,如果有多個地址的話,使用逗號分隔,格式可以為“IP地址:端口號”,例如:-l 指定192.168.0.184:19830,192.168.0.195:13542;端口號也可以通過-p選項指定 |
| -d, --daemon | run as a daemon(指定memcached進程作為一個守護進程啟動) |
| -r, --enable-coredumps | 設置產(chǎn)生core文件大小 |
| -u, --user=<user> | assume identity of <username>(運行memcached的用戶,不可以以root用戶運行) |
| -m, --memory-limit=<num> | item memory in megabytes (default: 64 MB)指定分配給memcached使用的內(nèi)存,單位是MB(默認是64M) |
| -M, --disable-evictions | 當內(nèi)存使用超出配置值時,禁止自動清除緩存中的數(shù)據(jù)項,此時Memcached不可以,直到內(nèi)存被釋放 |
| -c, --conn-limit=<num> | max simultaneous connections (default: 1024)設置最大運行的并發(fā)連接數(shù),默認是1024 |
| -k, --lock-memory | 設置鎖定所有分頁的內(nèi)存,對于大緩存應用場景,謹慎使用該選項 |
| -v, --verbose | 輸出警告和錯誤信息 |
| -vv | 打印信息比-v更詳細:不僅輸出警告和錯誤信息,也輸出客戶端請求和響應信息 |
| -vvv | |
| -h, --help | 查看幫助 |
| -i, --license | 打印libevent和Memcached的licenses信息 |
| -V, --version | |
| -P, --pidfile=<file> | 保存memcached進程的pid文件 |
| -f, --slab-growth-factor=<num> | 用于計算緩存數(shù)據(jù)項的內(nèi)存塊大小的乘數(shù)因子,默認是1.25 |
| -n, --slab-min-size=<bytes> | 為緩存數(shù)據(jù)項的key、value、flag設置最小分配字節(jié)數(shù),默認是48 |
| -L, --enable-largepages | 嘗試使用大內(nèi)存分頁(pages) |
| -D <char> | 用于統(tǒng)計報告中Key前綴和ID之間的分隔符,默認是冒號“:” |
| -t, --threads=<num> | 指定用來處理請求的線程數(shù),默認為4 |
| -R, --max-reqs-per-event | 為避免客戶端餓死(starvation),對連續(xù)達到的客戶端請求數(shù)設置一個限額,如果超過該設置,會選擇另一個連接來處理請求,默認為20 |
| -C, --disable-cas | 禁用CAS |
| -b, --listen-backlog=<num> | |
| -B, --protocol=<name> | 指定使用的協(xié)議,默認行為是自動協(xié)商(autonegotiate),可能使用的選項有auto、ascii、binary。 |
| -I, --max-item-size=<num> | 覆蓋默認的STAB頁大小,默認是1M |
| -F, --disable-flush-all | 禁用flush_all命令 |
| -X, --disable-dumping | |
| -o, --extended | 指定逗號分隔的選項,一般用于用于擴展或?qū)嶒炐再|(zhì)的選項 |
可以使用memcached -h查看幫助來了解各個參數(shù)的意義。
| 選項 | 含義說明 |
|---|---|
| -p, --port=<num> | TCP port to listen on (default: 11211)Memcached監(jiān)聽的端口,要保證該端口號未被占用 |
| -U, --udp-port=<num> | UDP port to listen on (default: 11211, 0 is off)指定監(jiān)聽UDP的端口,默認11211,0表示關閉 |
| -s, --unix-socket=<file> | 指定Memcached用于監(jiān)聽的UNIX socket文件 |
| -A, --enable-shutdown | |
| -a, --unix-mask=<mask> | 設置-s選項指定的UNIX socket文件的權限 |
| -l, --listen=<addr> | 監(jiān)聽的服務器IP地址,如果有多個地址的話,使用逗號分隔,格式可以為“IP地址:端口號”,例如:-l 指定192.168.0.184:19830,192.168.0.195:13542;端口號也可以通過-p選項指定 |
| -d, --daemon | run as a daemon(指定memcached進程作為一個守護進程啟動) |
| -r, --enable-coredumps | 設置產(chǎn)生core文件大小 |
| -u, --user=<user> | assume identity of <username>(運行memcached的用戶,不可以以root用戶運行) |
| -m, --memory-limit=<num> | item memory in megabytes (default: 64 MB)指定分配給memcached使用的內(nèi)存,單位是MB(默認是64M) |
| -M, --disable-evictions | 當內(nèi)存使用超出配置值時,禁止自動清除緩存中的數(shù)據(jù)項,此時Memcached不可以,直到內(nèi)存被釋放 |
| -c, --conn-limit=<num> | max simultaneous connections (default: 1024)設置最大運行的并發(fā)連接數(shù),默認是1024 |
| -k, --lock-memory | 設置鎖定所有分頁的內(nèi)存,對于大緩存應用場景,謹慎使用該選項 |
| -v, --verbose | 輸出警告和錯誤信息 |
| -vv | 打印信息比-v更詳細:不僅輸出警告和錯誤信息,也輸出客戶端請求和響應信息 |
| -vvv | |
| -h, --help | 查看幫助 |
| -i, --license | 打印libevent和Memcached的licenses信息 |
| -V, --version | |
| -P, --pidfile=<file> | 保存memcached進程的pid文件 |
| -f, --slab-growth-factor=<num> | 用于計算緩存數(shù)據(jù)項的內(nèi)存塊大小的乘數(shù)因子,默認是1.25 |
| -n, --slab-min-size=<bytes> | 為緩存數(shù)據(jù)項的key、value、flag設置最小分配字節(jié)數(shù),默認是48 |
| -L, --enable-largepages | 嘗試使用大內(nèi)存分頁(pages) |
| -D <char> | 用于統(tǒng)計報告中Key前綴和ID之間的分隔符,默認是冒號“:” |
| -t, --threads=<num> | 指定用來處理請求的線程數(shù),默認為4 |
| -R, --max-reqs-per-event | 為避免客戶端餓死(starvation),對連續(xù)達到的客戶端請求數(shù)設置一個限額,如果超過該設置,會選擇另一個連接來處理請求,默認為20 |
| -C, --disable-cas | 禁用CAS |
| -b, --listen-backlog=<num> | |
| -B, --protocol=<name> | 指定使用的協(xié)議,默認行為是自動協(xié)商(autonegotiate),可能使用的選項有auto、ascii、binary。 |
| -I, --max-item-size=<num> | 覆蓋默認的STAB頁大小,默認是1M |
| -F, --disable-flush-all | 禁用flush_all命令 |
| -X, --disable-dumping | |
| -o, --extended | 指定逗號分隔的選項,一般用于用于擴展或?qū)嶒炐再|(zhì)的選項 |
Memcached基本使用
PHP操作Memcache
| 方法 | 方法說明 |
|---|---|
| connect() | 打開一個memcached服務端連接 |
| add() | 增加一個條目到緩存服務器 |
| addServer() | 向連接池中添加一個memcache服務器 |
| increment() | 增加一個元素的值 |
| decrement() | 減小一個元素的值 |
| delete() | 從服務端刪除一個元素 |
| flush() | 清洗(刪除)已經(jīng)存儲的所有的元素 |
| get() | 從服務端檢回一個元素 |
| set() | 保存數(shù)據(jù)到緩存服務器 |
| replace() | 替換已經(jīng)存在的元素的值 |
| pconnect() | 打開一個到服務器的持久化連接 |
| close() | 關閉memcache連接 |
PHP連接memcache服務
$memcache = new Memcache();
$memcache->connect("192.168.20.131", 11211);
bool Memcache::set (string $key , mixed $var [, int $flag [, int $expire ]])
$key:要設置值的key
$var:要存儲的值,字符串和數(shù)值直接存儲,其他類型序列化后存儲。數(shù)據(jù)的最大長度為1M。
$flag: 使用MEMCACHE_COMPRESSED指定對值進行壓縮(使用zlib)。通常傳入0即可,表示不需要壓縮。
$expire:當前寫入緩存的數(shù)據(jù)的失效時間。如果此值設置為0表明此數(shù)據(jù)永不過期。當時間小于30天時表示的是時間間隔,當時間大于30天表示時間戳
bool Memcache::add (string $key , mixed $var [, int $flag [, int $expire ]])
說明:與set類似,僅僅可以執(zhí)行添加操作,不能執(zhí)行修改操作,當key已經(jīng)存在時,則add失敗
bool Memcache::replace (string $key , mixed $var [, int $flag [, int $expire ]])
說明:與set類似,僅僅可以執(zhí)行替換操作,僅僅在key存在時才可以執(zhí)行,當key不存在時,替換是失敗的
get($key [, $flag]) --- 獲取
獲取時, 有時需要設置第二個參數(shù), flag標志!
比如如果存儲時設置了第二個參數(shù)為壓縮存儲,那么獲取時也需要傳遞壓縮存儲的參數(shù)。
increment($key, $num) --- 遞增
在原有值得基礎上增加,第二個參數(shù)不寫默認是1
decrement($key, $num) --- 遞減
在原有值得基礎減少,
delete($key) --- 刪除
fulsh() --- 清空/刷新
close() --- 關閉連接
PHP
Memcached的內(nèi)存管理與刪除機制
內(nèi)存的碎片化
如果用 c 語言直接 malloc, free 來向操作系統(tǒng)申請和釋放內(nèi)存時,在不斷的申請和釋放過程中,形成了一些很小的內(nèi)存片斷,無法再利用。這種空閑,但無法利用內(nèi)存的現(xiàn)象,--稱為內(nèi)存的碎片化。
slab allocator緩解內(nèi)存碎片化
Memcached 用 slab allocator 機制來管理內(nèi)存。
Slab allocator 原理:預告把內(nèi)存劃分成數(shù)個 slab cl ass 倉庫。(每個 slab class 大小 1 M)各倉庫,切分成不同尺時的小塊(chunk).(圖 3.2)
需要存內(nèi)容時,判斷內(nèi)容的大小,為其選取合理的倉庫.

系統(tǒng)如何選擇合適的chunk?
Memcached 根據(jù)收到的數(shù)據(jù)的大小,選擇最適合數(shù)據(jù)大小的 chunk 組(slab class)(圖 3.3)。memcached 中保存著 slab class 內(nèi)空閑 chunk 的列表,根據(jù)該列表選擇空的 chunk,然后將數(shù)據(jù)緩存于其中。

警告:
如果有100byte的內(nèi)存要存,但122大小的倉庫的chunk滿了
并不會尋找更大的,如144的倉庫來存儲,
而是把122倉庫的舊數(shù)據(jù)踢掉!詳見過期與刪除機制
固定大小的chunk帶來的內(nèi)存浪費
由于 slab allocator 機制中,分配的 chunk 的大小是”固定”的,因此,對于特定的 item,可能造成內(nèi)存空間的浪費。
比如,將 100 字節(jié)的數(shù)據(jù)緩存到 122 字節(jié)的 chunk 中,剩余的 22 字節(jié)就浪費了圖 3.4)

對于 chunk 空間的浪費問題,無法徹底解決,只能緩解該問題。
開發(fā)者可以對網(wǎng)站中緩存中的 item 的長度進行統(tǒng)計,并制定合理的 slab class 中的 chunk 的大小。
可惜的是,我們目前還不能自定義 chunk 的大小,但可以通過參數(shù)來調(diào)整各 slab class 中 chunk 大小的增長速度。即增長因子,grow factor!
growfactor調(diào)優(yōu)
Memcached 在啟動時可以通過- f 選項指定 Growth Factor 因子,并在某種程度上控制 slab 之間的差異。默認值為 1.25. 但是,在該選項出現(xiàn)之前,這個因子曾經(jīng)固定為 2, 稱為”powers of2”策略。

對比可知,當 f=2 時,各 slab 中的 chunk size 增長很快,有些情況下就相當浪費內(nèi)存。因此,我們應細心統(tǒng)計緩存的大小,制定合理的增長因子。
注意:
當 f=1.25 時,從輸出結(jié)果來看,某些相鄰的 slab class 的大小比值并非為 1.25,可能會覺得有些 計算誤差,這些誤差是為了保持字節(jié)數(shù)的對齊而故意設置的.
memcached的過期數(shù)據(jù)惰性刪除
1: 當某個值過期后,并沒有從內(nèi)存刪除, 因此,stats 統(tǒng)計時, curr_item 有其信息
2: 當某個新值去占用他的位置時,當成空 chunk 來占用.
3: 當 get 值時,判斷是否過期,如果過期,返回空,并且清空, curr_item 就減少了.
這個過期,只是讓用戶看不到這個數(shù)據(jù)而已,并沒有在過期的瞬間立即從內(nèi)存刪除. 這個稱為 lazy expiration, 惰性失效.
好處:節(jié)省了CPU時間和檢測的成本
memcached的LRU刪除機制
如果以 122byte 大小的 chunk 舉例, 122 的 chunk 都滿了, 又有新的值(長度為 120)要加入, 要 擠掉誰?
memcached 此處用的 lru 刪除機制.
(操作系統(tǒng)的內(nèi)存管理,常用 fifo,lru 刪除)
lru: least recently used 最近最少使用
fifo: first in ,first out
原理:當某個單元被請求時,維護一個計數(shù)器,通過計數(shù)器來判斷最近誰最少被使用. 就把誰t出.
注意:即使某個key是設置的永久有效期,也一樣會被踢出來!即-永久數(shù)據(jù)被踢現(xiàn)象
Memcached的一些參數(shù)限制
key 的長度: 250 字節(jié), (二進制協(xié)議支持 65536 個字節(jié))
value 的限制: 1m, 一般都是存儲一些文本,如新聞列表等等,這個值足夠了.
內(nèi)存的限制: 32 位下最大設置到 2G.
如果有 30g 數(shù)據(jù)要緩存,一般也不會單實例裝 30G, (不要把雞蛋裝在一個籃子里), 一般建議 開啟多個實例(可以在不同的機器,或同臺機器上的不同端口)
分布式集群算法
Memcached如何實現(xiàn)分布式
Memcached并不像MongoDB那樣,允許配置多個節(jié)點,且節(jié)點之間"自動分配數(shù)據(jù)"。也就是說,Memcached節(jié)點之間是不互相通信的
因此,Memcached的分布式,要靠用戶去設計算法,把數(shù)據(jù)分布在多個Memcached節(jié)點中。
求余/取模算法
代碼:
interface hasher {
public function hash($str);
}
interface distribution {
public function lookup($key);
}
/**
* Class Moder
* 求余算法
*/
class Moder implements hasher, distribution {
protected $server = array();
protected $num = 0;
//計算一個字符串對應的32 位循環(huán)冗余校驗碼多項式
public function hash($str) {
return sprintf("%u", crc32($str));
}
//查詢數(shù)據(jù)應存放的節(jié)點服務器
public function lookup($key) {
$index = $this->hash($key) % $this->num;
return $this->server[$index];
}
//模擬增加一臺Memcached服務器
public function addNode($s) {
$this->server[] = $s;
$this->num++;
}
//模擬Memcached服務器宕機
public function delNode($s) {
foreach ($this->server as $key => $value) {
if($s == $value) {
unset($this->server[$key]);
}
}
$this->num--;
$this->server = array_merge($this->server); //重新整理server的鍵,使其按照0->1->2遞增
}
}
$moder = new Moder();
$moder->addNode('a');
$moder->addNode('b');
$moder->addNode('c');
$moder->addNode('d');
for($i = 0; $i < 100; $i++) {
$key = 'key_'.$i;
echo $key, '---->', $moder->lookup($key), '<br>';
}
一致性哈希算法
通俗理解一致性哈希:把各服務器節(jié)點映射放在鐘表的各個時刻上,把key也映射到鐘表的某個時刻上,該key沿著鐘表順時針走,碰到的第一個節(jié)點即為該key的存儲節(jié)點。如下圖所示:

一致性哈希+虛擬節(jié)點算法代碼
interface hasher {
public function hash($str);
}
interface distribution {
public function lookup($key);
}
class Consistency implements hasher, distribution {
protected $nodes = array();
protected $points = array();
protected $multi = 64; //每個Memcached服務器對應的虛擬節(jié)點數(shù)量
//計算一個字符串對應的32 位循環(huán)冗余校驗碼多項式
public function hash($str) {
return sprintf("%u", crc32($str));
}
//查詢數(shù)據(jù)應存放的節(jié)點服務器
public function lookup($key) {
$position = $this->hash($key);
reset($this->points); //重置指針(因為foreach遍歷時會數(shù)組指針后移,等到下次遍歷數(shù)組時,key()方法獲取的就不是第一個元素的鍵了)
$needle = key($this->points); //默認會落在第一個節(jié)點
foreach ($this->points as $key => $value) {
if($position <= $key) {
$needle = $key;
break;
}
}
return $this->points[$needle];
}
//添加節(jié)點
public function addNode($node) {
$this->nodes[$node] = array();
for($i = 0; $i < $this->multi; $i++) {
$point = $node . '_' . $i;
$point = $this->hash($point); //虛擬節(jié)點轉(zhuǎn)為數(shù)字
$this->points[$point] = $node;
$this->nodes[$node][] = $point;
$this->resort();
}
}
public function delNode($node) {
foreach ($this->nodes[$node] as $p) { //清除64個虛擬節(jié)點
unset($this->points[$p]);
}
unset($this->nodes[$node]); //去掉節(jié)點
}
//對虛擬幾點進行排序
protected function resort() {
ksort($this->points);
}
}
$consistency = new Consistency();
$consistency->addNode('A');
$consistency->addNode('B');
$consistency->addNode('C');
$consistency->addNode('D');
echo $consistency->hash('name');
echo $consistency->lookup('name');
并發(fā)處理-樂觀鎖
在新版的memcached中,增加了對并發(fā)的控制,處理方案是:樂觀鎖
并發(fā):多個進程(連接), 同時操作一個key. 就是并發(fā)操作.
樂觀鎖:
進程A, 先操作了緩存項
在進程A第二次操作緩存項前, 進程B操作了緩存項.
之后, 進程A第二次操作緩存項. 檢查, 在進程A第一次操作后, 是否有其他進程操作過需要的緩存項. 如果有, 則放棄第二次操作. 采取樂觀的處理態(tài)度.(樂觀鎖定)
支持樂觀鎖的操作:
gets() 獲取
cas() 設置
注意:Memcache擴展還不支持這兩個操作,在telnet上可以演示
Memcached經(jīng)典問題或現(xiàn)象
緩存雪崩現(xiàn)象及真實案例
緩存雪崩一般是由某個緩存節(jié)點失效,導致其他節(jié)點的緩存命中率下降,緩存中缺失的數(shù)據(jù)去數(shù)據(jù)庫查詢。短時間內(nèi),造成數(shù)據(jù)庫服務器崩潰。
重啟DB,短期又被壓垮,但緩存數(shù)據(jù)也多一些。
DB反復多次啟動,緩存重建完畢,DB才穩(wěn)定運行。
或者,是由于緩存周期性的失效,比如每6個小時失效一次,那么每6小時,將有一個請求“峰值”,嚴重者甚至會令DB崩潰。
解決辦法/方案:把緩存設置為隨機3-9個小時的生命周期,這樣不同時失效,把工作分擔到各個時間點。
緩存的無底洞現(xiàn)象multiget-hole
永久數(shù)據(jù)被踢現(xiàn)象
網(wǎng)上有人反饋為"memcached 數(shù)據(jù)丟失",明明設為永久有效,卻莫名其妙的丟失了.
其實,這要從 2 個方面來找原因:
即前面介紹的 惰性刪除,與 LRU 最近最少使用記錄刪除.
分析(如下圖)
1:如果 slab 里的很多 chunk,已經(jīng)過期,但過期后沒有被 get 過, 系統(tǒng)不知他們已經(jīng)過期.
2:永久數(shù)據(jù)很久沒 get 了,不活躍,如果新增 item,則永久數(shù)據(jù)被踢了.
3: 當然,如果那些非永久數(shù)據(jù)被 get,也會被標識為 expire,從而不會再踢掉永久數(shù)據(jù)
