您的位置:首页 > 数据库 > Memcache

Memcached 使用举例及原理解释

2017-03-17 09:30 246 查看
代码示例:

import Java.util.Date;
import com.danga.MemCached.MemCachedClient;
import com.danga.MemCached.SockIOPool;
/**
* 使用memcached的缓存实用类.
*
* @author Aires
*
*/
public class MemcacheDemo
{
// 创建全局的唯一实例
protected static MemCachedClient mcc = new MemCachedClient();
protected static MemcacheDemo= new MemcacheDemo();
// 设置与缓存服务器的连接池
static {
// 服务器列表和其权重
String[] servers = {"10.62.100.78:12306"};
Integer[] weights = {3};
// 获取socke连接池的实例对象
SockIOPool pool = SockIOPool.getInstance();
// 设置服务器信息
pool.setServers( servers );
pool.setWeights( weights );
// 设置初始连接数、最小和最大连接数以及最大处理时间
pool.setInitConn( 5 );
pool.setMinConn( 5 );
pool.setMaxConn( 250 );
pool.setMaxIdle( 1000 * 60 * 60 * 6 );
// 设置主线程的睡眠时间
pool.setMaintSleep( 30 );
// 设置TCP的参数,连接超时等
pool.setNagle( false );
pool.setSocketTO( 3000 );
pool.setSocketConnectTO( 0 );
// 初始化连接池
pool.initialize();
// 压缩设置,超过指定大小(单位为K)的数据都会被压缩
mcc.setCompressEnable( true );
mcc.setCompressThreshold( 64 * 1024 );
}

/**
* 保护型构造方法,不允许实例化!
*/
protected MemcacheDemo()
{
}

/**
* 获取唯一实例.
* @return
*/
public static MemcacheDemo getInstance()
{
return memCached;
}

/**
* 添加一个指定的值到缓存中.
* @param key
* @param value
* @return
*/
public boolean add(String key, Object value)
{
return mcc.add(key, value);
}

public boolean add(String key, Object value, Date expiry)
{
return mcc.add(key, value, expiry);
}
//更新
public boolean replace(String key, Object value)
{
return mcc.replace(key, value);
}

public boolean replace(String key, Object value, Date expiry)
{
return mcc.replace(key, value, expiry);
}

//删除
public boolean delete(String key, Object value, Date expiry)
{
return mcc.delete(key, expiry);
}
/**
* 根据指定的关键字获取对象.
* @param key
* @return
*/
public Object get(String key)
{
return mcc.get(key);
}

public static void main(String[] args)
{
test cache = test.getInstance();
//cache.add("hello", 234);
cache.add("hello", 234);
System.out.print("get value : " + cache.get("hello"));

testjopo tb = new testjopo();
tb.setName("Aires");
cache.add("bean", tb);
testjopo tb1 = (testjopo)cache.get("bean");
System.out.print("name=" + tb1.getName());
tb1.setName("Modify by  aires");
tb1 = (testjopo)cache.get("bean");
System.out.print("name=" + tb1.getName());
}
}


Memcached是一种集中式Cache,支持分布式横向扩展。这里需要有点说明,很多开发者觉得Memcached是一种分布式Cache,但是其实Memcached服务端本身是单实例的,只是在客户端实现过程中可以根据存储的主键作分区存储,而这个区就是Memcached服务端的一个或者多个实例,如果将客户端也囊括到Memcached中,那么可以部分概念上说是集中式的。其实回顾一下集中式的构架,无非两种情况:1.节点均衡的网状(JBoss Tree Cache),利用JGroup的多播通信机制来同步数据。2.Master-Slaves模式(分布式文件系统),由Master来管理Slave,如何选择Slave,如何迁移数据,都是由Master来完成,但是Master本身也存在单点问题。

总结几个它的特点来理解一下它的优点和限制。

Memory:内存存储,不言而喻,速度快,对于内存的要求高,不指出的话所缓存的内容非持久化。对于CPU要求很低,所以常常采用将Memcached服务端和一些CPU高消耗Memory低消耗应用部属在一起。(作为我们AEP正好有这样的环境,我们的接口服务器有多台,接口服务器对于CPU要求很高(由于WS-Security),但是对于Memory要求很低,因此可以用作Memcached的服务端部属机器)

集中式Cache:避开了分布式Cache的传播问题,但是需要非单点保证其可靠性,这个就是后面集成中所作的cluster的工作,可以将多个Memcached作为一个虚拟的cluster,同时对于cluster的读写和普通的memcached的读写性能没有差别。

分布式扩展:Memcached的很突出一个优点,就是采用了可分布式扩展的模式。可以将部属在一台机器上的多个Memcached服务端或者部署在多个机器上的Memcached服务端组成一个虚拟的服务端,对于调用者来说完全屏蔽和透明。提高的单机器的内存利用率,也提供了scale out的方式。

Socket通信:传输内容的大小以及序列化的问题需要注意,虽然Memcached通常会被放置到内网作为Cache,Socket传输速率应该比较高(当前支持Tcp和udp两种模式,同时根据客户端的不同可以选择使用nio的同步或者异步调用方式),但是序列化成本和带宽成本还是需要注意。这里也提一下序列化,对于对象序列化的性能往往让大家头痛,但是如果对于同一类的Class对象序列化传输,第一次序列化时间比较长,后续就会优化,其实也就是说序列化最大的消耗不是对象序列化,而是类的序列化。如果穿过去的只是字符串,那么是最好的,省去了序列化的操作,因此在Memcached中保存的往往是较小的内容。

特殊的内存分配机制:首先要说明的是Memcached支持最大的存储对象为1M。它的内存分配比较特殊,但是这样的分配方式其实也是对于性能考虑的,简单的分配机制可以更容易回收再分配,节省对于CPU的使用。这里用一个酒窖比喻来说明这种内存分配机制,首先在Memcached起来的时候可以通过参数设置使用的总共的Memory,这个就是建造一个酒窖,然后在有酒进入的时候,首先申请(通常是1M)的空间,用来建酒架,酒架根据这个酒瓶的大小分割酒架为多个小格子安放酒瓶,将同样大小范围内的酒瓶都放置在一类酒架上面。例如20cm半径的酒瓶放置在可以容纳20-25cm的酒架A上,30cm半径的酒瓶就放置在容纳25-30cm的酒架B上。回收机制也很简单,首先新酒入库,看看酒架是否有可以回收的地方,如果有直接使用,如果没有申请新的地方,如果申请不到,采用配置的过期策略。这个特点来看,如果要放的内容大小十分离散,同时大小比例相差梯度很明显,那么可能对于使用空间来说不好,可能在酒架A上就放了一瓶酒,但占用掉了一个酒架的位置。

Cache机制简单:有时候很多开源的项目做的面面俱到,但是最后也就是因为过于注重一些非必要性的功能而拖累了性能,这里要提到的就是Memcached的简单性。首先它没有什么同步,消息分发,两阶段提交等等,它就是一个很简单的Cache,把东西放进去,然后可以取出来,如果发现所提供的Key没有命中,那么就很直白的告诉你,你这个key没有任何对应的东西在缓存里,去数据库或者其他地方取,当你在外部数据源取到的时候,可以直接将内容置入到Cache中,这样下次就可以命中了。这里会提到怎么去同步这些数据,两种方式,一种就是在你修改了以后立刻更新Cache内容,这样就会即时生效。另一种是说容许有失效时间,到了失效时间,自然就会将内容删除,此时再去去的时候就会命中不了,然后再次将内容置入Cache,用来更新内容。后者用在一些时时性要求不高,写入不频繁的情况。

客户端的重要性:Memcached是用C写的一个服务端,客户端没有规定,反正是Socket传输,只要语言支持Socket通信,通过Command的简单协议就可以通信,但是客户端设计的合理十分重要,同时也给使用者提供了很大的空间去扩展和设计客户端来满足各种场景的需要,包括容错,权重,效率,特殊的功能性需求,嵌入框架等等。

几个应用点:小对象的缓存(用户的token,权限信息,资源信息)。小的静态资源缓存。Sql结果的缓存(这部分用的好,性能提高相当大,同时由于Memcached自身提供**scale out**,那么对于db scale out的老大难问题无疑是一剂好药)。ESB消息缓存。

MemCached Cache在大型网站被应用得越来越广泛,不同语言的客户端也都在官方网站上有提供,但是Java的选择并不多。由于现在的MemCached Cache服务端是用C写的,因此我这个C不太熟悉的人也就没有办法去优化它,当然对于它的内存分配机制等细节还是有所了解,因此在使用的时候也会十分注意,这些文章Google一把应该也有很多了。这里就说说对于MemCache Java客户端的优化的两个阶段。


First Stage

第一阶段主要是在官方推荐的Java客户端之一whalin开源实现基础上作了再次封装。

Cache服务接口化。

定义了IMemCache接口,在应用部分仅仅只是使用接口,为将来替换Cache服务实现提供基础。

使用配置代替代码初始化客户端。

通过配置客户端和SocketIO Pool属性,直接交管由CacheManager来维护Cache Client Pool的生命周期,方便实用以及单元测试。

KeySet的实现。

对于MemCached来说本身是不提供KeySet的方法的,但是SIP的一个场景的出现,让我不得不去实现了KeySet。

SIP在作服务访问频率控制的时候需要记录在控制间隔期内的访问次数和流量,此时由于是集群,因此数据必须放在集中式的存储或者缓存中,数据库肯定是撑不住这样大数据量的更新频率的,因此考虑使用Memcached的很出彩的操作,全局计数器(storeCounter,getCounter,inc,dec),但是在检查计数器的时候如何去获取当前所有的计数器,曾考虑使用DB或者文件,但是效率还是问题,同时如果放在一个字段中并发还是有问题。因此不得不实现了KeySet,在使用KeySet的时候有一个参数,类型是Boolean,这个字段的存在是因为,在Memcached中数据的删除并不是直接删除,而是标注一下,这样会导致实现keySet的时候取出可能已经删除的数据,如果对于数据严谨性要求低,速度要求高,那么不需要再去验证key是否真的有效,如果要求key必须正确存在,就需要再多一次的轮询查找。

Cluster的实现。

Memcached作为集中式Cache,就存在着集中式的致命问题:单点问题,Memcached支持多Instance分布在多台机器上,仅仅只是解决了数据全部丢失的问题,但是当其中一台机器出错以后,还是会导致部分数据的丢失,一个篮子掉在地上还是会把部分的鸡蛋打破。

因此就需要实现一个备份机制,能够保证Memcached在部分失效以后,数据还能够依然使用,当然大家很多时候都用Cache不命中就去数据源获取的策略,但是在SIP的场景中,如果部分信息找不到就去数据库查找,那么要把SIP弄垮真的是很容易,因此SIP对于Memcached中的数据认为是可信的,因此做Cluster也是必要的。

(1) 应用传入需要操作的key,通过CacheManager获取配置在Cluster中的客户端。

(2) 当获得Cache Client以后,执行Cache操作。

(3) A.如果是读取操作,当不能命中时去集群其他Cache客户端获取数据,如果获取到数据,尝试写入到本次获得的Cache客户端,并返回结果。(达到数据恢复的作用)

B.如果是更新操作,在本次获取得Cache客户端执行更新操作以后,立即返回,将更新集群其他机器命令提交给客户端的异步更新线程对列去异步执行。(由于如果是根据key来获取Cache,那么异步执行不会影响到此主键的查询操作)

存在的问题:如果是设置了Timeout的数据,那么在丢失以后被复制的过程中就会变成永久有效的内容。

LocalCache结合Memcached使用,提高数据获取效率。

在第一次压力测试过程中,发现和原先预料的一样,Memcached并不是完全无损失的,Memcached是通过Socket数据交互来进行通信的,因此机器的带宽,网络IO,Socket连接数都是制约Memcached发挥其作用的障碍。Memcache的一个突出优点就是Timeout的设置,也就是放入进去的数据可以设置有效期,自动会失效,这样对于一些不敏感的数据就可以在一定的容忍时间内不去更新,提高效率。根据这个思想,其实在集群中的每一个Memcached客户端也可以使用本地的Cache,来缓存获取过的数据,设置一定的失效时间,来减少对于Memcached的访问次数,提高整体性能。

因此,在每一个客户端中内置了一个有超时机制的本地缓存(采用lazy timeout机制),在获取数据的时候,首先在本地查询数据是否存在,如果不存在则再向Memcache发起请求,获得数据以后,将其缓存在本地,并设置有效时间。方法定义如下:


/

* 降低memcache的交互频繁造成的性能损失,因此采用本地cache结合memcache的方式

* @param key

* @param 本地缓存失效时间单位秒

* @return

*/

public Object get(String key,int localTTL);**

Second Stage

第一阶段的封装基本上已经可以满足现有的需求,但是Memcache客户端里面在SocketIO代码里面有太多的synchronized,多多少少会影响性能。虽然过去看过这部分代码,但是当时只是关注里面的Hash算法,那天回去后一看,果然有不少的synchronized,可能是与客户端当时写的时候Jdk版本较早的缘故造成的,现在Concurrent包被广泛应用,因此优化并不是一件很难的事情。但是由于原有whalin没有提供扩展的接口,因此不得不将whalin除了SockIO部分全部纳入到封装过的客户端中,然后改造SockIO部分。


一. 优化synchronized部分。在原有代码中SockIO的资源池分成三个池(普通Map实现),Free,Busy,Dead,然后根据SockIO使用情况来维护这三个资源池。

优化方式,首先简化资源池,只有一个资源池,设置一个状态池,在变更资源状态的过程时仅仅变更资源池中的内容。再次,用ConcurrentMap来替代Map,同时使用putIfAbsent方法来简化Synchronized,具体的代码可以看open source的代码部分。

二. 原以为这优化后,效率应该会有很大的提高,但是在初次压力测试后发现,并没有明显的提高,看来有其他地方的耗时远远大于连接池资源维护,因此用JProfiler作了性能分析,发现了最大的一个瓶颈:read数据部分,原有设计中读取数据是按照单字节读取,然后逐步分析,为的仅仅就是遇到协议中的分割符可以识别,但是循环read单字节和批量分页read性能相差很大,因此内置了读入缓存页(可设置大小),然后再按照协议的需求去读取和分析数据,效率得到了很大的提高。具体的看最后部分的压力测试结果。

上面两部分的工作不论是否提升了性能,但是对于客户端本身来说都是有意义的,当然提升性能给应用带来的吸引力更大。这部分细节内容可以参看代码实现部分,对于调用者来说完全没有任何功能影响,仅仅只是性能。

open source: http://code.google.com/p/memcache-client-forjava/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  缓存 memcached