Memcached是一种集中式Cache,支持分布式横向扩展。这里须要解释说明一下,不少开发者以为Memcached是一种分布式缓存系统, 可是其实Memcached服务端自己是单实例的,只是在客户端实现过程当中能够根据存储的主键作分区存储,而这个区就是Memcached服务端的一个或 者多个实例,若是将客户端也囊括到Memcached中,那么能够部分概念上说是集中式的。其实回顾一下集中式的构架,无非两种状况:一是节点均衡的网状 (JBoss Tree Cache),利用JGroup的多播通讯机制来同步数据;二是Master-Slaves模式(分布式文件系统),由Master来管理Slave,比 如如何选择Slave,如何迁移数据等都是由Master来完成,可是Master自己也存在单点问题。下面再总结几个它的特色来理解一下其优势和限制。 java
内存存储:不言而喻,速度快,但对于内存的要求高。这种状况对CPU要求很低,因此经常采用将 Memcached服务端和一些CPU高消耗内存、低消耗应用部署在一块儿。(咱们的某个产品正好有这样的环境,咱们的接口服务器有多台,它们对CPU要求 很高——缘由在于WS-Security的使用,可是对于内存要求很低,所以能够用做Memcached的服务端部署机器)。 算法
集中式缓存(Cache):避开了分布式缓存的传播问题,可是须要非单点来保证其可靠性,这个就是后面集成中所做的集群(Cluster)工做,能够将多个Memcached做为一个虚拟的集群,同时对于集群的读写和普通的Memcached的读写性能没有差异。 数据库
分布式扩展:Memcached很突出的一个优势就是采用了可分布式扩展的模式。能够将部署在一台机器上的多个 Memcached服务端或者部署在多个机器上的Memcached服务端组成一个虚拟的服务端,对于调用者来讲则是彻底屏蔽和透明的。这样作既提升了单 机的内存利用率,也提供了向上扩容(Scale Out)的方式。 服务器
Socket通讯:这儿须要注意传输内容的大小和序列化的问题,虽然Memcached一般会被放置到内网做为 缓存,Socket传输速率应该比较高(当前支持TCP和UDP两种模式,同时根据客户端的不一样能够选择使用NIO的同步或者异步调用方式),可是序列化 成本和带宽成本仍是须要注意。这里也提一下序列化,对于对象序列化的性能每每让你们头痛,可是若是对于同一类的Class对象序列化传输,第一次序列化时 间比较长,后续就会优化,也就是说序列化最大的消耗不是对象序列化,而是类的序列化。若是穿过去的只是字符串,这种状况是最理想的,省去了序列化的操做, 所以在Memcached中保存的每每是较小的内容。 网络
特殊的内存分配机制:首先要说明的是Memcached支持最大的存储对象为1M。它的内存分配比较特殊,可是 这样的分配方式其实也是基于性能考虑的,简单的分配机制能够更容易回收再分配,节省对CPU的使用。这里用一个酒窖作比来讲明这种内存分配机制,首先在 Memcached启动的时候能够经过参数来设置使用的全部内存——酒窖,而后在有酒进入的时候,首先申请(一般是1M)的空间,用来建酒架,而酒架根据 这个酒瓶的大小将本身分割为多个小格子来安放酒瓶,并将一样大小范围内的酒瓶都放置在一类酒架上面。例如20厘米半径的酒瓶放置在能够容纳20-25厘米 的酒架A上,30厘米半径的酒瓶就放置在容纳25-30厘米的酒架B上。回收机制也很简单,首先新酒入库,看看酒架是否有能够回收的地方,若是有就直接使 用,若是没有则申请新的地方,若是申请不到,就采用配置的过时策略。从这个特色来看,若是要放的内容大小十分离散,同时大小比例相差梯度很明显的话,那么 可能对于空间使用来讲效果很差,由于极可能在酒架A上就放了一瓶酒,但却占用掉了一个酒架的位置。 多线程
缓存机制简单:有时候不少开源项目作的面面俱到,但到最后由于过于注重一些非必要的功能而拖累了性能,这里提到 的就是Memcached的简单性。首先它没有什么同步,消息分发,两阶段提交等等,它就是一个很简单的缓存,把东西放进去,而后能够取出来,若是发现所 提供的Key没有命中,那么就很直白地告诉你,你这个Key没有任何对应的东西在缓存里,去数据库或者其余地方取;当你在外部数据源取到的时候,能够直接 将内容置入到缓存中,这样下次就能够命中了。这里介绍一下同步这些数据的两种方式:一种是在你修改了之后马上更新缓存内容,这样就会即时生效;另外一种是说 允许有失效时间,到了失效时间,天然就会将内容删除,此时再去取的时候就不会命中,而后再次将内容置入缓存,用来更新内容。后者用在一些实时性要求不高, 写入不频繁的状况。 并发
客户端的重要性:Memcached是用C写的一个服务端,客户端没有规定,反正是Socket传输,只要语言 支持Socket通讯,经过Command的简单协议就能够通讯。可是客户端设计的合理十分重要,同时也给使用者提供了很大的空间去扩展和设计客户端来满 足各类场景的须要,包括容错、权重、效率、特殊的功能性需求和嵌入框架等等。 框架
几个应用点:小对象的缓存(用户的Token、权限信息、资源信息);小的静态资源缓存;SQL结果的缓存(这部分若是用的好,性能提升会至关大,同时因为Memcached自身提供向上扩容,那么对于数据库向上扩容的老大难问题无疑是一剂好药);ESB消息缓存。 异步
MemCached在大型网站被应用得愈来愈普遍,不一样语言的客户端也都在官方网站上有提供,可是Java开发者的选择并很少。因为如今的 MemCached服务端是用C写的,所以我这个C不太熟悉的人也就没有办法去优化它。固然对于它的内存分配机制等细节仍是有所了解,所以在使用的时候也 会十分注意,这些文章在网络上有不少。这里我重点介绍一下对于MemCache系统的Java客户端优化的两个阶段。
第一阶段主要是在官方推荐的Java客户端之一whalin开源实现基础上作再次封装。
所以,在每个客户端中都内置了一个有超时机制的本地缓存(采用Lazy Timeout机制),在获取数据的时候,首先在本地查询数据是否存在,若是不存在则再向Memcache发起请求,得到数据之后,将其缓存在本地,并设置有效时间。方法定义以下:
/** * 下降memcache的交互频繁形成的性能损失,所以采用本地cache结合memcache的方式 * @param key * @param 本地缓存失效时间单位秒 * @return**/ public Object get(String key,int localTTL);
第一阶段的封装基本上已经能够知足现有的需求,也被本身的项目和其余产品线所使用,可是不经意的一句话,让我开始了第二阶段的优化。有同事告诉我说 Memcached客户端的SocketIO代码里面有太多的Synchronized(同步),多多少少会影响性能。虽然过去看过这部分代码,可是当时 只是关注里面的Hash算法。根据同事所说的回去一看,果真有很多的同步,多是做者当时写客户端的时候JDK版本较老的缘故形成的,如今 Concurrent包被普遍应用,所以优化并非一件很难的事情。可是因为原有Whalin没有提供扩展的接口,所以不得不将Whalin除了 SockIO,其他所有归入到封装过的客户端的设想,而后改造SockIO部分。
结果也就有了这个放在Google上的开源客户端:http://code.google.com/p/memcache-client-forjava/。
上面两部分的工做不管是否提高了性能,可是对于客户端自己来讲都是有意义的,固然提高性能给应用带来的吸引力更大。这部分细节内容能够参看代码实现部分,对于调用者来讲彻底没有任何功能影响,仅仅只是性能。
在这个压力测试以前,其实已经作过不少次压力测试了,测试中的数据自己并无衡量Memcached的意义,由于测试是使用我本身的机器,其中性 能、带宽、内存和网络IO都不是服务器级别的,这里仅仅是将使用原有的第三方客户端和改造后的客户端做一个比较。场景就是模拟多用户多线程在同一时间发起 缓存操做,而后记录下操做的结果。
Client版本在测试中有两个:2.0和2.2。2.0是封装调用Whalin Memcached Client 2.0.1版本的客户端实现。2.2是使用了新SockIO的无第三方依赖的客户端实现。checkAlive指的是在使用链接资源之前是否须要验证链接 资源有效(发送一次请求并接受响应),所以启用该设置对于性能来讲会有很多的影响,不过建议仍是使用这个检查。
单个缓存服务端实例的各类配置和操做下比较:
缓存配置 | 用户 | 操做 | 客户端 版本 | 总耗时(ms) | 单线程耗时(ms) | 提升处理能力百分比 |
checkAlive | 100 | 1000 put simple obj 1000 get simple obj |
2.0 2.2 |
13242565 7772767 |
132425 77727 |
+41.3% |
No checkAlive | 100 | 1000 put simple obj 1000 put simple obj |
2.0 2.2 |
7200285 4667239 |
72002 46672 |
+35.2% |
checkAlive | 100 | 1000 put simple obj 2000 get simple obj |
2.0 2.2 |
20385457 11494383 |
203854 114943 |
+43.6% |
No checkAlive | 100 | 1000 put simple obj 2000 get simple obj |
2.0 2.2 |
11259185 7256594 |
112591 72565 |
+35.6% |
checkAlive | 100 | 1000 put complex obj 1000 get complex obj |
2.0 2.2 |
15004906 9501571 |
150049 95015 |
+36.7% |
No checkAlive | 100 | 1000 put complex obj 1000 put complex obj |
2.0 2.2 |
9022578 6775981 |
90225 67759 |
+24.9% |
从上面的压力测试能够看出这么几点,首先优化SockIO提高了很多性能,其次SockIO优化的是get的性能,对于put没有太大的做用。原觉得获取数据越大性能效果提高越明显,但结果并非这样。
单个缓存实例和双缓存实例的测试比较:
缓存配置 | 用户 | 操做 | 客户端 版本 | 总耗时(ms) | 单线程耗时(ms) | 提升处理能力百分比 |
One Cache instance checkAlive |
100 | 1000 put simple obj 1000 get simple obj |
2.0 2.2 |
13242565 7772767 |
132425 77727 |
+41.3% |
Two Cache instance checkAlive |
100 | 1000 put simple obj 1000 put simple obj |
2.0 2.2 |
13596841 7696684 |
135968 76966 |
+43.4% |
结果显示,单个客户端对应多个服务端实例性能提高略高于单客户端对应单服务端实例。
缓存集群的测试比较:
缓存配置 | 用户 | 操做 | 客户端 版本 | 总耗时(ms) | 单线程耗时(ms) | 提升处理能力百分比 |
No Cluster checkAlive |
100 | 1000 put simple obj 1000 get simple obj |
2.0 2.2 |
13242565 7772767 |
132425 77727 |
+41.3% |
Cluster checkAlive |
100 | 1000 put simple obj 1000 put simple obj |
2.0 2.2 |
25044268 8404606 |
250442 84046 |
+66.5% |
这部分和SocketIO优化无关。2.0采用的是向集群中全部客户端更新成功之后才返回的策略,2.2采用了异步更新,而且是分布式客户端节点获取的方式来分散压力,所以提高效率不少。