我對(duì)于Memcached的接觸,還是在去年看了CSDN的一系列國(guó)外大型網(wǎng)站架構(gòu)設(shè)計(jì)而開(kāi)始的。最初的時(shí)候只是簡(jiǎn)單的封裝了Memcached Java版的客戶端,主要是對(duì)于配置的簡(jiǎn)化以及Memcached多點(diǎn)備份作了一些工作,然后就作為ASF的組件一部分提供給其他Team使用。其實(shí)看過(guò)Memcached Java客戶端代碼的人就會(huì)了解其實(shí)客戶端的事情很簡(jiǎn)單,就是要有一套高性能的Socket通信框架以及對(duì)Memcached的私有協(xié)議實(shí)現(xiàn)的接口,自己去做這些事情也是很簡(jiǎn)單的,不過(guò)既然有可以滿足自己需求的開(kāi)源部分,那么就去實(shí)現(xiàn)自己需要的但沒(méi)有實(shí)現(xiàn)的。這里我用的是Whalin的客戶端版本,這里為什么還要提出來(lái)講這個(gè),后面會(huì)提到。
在對(duì)Java客戶端作了簡(jiǎn)單封裝和擴(kuò)展以后,由于其他Team使用的沒(méi)有什么特殊需求,也就沒(méi)有再去做太多的修改,直到最近自己的服務(wù)集成平臺(tái)需要做服務(wù)訪問(wèn)控制,才重新豐富了Cache組件,也就是這個(gè)過(guò)程中對(duì)于Memcached的一些特性和小的細(xì)節(jié)有了一些新的認(rèn)識(shí)。
作為服務(wù)集成平臺(tái)需要對(duì)服務(wù)有所監(jiān)控,包括訪問(wèn)頻率控制以及訪問(wèn)次數(shù)控制。頻率控制其實(shí)很類似于硬件方面的頻率控制,例如硬件可以對(duì)IP的高頻率訪問(wèn)視為攻擊,列入黑名單。而作為服務(wù)的訪問(wèn),對(duì)于服務(wù)訪問(wèn)者的控制其實(shí)涉及到了業(yè)務(wù)參數(shù),那么硬件就不是很適合去做這方面的控制,為此我也考慮了很久,最開(kāi)始打算在Apache上做一個(gè)模塊控制,但是最后覺(jué)得還是放在后面的業(yè)務(wù)框架上做這件事情。當(dāng)然后面我說(shuō)說(shuō)的方案可能并不好,但是也算是一種想法。要把頻繁的訪問(wèn)數(shù)據(jù)記錄下來(lái)同時(shí)分析,那么數(shù)據(jù)庫(kù)肯定是不行的,最簡(jiǎn)單的方式就是采用Cache,又因?yàn)槭羌悍秶鷥?nèi)的控制,那么集中式Cache就非Memcached莫數(shù)了(分布式的Cache傳播本身?yè)p耗太大,集中式Cache本來(lái)的最大缺點(diǎn)就是單點(diǎn),但作簡(jiǎn)單的備份操作就可以基本解決此類問(wèn)題)。
作為解決這個(gè)問(wèn)題的方法來(lái)說(shuō)只需要實(shí)現(xiàn)兩部分工作:訪問(wèn)計(jì)數(shù)器,定時(shí)任務(wù)。定時(shí)任務(wù)在我做日志分析框架的時(shí)候都是采用了Jdk5的Concurrent包里面的ScheduledExecutorService,這個(gè)作簡(jiǎn)單的循環(huán)任務(wù)足夠用了,同時(shí)也是有很好的多線程異步支持,復(fù)雜一點(diǎn)么用Quartz。計(jì)數(shù)器就要靠Memcached來(lái)實(shí)現(xiàn)了,本來(lái)一般的Cache最大的問(wèn)題就是高并發(fā)下的事務(wù)保證,如果采用Get+Set來(lái)完成計(jì)數(shù)的話,那么高并發(fā)下計(jì)數(shù)器就會(huì)出現(xiàn)讀寫不一致性的問(wèn)題,幸好Memcached提供了計(jì)數(shù)累加功能,讓這種累加動(dòng)作能夠在服務(wù)端一次做好,服務(wù)端控制并發(fā)寫入,保證數(shù)據(jù)的一致性。
下面就看看以下幾個(gè)方法:
boolean storeCounter(String key, long count):存儲(chǔ)key的計(jì)數(shù)器,值為count。
long getCounter(String key):獲取key的計(jì)數(shù)器,如果不存在返回-1。
long addOrDecr(String key, long decr):計(jì)數(shù)器值減去decr,如果計(jì)數(shù)器不存在,保存decr作為計(jì)數(shù)器值
long addOrIncr(String key, long inc):計(jì)數(shù)器值增加inc,如果計(jì)數(shù)器不存在,保存inc作為計(jì)數(shù)器值
long decr(String key, long decr):與addOrDecr不同的是在計(jì)數(shù)器不存在的時(shí)候不保存任何值,返回-1
long incr(String key, long inc) :與addOrIncr不同的是在計(jì)數(shù)器不存在的時(shí)候不保存任何值,返回-1
這里需要說(shuō)明幾點(diǎn):
storeCounter和普通的set方法不同,如果通過(guò)set方式置入key:value的話,getCounter等其他四個(gè)方法都認(rèn)為技術(shù)器不存在。所以Counter的存儲(chǔ)方式是和普通內(nèi)容存儲(chǔ)不同的。
在不同的場(chǎng)景要慎用addOrXXXX和XXXX的方法,兩者還是有比較大的區(qū)別的。
計(jì)數(shù)器沒(méi)有提供移除特殊方法,使用delete方法可以移除計(jì)數(shù)器,但是頻繁的delete和addOrXXXX有時(shí)候會(huì)出現(xiàn)一些奇怪的問(wèn)題(例如同名的計(jì)數(shù)器就沒(méi)有辦法再次被創(chuàng)建,不過(guò)這個(gè)還需要進(jìn)一步的去研究一下看看)。一般情況下如果計(jì)數(shù)器的key不是很多,同時(shí)也會(huì)被復(fù)用,那么可以通過(guò)置為0或者減去已經(jīng)分析過(guò)的數(shù)量來(lái)復(fù)位。
有上面的一套計(jì)數(shù)器機(jī)制就可以很方便的實(shí)現(xiàn)Memcached的計(jì)數(shù)功能,但是又一個(gè)問(wèn)題出現(xiàn)了,如何讓定時(shí)任務(wù)去遍歷計(jì)數(shù)器,分析計(jì)數(shù)器是否到了閥值,觸發(fā)創(chuàng)建黑名單記錄的工作。早先我同事希望我能夠提供封裝好的keySet接口,但是我自己覺(jué)得其實(shí)作為Cache來(lái)說(shuō)簡(jiǎn)單就是最重要的,Cache不需要去遍歷。首先使用Cache的角色就應(yīng)該知道Key,然后去Cache里面找,找不到就去后臺(tái)例如DB里面去搜索,然后將搜索的結(jié)果在考慮更新到Cache里面,這樣才是最高效并且最可靠的,Cache靠不住阿,隨時(shí)都可能會(huì)丟失或者崩潰,因此作為類似于一級(jí)緩存或者這類數(shù)據(jù)完整性要求不高,性能要求很高的場(chǎng)景使用最合適。當(dāng)時(shí)就沒(méi)有提供這樣的接口,直到今天自己需要了,才考慮如何去做這件事情。
開(kāi)始考慮是否能夠?qū)?/span>key都記錄在另外的Cache中或者是Memcached中,首先在高并發(fā)下更新操作就是一大問(wèn)題,再者Memcached的內(nèi)存分配回收機(jī)制以及Value的大小限制都不能滿足這樣的需求,如果使用數(shù)據(jù)庫(kù),那么頻繁更新操作勢(shì)必不可行,采用異步緩存刷新又有一個(gè)時(shí)間間隔期,同時(shí)更新也不是很方便。最后考慮如果能夠讓Memcached實(shí)現(xiàn)Keyset那么就是最好的解決方案,網(wǎng)上搜索了一下,找到一種策略,然后自己優(yōu)化了一下,優(yōu)化后的代碼如下:

Code
@SuppressWarnings("unchecked")
public Set keySet(int limit,boolean fast)
{
Set<String> keys = new HashSet<String>();
Map<String,Integer> dumps = new HashMap<String,Integer>();
Map slabs = getCacheClient().statsItems();
if (slabs != null && slabs.keySet() != null)
{
Iterator itemsItr = slabs.keySet().iterator();
while(itemsItr.hasNext())
{
String server = itemsItr.next().toString();
Map itemNames = (Map) slabs.get(server);
Iterator itemNameItr = itemNames.keySet().iterator();
while(itemNameItr.hasNext())
{
String itemName = itemNameItr.next().toString();
// itemAtt[0] = itemname
// itemAtt[1] = number
// itemAtt[2] = field
String[] itemAtt = itemName.split(":");
if (itemAtt[2].startsWith("number"))
dumps.put(itemAtt[1], Integer.parseInt(itemAtt[1]));
}
}
if (!dumps.values().isEmpty())
{
Iterator<Integer> dumpIter = dumps.values().iterator();
while(dumpIter.hasNext())
{
int dump = dumpIter.next();
Map cacheDump = statsCacheDump(dump,limit);
Iterator entryIter = cacheDump.values().iterator();
while (entryIter.hasNext())
{
Map items = (Map)entryIter.next();
Iterator ks = items.keySet().iterator();
while(ks.hasNext())
{
String k = (String)ks.next();
try
{
k = URLDecoder.decode(k,"UTF-8");
}
catch(Exception ex)
{
Logger.error(ex);
}
if (k != null && !k.trim().equals(""))
{
if (fast)
keys.add(k);
else
if (containsKey(k))
keys.add(k);
}
}
}
}
}
}
return keys;
}
對(duì)于上面代碼的了解需要從Memcached內(nèi)存分配和回收機(jī)制開(kāi)始,以前接觸Memcached的時(shí)候只是了解,這部分代碼寫了以后就有些知道怎么回事了。Memcached為了提高內(nèi)存的分配和回收效率,采用了slab和dump分區(qū)的概念。Memcached一大優(yōu)勢(shì)就是能夠充分利用Memory資源,將同機(jī)器或者不同機(jī)器的Memcached服務(wù)端組合成為對(duì)客戶端看似統(tǒng)一的存儲(chǔ)空間,Memcached可以在一臺(tái)機(jī)器上開(kāi)多個(gè)端口作為服務(wù)端多個(gè)實(shí)例,也可以在多臺(tái)機(jī)器上開(kāi)多個(gè)服務(wù)實(shí)例,而slab就是Memcached的服務(wù)端。下面是我封裝后的Cache配置:
配置
<?xml version="1.0" encoding="UTF-8"?>
<memcached>
<client name="mclient0" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool0">
<!--errorHandler></errorHandler-->
</client>
<client name="mclient1" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool1">
<!--errorHandler></errorHandler-->
</client>
<client name="mclient11" compressEnable="true" defaultEncoding="UTF-8" socketpool="pool11">
<!--errorHandler></errorHandler-->
</client>
<socketpool name="pool0" failover="true" initConn="10" minConn="5" maxConn="250" maintSleep="0"
nagle="false" socketTO="3000" aliveCheck="true">
<servers>10.2.225.210:13000,10.2.225.210:13001,10.2.225.210:13002</servers>
</socketpool>
<socketpool name="pool1" failover="true" initConn="10" minConn="5" maxConn="250" maintSleep="0"
nagle="false" socketTO="3000" aliveCheck="true">
<servers>10.2.225.210:13000</servers>
</socketpool>
<socketpool name="pool11" failover="true" initConn="10" minConn="5" maxConn="250" maintSleep="0"
nagle="false" socketTO="3000" aliveCheck="true">
<servers>10.2.225.210:13000</servers>
</socketpool>
<cluster name="cluster1">
<memCachedClients>mclient1,mclient11</memCachedClients>
</cluster>
</memcached>
可以看到其實(shí)pool才是最終連接服務(wù)端的配置,看看pool0,它會(huì)連接10.2.225.210:13000,10.2.225.210:13001,10.2.225.210:13002這些機(jī)器和他們的端口,但是對(duì)于使用pool0的mclient0來(lái)說(shuō)它僅僅只是知道有一個(gè)叫做mclient0的cache可以保存數(shù)據(jù)。此時(shí)slab就有三個(gè):10.2.225.210:13000和10.2.225.210:13001和10.2.225.210:13002。
當(dāng)一個(gè)key:value要被放入到Memcached中,首先Memcached會(huì)根據(jù)key的hash算法獲取到hash值來(lái)選擇被分配的slab,然后根據(jù)value選擇適合的dump區(qū)。所謂dump區(qū)其實(shí)就是根據(jù)value的大小來(lái)將內(nèi)存按照存儲(chǔ)單元內(nèi)容大小分頁(yè)。這個(gè)是可以配置Memcached的,例如Memcached將slab中的內(nèi)存劃分成4個(gè)dump,第一dump區(qū)存儲(chǔ)0-50k大小的數(shù)據(jù),第二dump區(qū)存儲(chǔ)50-100k的數(shù)據(jù),第三dump區(qū)存儲(chǔ)100-500k的數(shù)據(jù),第四dump區(qū)存儲(chǔ)500-1000K的數(shù)據(jù)。那么當(dāng)key:value需要被寫入的時(shí)候,很容易定位到value所處的dump,分配內(nèi)存給value。這種分dump模式簡(jiǎn)化內(nèi)存管理,加速了內(nèi)存回收和分配。但是這里需要注意的幾點(diǎn)就是,首先當(dāng)你的應(yīng)用場(chǎng)景中保存的數(shù)據(jù)大小離散度很高,那么就不是很適合Memcached的這種分配模式,容易造成浪費(fèi),例如第一dump區(qū)已經(jīng)滿了,第二第三dump區(qū)都還是只有一個(gè)數(shù)據(jù),那么第二第三dump區(qū)不會(huì)被回收,第二第三dump區(qū)的空間就浪費(fèi)了。同時(shí)Memcached對(duì)于value的大小支持到1M,大于1M的內(nèi)容不適合Memcached存儲(chǔ)。其實(shí)在Cache的設(shè)計(jì)中這樣的情況發(fā)生本來(lái)就證明設(shè)計(jì)有問(wèn)題,Cache只是加速,一般保存都是較小的id或者小對(duì)象,用來(lái)驗(yàn)證以及為數(shù)據(jù)定位作精準(zhǔn)細(xì)化,而大數(shù)據(jù)量的內(nèi)容還是在數(shù)據(jù)庫(kù)等存儲(chǔ)中。
知道了基本的分配機(jī)制以后再回過(guò)頭來(lái)看看代碼:
Map slabs = getCacheClient().statsItems();//獲取所有的slab
//用來(lái)收集所有slab的dump號(hào)
while(itemsItr.hasNext())
{
String server = itemsItr.next().toString();
Map itemNames = (Map) slabs.get(server);
Iterator itemNameItr = itemNames.keySet().iterator();
while(itemNameItr.hasNext())
{
String itemName = itemNameItr.next().toString();
// itemAtt[0] = itemname
// itemAtt[1] = number
// itemAtt[2] = field
String[] itemAtt = itemName.split(":");
// 如果是itemName中是:number來(lái)表示,那么證明是一個(gè)存儲(chǔ)數(shù)據(jù)的dump,還有一些是age的部分
if (itemAtt[2].startsWith("number"))
dumps.put(itemAtt[1], Integer.parseInt(itemAtt[1]));
}
}
//根據(jù)收集到的dump來(lái)獲取keys
if (!dumps.values().isEmpty())
{
Iterator<Integer> dumpIter = dumps.values().iterator();
while(dumpIter.hasNext())
{
int dump = dumpIter.next();
// statsCacheDump支持三個(gè)參數(shù)String[],int,int,第一個(gè)參數(shù)可以省略,默認(rèn)填入null,表示從那些slab中獲取dump號(hào)為第二個(gè)參數(shù)的keys,如果是null就從當(dāng)前所有的slab中獲取。第二個(gè)參數(shù)表示dump號(hào),第三個(gè)參數(shù)表示返回最多多少個(gè)結(jié)果。
Map cacheDump = statsCacheDump(dump,limit);
Iterator entryIter = cacheDump.values().iterator();
while (entryIter.hasNext())
{
Map items = (Map)entryIter.next();
Iterator ks = items.keySet().iterator();
while(ks.hasNext())
{
String k = (String)ks.next();
try
{
//這里為什么要作decode,因?yàn)槠鋵?shí)在我使用的這個(gè)Java客戶端存儲(chǔ)的時(shí)候,默認(rèn)會(huì)把key都作encoding一次,所以必須要做,不然會(huì)出現(xiàn)問(wèn)題。
k = URLDecoder.decode(k,"UTF-8");
}
catch(Exception ex)
{
Logger.error(ex);
}
if (k != null && !k.trim().equals(""))
{
//這里的fast參數(shù)是在方法參數(shù)中傳入,作用是什么,其實(shí)采用這種搜索slab以及dump的方式獲取keys會(huì)發(fā)現(xiàn)返回的可能還有一些已經(jīng)移除的內(nèi)容的keys,如果覺(jué)得需要準(zhǔn)確的keys,就在做一次contains的檢查,不過(guò)速度就會(huì)有一定的影響。
if (fast)
keys.add(k);
else
if (containsKey(k))
keys.add(k);
}
}
}
}
}
NET技術(shù):Memcached使用點(diǎn)滴,轉(zhuǎn)載需保留來(lái)源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。