memcached的總結(jié)和分布式一致性hash
10年積累的成都網(wǎng)站制作、網(wǎng)站設(shè)計經(jīng)驗,可以快速應對客戶對網(wǎng)站的新想法和需求。提供各種問題對應的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務。我雖然不認識你,你也不認識我。但先網(wǎng)站設(shè)計后付款的網(wǎng)站建設(shè)流程,更有磁縣免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
當前很多大型的web系統(tǒng)為了減輕數(shù)據(jù)庫服務器負載,會采用memchached作為緩存系統(tǒng)以提高響應速度。
目錄: ()
memchached簡介
hash
取模
一致性hash
虛擬節(jié)點
源碼解析
參考資料
1. memchached簡介
memcached是一個開源的高性能隱悶野分布式內(nèi)存對象緩存系統(tǒng)。
其實思想還是比較簡單的,實現(xiàn)包括server端(memcached開源項目一般只單指server端)和client端兩部分:
server端本質(zhì)是一個in-memory key-value store,通過在內(nèi)存中維護一個大的hashmap用來存儲小塊的任意數(shù)據(jù),對外通過統(tǒng)一的簡單接口(memcached protocol)來提供操作。
client端是一個library,負責處理memcached protocol的網(wǎng)絡(luò)通信細節(jié),與memcached server通信,針對各種語言的不同實現(xiàn)分裝了易用的API實現(xiàn)了與不同語言平臺的集成。
web系統(tǒng)則通過client庫來使用memcached進行對象緩存。
2. hash
memcached的分布式主要體現(xiàn)在client端,對于server端,僅僅是部署多個memcached server組成集群,每個server獨自維護自己的數(shù)據(jù)(互相之間沒有任何通信),通過daemon監(jiān)聽端口等待client端的請求。
而在client端,通過一致的hash算法,將要存儲的數(shù)據(jù)分布到某個特定的server上進行存儲,后續(xù)讀取查詢使用同樣的hash算法即可定位。
client端可以采用各種hash算法來定位server:
取模
最簡單的hash算法
targetServer = serverList[hash(key) % serverList.size]
直接用key的hash值(計算key的hash值的方法可以自由選擇,比如算法CRC32、MD5,甚至本地hash系統(tǒng),如java的hashcode)模上server總數(shù)來定位目標server。這種算法不僅簡單,而且具有不錯的隨機分布特性。
但是問題也很明顯,server總數(shù)不能輕易變化。因為如果增加/減少memcached server的數(shù)量,對原先存儲的所有key的后續(xù)查詢都將定位到別的server上,導致所有的cache都不能被命中而失效。
一致性hash
為了解決這個問題,需要采用一致性hash算法(consistent hash)
相對于取模的算法,一致性hash算法除了計算key的hash值外,還會計算每個server對應的hash值,然后將這些hash值映射到一個有限的值域上(比如0~2^32)。灶喊通過尋找hash值大于hash(key)的最小server作為存儲該key數(shù)據(jù)的目標server。如果找不到,則直接把具有罩野最小hash值的server作為目標server。
為了方便理解,可以把這個有限值域理解成一個環(huán),值順時針遞增。
如上圖所示,集群中一共有5個memcached server,已通過server的hash值分布到環(huán)中。
如果現(xiàn)在有一個寫入cache的請求,首先計算x=hash(key),映射到環(huán)中,然后從x順時針查找,把找到的第一個server作為目標server來存儲cache,如果超過了2^32仍然找不到,則命中第一個server。比如x的值介于A~B之間,那么命中的server節(jié)點應該是B節(jié)點
可以看到,通過這種算法,對于同一個key,存儲和后續(xù)的查詢都會定位到同一個memcached server上。
那么它是怎么解決增/刪server導致的cache不能命中的問題呢?
假設(shè),現(xiàn)在增加一個server F,如下圖
此時,cache不能命中的問題仍然存在,但是只存在于B~F之間的位置(由C變成了F),其他位置(包括F~C)的cache的命中不受影響(刪除server的情況類似)。盡管仍然有cache不能命中的存在,但是相對于取模的方式已經(jīng)大幅減少了不能命中的cache數(shù)量。
虛擬節(jié)點
但是,這種算法相對于取模方式也有一個缺陷:當server數(shù)量很少時,很可能他們在環(huán)中的分布不是特別均勻,進而導致cache不能均勻分布到所有的server上。
如圖,一共有3臺server – 1,2,4。命中4的幾率遠遠高于1和2。
為解決這個問題,需要使用虛擬節(jié)點的思想:為每個物理節(jié)點(server)在環(huán)上分配100~200個點,這樣環(huán)上的節(jié)點較多,就能抑制分布不均勻。
當為cache定位目標server時,如果定位到虛擬節(jié)點上,就表示cache真正的存儲位置是在該虛擬節(jié)點代表的實際物理server上。
另外,如果每個實際server的負載能力不同,可以賦予不同的權(quán)重,根據(jù)權(quán)重分配不同數(shù)量的虛擬節(jié)點。
// 采用有序map來模擬環(huán)
this.consistentBuckets = new TreeMap();
MessageDigest md5 = MD5.get();//用MD5來計算key和server的hash值
// 計算總權(quán)重
if ( this.totalWeight for ( int i = 0; i this.weights.length; i++ )
this.totalWeight += ( this.weights[i] == null ) ? 1 : this.weights[i];
} else if ( this.weights == null ) {
this.totalWeight = this.servers.length;
}
// 為每個server分配虛擬節(jié)點
for ( int i = 0; i servers.length; i++ ) {
// 計算當前server的權(quán)重
int thisWeight = 1;
if ( this.weights != null this.weights[i] != null )
thisWeight = this.weights[i];
// factor用來控制每個server分配的虛擬節(jié)點數(shù)量
// 權(quán)重都相同時,factor=40
// 權(quán)重不同時,factor=40*server總數(shù)*該server權(quán)重所占的百分比
// 總的來說,權(quán)重越大,factor越大,可以分配越多的虛擬節(jié)點
double factor = Math.floor( ((double)(40 * this.servers.length * thisWeight)) / (double)this.totalWeight );
for ( long j = 0; j factor; j++ ) {
// 每個server有factor個hash值
// 使用server的域名或IP加上編號來計算hash值
// 比如server - "172.45.155.25:11111"就有factor個數(shù)據(jù)用來生成hash值:
// 172.45.155.25:11111-1, 172.45.155.25:11111-2, ..., 172.45.155.25:11111-factor
byte[] d = md5.digest( ( servers[i] + "-" + j ).getBytes() );
// 每個hash值生成4個虛擬節(jié)點
for ( int h = 0 ; h 4; h++ ) {
Long k =
((long)(d[3+h*4]0xFF) 24)
| ((long)(d[2+h*4]0xFF) 16)
| ((long)(d[1+h*4]0xFF) 8 )
| ((long)(d[0+h*4]0xFF));
// 在環(huán)上保存節(jié)點
consistentBuckets.put( k, servers[i] );
}
}
// 每個server一共分配4*factor個虛擬節(jié)點
}
// 采用有序map來模擬環(huán)
this.consistentBuckets = new TreeMap();
MessageDigest md5 = MD5.get();//用MD5來計算key和server的hash值
// 計算總權(quán)重
if ( this.totalWeight for ( int i = 0; i this.weights.length; i++ )
this.totalWeight += ( this.weights[i] == null ) ? 1 : this.weights[i];
} else if ( this.weights == null ) {
this.totalWeight = this.servers.length;
}
// 為每個server分配虛擬節(jié)點
for ( int i = 0; i servers.length; i++ ) {
// 計算當前server的權(quán)重
int thisWeight = 1;
if ( this.weights != null this.weights[i] != null )
thisWeight = this.weights[i];
// factor用來控制每個server分配的虛擬節(jié)點數(shù)量
// 權(quán)重都相同時,factor=40
// 權(quán)重不同時,factor=40*server總數(shù)*該server權(quán)重所占的百分比
// 總的來說,權(quán)重越大,factor越大,可以分配越多的虛擬節(jié)點
double factor = Math.floor( ((double)(40 * this.servers.length * thisWeight)) / (double)this.totalWeight );
for ( long j = 0; j factor; j++ ) {
// 每個server有factor個hash值
// 使用server的域名或IP加上編號來計算hash值
// 比如server - "172.45.155.25:11111"就有factor個數(shù)據(jù)用來生成hash值:
// 172.45.155.25:11111-1, 172.45.155.25:11111-2, ..., 172.45.155.25:11111-factor
byte[] d = md5.digest( ( servers[i] + "-" + j ).getBytes() );
// 每個hash值生成4個虛擬節(jié)點
for ( int h = 0 ; h 4; h++ ) {
Long k =
((long)(d[3+h*4]0xFF) 24)
| ((long)(d[2+h*4]0xFF) 16)
| ((long)(d[1+h*4]0xFF) 8 )
| ((long)(d[0+h*4]0xFF));
// 在環(huán)上保存節(jié)點
consistentBuckets.put( k, servers[i] );
}
}
// 每個server一共分配4*factor個虛擬節(jié)點
}
// 用MD5來計算key的hash值
MessageDigest md5 = MD5.get();
md5.reset();
md5.update( key.getBytes() );
byte[] bKey = md5.digest();
// 取MD5值的低32位作為key的hash值
long hv = ((long)(bKey[3]0xFF) 24) | ((long)(bKey[2]0xFF) 16) | ((long)(bKey[1]0xFF) 8 ) | (long)(bKey[0]0xFF);
// hv的tailMap的第一個虛擬節(jié)點對應的即是目標server
SortedMap tmap = this.consistentBuckets.tailMap( hv );
return ( tmap.isEmpty() ) ? this.consistentBuckets.firstKey() : tmap.firstKey();
更多問題到問題求助專區(qū)()
當前做分布式的廠商有幾家,我知道比較出名的有“華為云分布式數(shù)據(jù)庫DDM”和“阿里云分布式數(shù)據(jù)庫”,感興趣可以閉臘自行搜素了解下。
分布式數(shù)據(jù)庫的幾點概念可以了解一下。
數(shù)據(jù)分庫:
以表為單位,把原有數(shù)據(jù)庫切分成多個數(shù)據(jù)庫。切分漏寬后不同的表存儲在不同的數(shù)據(jù)庫上。
以表中的數(shù)據(jù)行記錄為單位,把原有邏輯數(shù)據(jù)庫切分成多個物理數(shù)據(jù)庫分片,表數(shù)據(jù)記錄分布存儲在各個分片上。
路由分發(fā):
在分布式數(shù)據(jù)庫中,路由的作用即將SQL語句進行解析,并轉(zhuǎn)發(fā)到正確的分片上,保證SQL執(zhí)行后得到正確的結(jié)果,并且節(jié)約QPS資源。
讀寫分離:
數(shù)據(jù)庫中對計算和緩存資源消耗較多的往往是密集或復雜的SQL查詢。當系統(tǒng)資源被查詢語句消耗,反過來會影響數(shù)據(jù)寫入操作,進而導致數(shù)據(jù)庫整體性能下降,響應緩慢。因此,當數(shù)據(jù)庫CPU和內(nèi)存資源占用居高不下,且讀寫比例較高時,可以為數(shù)據(jù)庫添加只讀數(shù)返態(tài)亮據(jù)庫。
普通的Web開發(fā),常用的模式就是用戶登錄之后,登錄狀態(tài)信息保存在Session中,用戶一些常用的熱數(shù)據(jù)保存在文件緩存中,用戶上傳的附件信息保存在Web服務器的某個目錄上。這種方式對于一般的Web應用,使用很方便,完全能夠勝任。但是對于高并發(fā)的企業(yè)級網(wǎng)站,就應付不了了。需要采用Web集群實現(xiàn)負載均衡。
使用Web集群方式部署之后,首要調(diào)整的就是用戶狀態(tài)信息與附件信息。用戶狀態(tài)不能再保存到Session中,緩存也不能用本地Web服務器的文件緩存,以及附件,也不能保存在Web服務器上了。因為要埋祥賣保證集群里面的各個Web服務器,狀態(tài)完全一致。因此,需要將用戶狀態(tài)、緩存等保存到專用的緩存服務器,比如Memcache。附件需要保存到云存儲中。
Web負載均衡
Web負載均衡(Load Balancing),簡單地說就是給我們的彎逗服務器集群分配“工作任務”,而采用恰當?shù)姆峙浞绞剑瑢τ诒Wo處于后端的Web服務器來說,非常重要。
負載均衡的策略有很多,我們從簡單的講起。?
1. HTTP重定向?
當用戶發(fā)來請求的時候,Web服務器通過修改HTTP響應頭中的Location標記來返回一個新的url,然后瀏覽器再繼續(xù)請求這個新url,實際上就是頁面重定宴兆向。通過重定向,來達到“負載均衡”的目標。
2. 反向代理負載均衡?
反向代理服務的核心工作主要是轉(zhuǎn)發(fā)HTTP請求,扮演了瀏覽器端和后臺Web服務器中轉(zhuǎn)的角色。因為它工作在HTTP層(應用層),也就是網(wǎng)絡(luò)七層結(jié)構(gòu)中的第七層,因此也被稱為“七層負載均衡”??梢宰龇聪虼淼能浖芏?,比較常見的一種是Nginx。
Nginx是一種非常靈活的反向代理軟件,可以自由定制化轉(zhuǎn)發(fā)策略,分配服務器流量的權(quán)重等。反向代理中,常見的一個問題,就是Web服務器存儲的session數(shù)據(jù)。
3. IP負載均衡
IP負載均衡服務是工作在網(wǎng)絡(luò)層(修改IP)和傳輸層(修改端口,第四層),比起工作在應用層(第七層)性能要高出非常多。原理是,他是對IP層的數(shù)據(jù)包的IP地址和端口信息進行修改,達到負載均衡的目的。這種方式,也被稱為“四層負載均衡”。常見的負載均衡方式,是LVS(Linux Virtual Server,Linux虛擬服務),通過IPVS(IP Virtual Server,IP虛擬服務)來實現(xiàn)。
Think3.2.3配置多個數(shù)據(jù)庫連接方法:
一、全局配置定義
常用的配置方式是在應用配置文件或者模塊配置文件中添加下面的配置參數(shù):
//數(shù)據(jù)庫配置信息
'DB_TYPE' = 'mysql', // 數(shù)據(jù)庫類型
'DB_HOST' = 'localhost', // 服務器地址
'DB_NAME' = 'thinkphp', // 數(shù)據(jù)庫名
'DB_USER' = 'root', // 用戶名
'DB_PWD' = '123456', // 密碼
'DB_PORT' = 3306, // 端口
'DB_PREFIX' = 'think_', // 數(shù)據(jù)庫表前綴
'DB_CHARSET'= 'utf8', // 字符集
數(shù)據(jù)庫的類型由DB_TYPE參數(shù)設(shè)置。
下面是目前支持的數(shù)據(jù)庫設(shè)置:
DB_TYPE設(shè)置 支持的數(shù)據(jù)庫類型
mysql或mysqli mysql
pgsql pgsql
sqlite sqlite
mssql 或sqlsrv sqlserver
oracle oracle
ibase ibase
mongo mongo
PDO PDO支持的所有數(shù)據(jù)庫
如果DB_TYPE使用PDO類型的話,數(shù)據(jù)庫類型則由DB_DSN配置決定。
或者采用如下配置
'DB_DSN' = 'mysql://root:123456@localhost:3306/thinkphp#utf8'
使用DB_DSN方式定義可以簡化配置參數(shù),DSN參數(shù)格式為:
數(shù)據(jù)庫類型://用戶名:密碼@數(shù)據(jù)庫地址:數(shù)據(jù)庫端口/數(shù)據(jù)庫名#字符集
字符集設(shè)置需要3.2.1版本以上有效,字符集如果沒有設(shè)置的話,默認為utf8。
如果兩種配置參數(shù)同時存在的話,DB_DSN配置參數(shù)優(yōu)先。
注意:如果要設(shè)置分布式數(shù)據(jù)庫,暫時不支持DB_DSN方式配置。
如果采用PDO驅(qū)動的話,則必須首先配置DB_TYPE為pdo,然后還需要單獨配置其他參數(shù),例如:
//PDO連接方式
'DB_TYPE' = 'pdo', // 數(shù)據(jù)庫類型
'DB_USER' = 'root', // 用戶名
'DB_PWD' = '', // 密碼
'DB_PREFIX' = 'think_', // 數(shù)據(jù)庫表前綴
'DB_DSN' = 'mysql:host=localhost;dbname=thinkphp;charset=utf8'
注意:PDO方式的DB_DSN配置格式有所區(qū)別,根據(jù)不同的數(shù)據(jù)庫類型設(shè)物族置有所不同,具體可以參考PHP手冊。
配置文件定義的數(shù)據(jù)庫連接信息一般是系統(tǒng)默認采用的,因為一般一個應用的數(shù)據(jù)庫訪問配置是相同的。該方法系統(tǒng)在連接數(shù)據(jù)庫的時候會自動獲取,無需手動連接。
可以對每個緩悶模塊定義不同的數(shù)據(jù)庫連接信息,如果開啟了調(diào)試模式的話,還可以在不同的應用狀態(tài)的配置文件里面定義獨立的數(shù)據(jù)庫配置信息。
二、模型類定義
如果在某個模型類里面定義了connection屬性的話,則實例化該自定義模型的時候會采用定義的數(shù)據(jù)庫連接信息,而不是配置文件中設(shè)置的默認連接信息,通常用于某些數(shù)據(jù)表位于當前數(shù)據(jù)庫連接之外的其它數(shù)據(jù)庫,例如:
//在模型里單獨設(shè)置數(shù)據(jù)庫連接信息
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
protected $connection = array(
'db_type' = 'mysql',
'db_user' = 'root',
'db_pwd' = '1234',
'db_host' = 'localhost',
'db_port' = '3306',
'db_name' = 'thinkphp',
'db_charset' = 'utf8',
);
}
也可以采用DSN方式定義,例如:
//在模型里單獨設(shè)置數(shù)據(jù)庫連接信息
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
//或者使用DSN定義
protected $connection = 'mysql://root:1234@localhost:3306/thinkphp#utf8';
}
如果我們已經(jīng)在配置文件中配置了額外的擾螞彎數(shù)據(jù)庫連接信息,例如:
//數(shù)據(jù)庫配置1
'DB_CONFIG1' = array(
'db_type' = 'mysql',
'db_user' = 'root',
'db_pwd' = '1234',
'db_host' = 'localhost',
'db_port' = '3306',
'db_name' = 'thinkphp',
'db_charset'= 'utf8',
),
//數(shù)據(jù)庫配置2
'DB_CONFIG2' = 'mysql://root:1234@localhost:3306/thinkphp#utf8';
那么,我們可以把模型類的屬性定義改為:
//在模型里單獨設(shè)置數(shù)據(jù)庫連接信息
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
//調(diào)用配置文件中的數(shù)據(jù)庫配置1
protected $connection = 'DB_CONFIG1';
}
//在模型里單獨設(shè)置數(shù)據(jù)庫連接信息
namespace Home\Model;
use Think\Model;
class InfoModel extends Model{
//調(diào)用配置文件中的數(shù)據(jù)庫配置1
protected $connection = 'DB_CONFIG2';
}
三、實例化定義
除了在模型定義的時候指定數(shù)據(jù)庫連接信息外,我們還可以在實例化的時候指定數(shù)據(jù)庫連接信息,例如: 如果采用的是M方法實例化模型的話,也可以支持傳入不同的數(shù)據(jù)庫連接信息,例如:
$User = M('User','other_','mysql://root:1234@localhost/demo#utf8');
表示實例化User模型,連接的是demo數(shù)據(jù)庫的other_user表,采用的連接信息是第三個參數(shù)配置的。如果我們在項目配置文件中已經(jīng)配置了DB_CONFIG2的話,也可以采用:
$User = M('User','other_','DB_CONFIG2');
需要注意的是,ThinkPHP的數(shù)據(jù)庫連接的惰性的,所以并不是在實例化的時候就連接數(shù)據(jù)庫,而是在有實際的數(shù)據(jù)操作的時候才會去連接數(shù)據(jù)庫(額外的情況是,在系統(tǒng)第一次實例化模型的時候,會自動連接數(shù)據(jù)庫獲取相關(guān)模型類對應的數(shù)據(jù)表的字段信息)。