可以看到其实pool才是最终连接服务端的配置,看看pool0,它会连接10.2.225.210:13000,10.2.225.210:13001,10.2.225.210:13002这些机器和他们的端口,但是对于使用pool0的mclient0来说它仅仅只是知道有一个叫做mclient0的cache可以保存数据。此时slab就有三个:10.2.225.210:13000和10.2.225.210:13001和10.2.225.210:13002。
当一个key:value要被放入到Memcached中,首先Memcached会根据key的hash算法获取到hash值来选择被分配的slab,然后根据value选择适合的dump区。所谓dump区其实就是根据value的大小来将内存按照存储单元内容大小分页。这个是可以配置Memcached的,例如Memcached将slab中的内存划分成4个dump,第一dump区存储0-50k大小的数据,第二dump区存储50-100k的数据,第三dump区存储100-500k的数据,第四dump区存储500-1000K的数据。那么当key:value需要被写入的时候,很容易定位到value所处的dump,分配内存给value。这种分dump模式简化内存管理,加速了内存回收和分配。但是这里需要注意的几点就是,首先当你的应用场景中保存的数据大小离散度很高,那么就不是很适合Memcached的这种分配模式,容易造成浪费,例如第一dump区已经满了,第二第三dump区都还是只有一个数据,那么第二第三dump区不会被回收,第二第三dump区的空间就浪费了。同时Memcached对于value的大小支持到1M,大于1M的内容不适合Memcached存储。其实在Cache的设计中这样的情况发生本来就证明设计有问题,Cache只是加速,一般保存都是较小的id或者小对象,用来验证以及为数据定位作精准细化,而大数据量的内容还是在数据库等存储中。
知道了基本的分配机制以后再回过头来看看代码:
Map slabs = getCacheClient().statsItems();//获取所有的slab
//用来收集所有slab的dump号
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来表示,那么证明是一个存储数据的dump,还有一些是age的部分
if (itemAtt[2].startsWith("number"))
dumps.put(itemAtt[1], Integer.parseInt(itemAtt[1]));
}
}
//根据收集到的dump来获取keys
if (!dumps.values().isEmpty())
{
Iterator<Integer> dumpIter = dumps.values().iterator();
while(dumpIter.hasNext())
{
int dump = dumpIter.next();
// statsCacheDump支持三个参数String[],int,int,第一个参数可以省略,默认填入null,表示从那些slab中获取dump号为第二个参数的keys,如果是null就从当前所有的slab中获取。第二个参数表示dump号,第三个参数表示返回最多多少个结果。
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,因为其实在我使用的这个java客户端存储的时候,默认会把key都作encoding一次,所以必须要做,不然会出现问题。
k = URLDecoder.decode(k,"UTF-8");
}
catch(Exception ex)
{
Logger.error(ex);
}
if (k != null && !k.trim().equals(""))
{
//这里的fast参数是在方法参数中传入,作用是什么,其实采用这种搜索slab以及dump的方式获取keys会发现返回的可能还有一些已经移除的内容的keys,如果觉得需要准确的keys,就在做一次contains的检查,不过速度就会有一定的影响。
if (fast)
keys.add(k);
else
if (containsKey(k))
keys.add(k);
}
}
}
}
}
至此,整个keySet的问题解决了,对于即时监控也基本都作好了,这里需要把过程中的两件小事情说一下。
1. statsCacheDump始终不能用。
刚开始的时候statsCacheDump方法始终报错说连接超时,跟踪到了java客户端代码中发现并不是什么连接超时,只是服务端返回了错误信息,而客户端认为还没有结束一直等待,导致超时。我就顺手给java客户端的开发人员mail了信息求助(代码里面有email)。再仔细看了看出错信息,返回的是不认识该指令的错误,因此就去解压memcached的服务端,看了看它的协议说明,这个Stat方法还是有的,很奇怪,没有办法了,虽然自己对于c不是很懂,但起码大致看懂逻辑还是不难,下载了Memcached的源码一看,发现居然对于StatsCacheDump这个方法调用必须还有一个参数limit,在我手头的客户端代码里面就没有这个参数,所以错误了,本来想扩展一下那个方法,但是那个方法中实现的不是很好,都是private的不容易扩展,这时候居然收到其中一个客户端开发者的回复邮件,说我手头的代码太老了,同时不建议去实现keyset,认为这样比较低效。我去下载了一个新版本,看了看源码果然已经修复了,我就回了邮件表示感谢,同时也和他说明了这么做的原因。因此大家如果要和我一样写上面的代码,就需要它2.0.1的那个版本。这里对那些国外的开源工作者表示敬佩,对于开发者是很负责任的。
2.关于fast那个选项
这个是我加上去的,做了一下测试,例如我先执行如下代码:
Cache.set(“key1”,”value1”);
Cache.set(“key2”,”value2”);
Cache.flushAll(null);
Cache.set(“key3”,”value3”);
Cache.set(“key4”,”value4”);
Boolean fast = true;
Set keys = Cache.keySet(fast);
system.out.println(keys);
Fast = false;
keys = Cache.keySet(fast);
system.out.println(keys);
得到的结果为:
Key1,key2,key3,key4
Key3,key4
可以看到其实如果通过StatsCacheDump来获取得到的keys会参杂一些已经失效的keys,只是没有回收,本来尝试获取时间戳来做判断,不过还不如使用containsKey来的有效。
同时这里采用containsKey而不是用get,就是因为counter是不能用get获得的,即使counter存在。