java之XMemcached使用及源码详解

转载请注明出处:http://blog.csdn.net/tang9140/article/details/43445511

前言

本文主要讲述如何使用XMemcached客户端与Memcached服务端进行交互。通过XMemcached的API调用与Memcached的set/get命令对比及跟踪XMemcached源码,使大家对XMemcached的API有更深层次的理解,能够从底层上去了解其工作原理,从而能在项目中进行一些针对性的接口封闭及优化工作。

是叫Memcache还是Memcached?

网上有种说法是:Memcache是这个项目的名称,而memcached是它服务器端的主程序文件名。我又查了Memcache的官网http://memcached.org/,home页一直引用的是Memcached。姑且不论该叫什么名称合适,在这里统一称呼为Memcached,仅代表我的个人习惯。

Memcached简介

言归正题,Memcached是分布式高性能内存级别的对象缓存系统,并且是开源免费项目。它的所有key-value数据全部放在内存中,这是其高效的一个原因,同时也意味着系统关闭时,全部数据就会丢失。利用Memcached作用缓存系统,可以减少动态网站数据库查询次数,提升网站性能,常作为web2.0网站缓存解决方案。Memcached客户端提供多种语言API支持,像C/C++、Perl、PHP、Java、C#、Ruby等。

Memcached的Java客户端目前有3个

  • Memcached Client for Java 比 SpyMemcached更稳定、更早、更广泛;
  • SpyMemcached 比 Memcached Client for Java更高效;
  • XMemcached 比 SpyMemcache并发效果更好;

前两个客户端的使用,这里不做详述。

分三部分讲解XMemcached客户端

  • XMemcached客户端使用演示
  • set/get方法源码追踪
  • 对比Memcached的set/get命令

一、XMemcached客户端使用演示

本人是用Maven构建的项目,为了使用XMemcached,需要在pom.xml中加入

<dependency>
	<groupId>com.googlecode.xmemcached</groupId>
	<artifactId>xmemcached</artifactId>
	<version>1.4.3</version>
</dependency>

XMemcached使用示例Demo如下

    public static void main(String[] args) throws IOException {
        MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("127.0.0.1:11211"));
        MemcachedClient memcachedClient = builder.build();

        try {
            memcachedClient.set("key", 0, "Hello World!");
            String value = memcachedClient.get("key");
            System.out.println("key值:" + value);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        try {
            memcachedClient.shutdown();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

接下来详细追踪下这两个方法的源码

二、set/get方法源码追踪

1.set

大家可以用过debug模式,一步步追踪set及get过程,具体过程不演示了,先直接列出set方法大概的源码调用过程如下(中间可能省略了某些方法调用)

XMemcachedClient.set()
 XMemcachedClient.sendCommand()
  MemcachedConnector.send()
   AbstractSession.write()
    MemcachedTCPSession.wrapMessage()
     TextStoreCommand.encode()
      TextStoreCommand.encodeValue()
       SerializingTranscoder.encode()
        BaseSerializingTranscoder.serialize()

先是调用XMemcacheClient.set(final String key, final int exp, final Object value)方法,key形参对应字符串“key”,exp形参对应整数0(表达缓存永不过期),value形参对应字符串“Hello World!”。经过上述一系列方法调用,最终调用到SerializingTranscoder.encode(Object o)方法,此时形参o接收到的实参值就是set的字符串“Hello World!”,该方法体代码如下:

	public final CachedData encode(Object o) {
		byte[] b = null;
		int flags = 0;
		if (o instanceof String) {
			b = encodeString((String) o);
		} else if (o instanceof Long) {
			if (this.primitiveAsString) {
				b = encodeString(o.toString());
			} else {
				b = this.transcoderUtils.encodeLong((Long) o);
			}
			flags |= SPECIAL_LONG;
		} else if (o instanceof Integer) {
			if (this.primitiveAsString) {
				b = encodeString(o.toString());
			} else {
				b = this.transcoderUtils.encodeInt((Integer) o);
			}
			flags |= SPECIAL_INT;
		} else if (o instanceof Boolean) {
			if (this.primitiveAsString) {
				b = encodeString(o.toString());
			} else {
				b = this.transcoderUtils.encodeBoolean((Boolean) o);
			}
			flags |= SPECIAL_BOOLEAN;
		} else if (o instanceof Date) {
			b = this.transcoderUtils.encodeLong(((Date) o).getTime());
			flags |= SPECIAL_DATE;
		} else if (o instanceof Byte) {
			if (this.primitiveAsString) {
				b = encodeString(o.toString());
			} else {
				b = this.transcoderUtils.encodeByte((Byte) o);
			}
			flags |= SPECIAL_BYTE;
		} else if (o instanceof Float) {
			if (this.primitiveAsString) {
				b = encodeString(o.toString());
			} else {
				b = this.transcoderUtils.encodeInt(Float
						.floatToRawIntBits((Float) o));
			}
			flags |= SPECIAL_FLOAT;
		} else if (o instanceof Double) {
			if (this.primitiveAsString) {
				b = encodeString(o.toString());
			} else {
				b = this.transcoderUtils.encodeLong(Double
						.doubleToRawLongBits((Double) o));
			}
			flags |= SPECIAL_DOUBLE;
		} else if (o instanceof byte[]) {
			b = (byte[]) o;
			flags |= SPECIAL_BYTEARRAY;
		} else {
			b = serialize(o);
			flags |= SERIALIZED;
		}
		assert b != null;
		if (this.primitiveAsString) {
			// It is not be SERIALIZED,so change it to string type
			if ((flags & SERIALIZED) == 0) {
				flags = 0;
			}
		}
		if (b.length > this.compressionThreshold) {
			byte[] compressed = compress(b);
			if (compressed.length < b.length) {
				if (log.isDebugEnabled()) {
					log.debug("Compressed " + o.getClass().getName() + " from "
							+ b.length + " to " + compressed.length);
				}
				b = compressed;
				flags |= COMPRESSED;
			} else {
				if (log.isDebugEnabled()) {
					log.debug("Compression increased the size of "
							+ o.getClass().getName() + " from " + b.length
							+ " to " + compressed.length);
				}
			}
		}
		return new CachedData(flags, b, this.maxSize, -1);
	}

先是申明了局部变量b(用来存储需要放入memcached服务器的字节数组)及flags(用来存储标志信息)。然后依次判断对象o是否字符串类型、长整型类型等,并将对象o编码成相应的字节数组存放在局部变量b中。

特别注意第57行,当o的类型不是字符串、基本类型的包装类型及byte[]数组时,会调用BaseSerializingTranscoder.serialize()方法,该方法源代码如下:

	protected byte[] serialize(Object o) {
		if (o == null) {
			throw new NullPointerException("Can't serialize null");
		}
		byte[] rv = null;
		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			ObjectOutputStream os = new ObjectOutputStream(bos);
			os.writeObject(o);
			os.close();
			bos.close();
			rv = bos.toByteArray();
		} catch (IOException e) {
			throw new IllegalArgumentException("Non-serializable object", e);
		}
		return rv;
	}

很明显,该方法就是进行对象序列化,将Java对象转化成byte数组并返回。相信大家看到这里,应该明白了为什么自定义对象需要实现Serializable接口才能保存进Memcached中。如果数据对象没有实现Serializable接口,那么在进行对象序列化时,将会抛出IOException,最终抛出IllegalArgumentException,并提示Non-serializable object。

另着重说明下CachedData类的作用,该类封装了cas值(该值用来实现原子更新,即客户端每次发出更新请求时,请求信息中都会附带该cas值,memcached服务端在收到请求后,会将该cas值与服务器中存储数据的cas值对比,如果相等,则用新的数据覆盖老的数据;否则,更新失败。在并发环境下特别有用)、data数据(即要缓存的数据值或者获取到的缓存数据,以byte[]数组形式存储),flag信息(标识byte[]数组额外数据类型信息及byte[]数组是否进行过压缩等信息,用一个int类型存储)及其它信息。

set源码分析到这里,下面说下get源码。

2.get

同样的,先列出get方法大概的源码调用过程如下:

XMemcachedClient.get()
 XMemcachedClient.fetch0()
  XMemcachedClient.sendCommand()
   MemcachedConnector.send()
    AbstractSession.write()
     MemcachedTCPSession.wrapMessage()
      TextGetCommand.encode()
       SerializingTranscoder.decode()
        SerializingTranscoder.decode0()
         BaseSerializingTranscoder.deserialize()

先是调用XMemcacheClient.get(final String key)方法,key形参对应字符串“key"。从该方法一直到TextGetCommand.encode()调用,可以看作是组装get命令并发送到服务器过程,在收到服务器响应消息后,将响应消息组装成CachedData,并调用SerializingTranscoder.decode(CachedData d)方法,即进行字节流解码工作。该方法代码如下:

	public final Object decode(CachedData d) {
		byte[] data = d.getData();

		int flags = d.getFlag();
		if ((flags & COMPRESSED) != 0) {
			data = decompress(d.getData());
		}
		flags = flags & SPECIAL_MASK;
		return decode0(d,data, flags);
	}

先是获取字节数组及标志信息,根据标志位决定是否要解压缩字节数组。最后调用decode0(CachedData cachedData,byte[] data, int flags)方法,代码如下:

	protected final Object decode0(CachedData cachedData,byte[] data, int flags) {
		Object rv = null;
		if ((cachedData.getFlag() & SERIALIZED) != 0 && data != null) {
			rv = deserialize(data);
		} else {
			if (this.primitiveAsString) {
				if (flags == 0) {
					return decodeString(data);
				}
			}
			if (flags != 0 && data != null) {
				switch (flags) {
				case SPECIAL_BOOLEAN:
					rv = Boolean.valueOf(this.transcoderUtils
							.decodeBoolean(data));
					break;
				case SPECIAL_INT:
					rv = Integer.valueOf(this.transcoderUtils.decodeInt(data));
					break;
				case SPECIAL_LONG:
					rv = Long.valueOf(this.transcoderUtils.decodeLong(data));
					break;
				case SPECIAL_BYTE:
					rv = Byte.valueOf(this.transcoderUtils.decodeByte(data));
					break;
				case SPECIAL_FLOAT:
					rv = new Float(Float.intBitsToFloat(this.transcoderUtils
							.decodeInt(data)));
					break;
				case SPECIAL_DOUBLE:
					rv = new Double(Double
							.longBitsToDouble(this.transcoderUtils
									.decodeLong(data)));
					break;
				case SPECIAL_DATE:
					rv = new Date(this.transcoderUtils.decodeLong(data));
					break;
				case SPECIAL_BYTEARRAY:
					rv = data;
					break;
				default:
					log
							.warn(String.format("Undecodeable with flags %x",
									flags));
				}
			} else {
				rv = decodeString(data);
			}
		}
		return rv;
	}

上面方法实际上就是encode(Object o)方法的逆向实现,即将字节数组转化成Object对象。注意第4行调用了deserialize(byte[] in)方法,该方法代码如下(省略了catch、finally部分):

	protected Object deserialize(byte[] in) {
		Object rv = null;
		ByteArrayInputStream bis = null;
		ObjectInputStream is = null;
		try {
			if (in != null) {
				bis = new ByteArrayInputStream(in);
                is = new ObjectInputStream(bis) {
                    @Override
                    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
                        try {
                            //When class is not found,try to load it from context class loader.
                            return super.resolveClass(desc);
                        } catch (ClassNotFoundException e) {
                            return Thread.currentThread().getContextClassLoader().loadClass(desc.getName());
                        }
                    }
                };
                rv = is.readObject();

			}
		}
		...
		return rv;
	}

上述代码就是反序列化对象并返回。每次反序列化操作,得到的都是一个全新对象,对该新对象进行的任何操作并不会影响memcached中存储的值。

三、对比Memcached的set/get命令

除了上述通过java代码与memcached交互外,我们还可以直接通过命令方式与其交互。步骤如下:

1.先打开cmd窗口

2.通过telnet连上memcached服务器,命令如下:

telnet 127.0.0.1 11211

3.若连接成功,可直接输入命令与memcached服务器交互。

1.存储命令

格式如下

<command name> <key> <flags> <exptime> <bytes>
<data block>

参数说明如下:

<command name> set/add/replace

<key> 查找关键字

<flags> 客户机使用它存储关于键值对的额外信息

<exptime> 该数据的存活时间,0表示永远

<bytes> 存储字节数

<data block> 存储的数据块(可直接理解为key-value结构中的value)

各存储命令特别说明:

  • set命令在key不存在时,进行添加操作,否则进行更新操作。
  • add命令在key不存在时,才能添加成功
  • replace命令在key存在时,才能替换成功

实例演示:

set myname 0 0 9
super man
STORED

2.读取命令

get  key [key1]...

获取一个或多个键值,键之间以空格分隔。

实例演示:

get myname
VALUE myname 0 9
super man
END

除了上述常用命令外,还有gets、cas、stats等命令,大家有兴趣的可以去学习下。

3.类比

类比下Xmemcached客户端使用,不难发现很有趣的地方:

set命令中<key>和<exptime>分别对应XMemcacheClient.set(final String key, final int exp, final Object value)方法中的key和exp参数。而<flags>和<data block>则对应CachedData封装类的flag和data成员变量。

get命令<key>对应到XMemcacheClient.get(final String key)方法的key参数,get命令的返回结果对应SerializingTranscoder.decode(CachedData d)方法的参数d,d的类型是CachedData,该类正是封装了flag和data信息。

通过上面的对比,不难发现,无论是Memcached命令还是Xmemcached客户端,都不过是memcached客户端一种实现而已,他们遵守相同的请求及应答消息规范。更底层来看,这两种方式都是通过建立tcp连接后,然后发送符合memcached约定的请求消息;在接收到memcached服务器应答消息后,也是按照memcached的应答消息约定进行解码(在Xmemcached客户端利用了flag字段实现将数据字节数组转化成应用层需要的类型)。

换句话说:Memcached缓存系统提供了服务端的实现(c语言),并约定了客户端与服务器进行通信的消息格式,更准确来说是字节流格式(通过tcp方式通信)。不同语言客户端,仅仅是这一规范的实现而已。当然Memcached已经提供了大部分语言的客户端实现,不过你也可以自己开发出一个客户端实现。

四、如何优化Xmemcached客户端代码,提高效率

从前面的源码分析可以看出,如果存入Memcached的是bean对象,需要实现Serializable接口以支持java对象序列化。据我了解,java自带的对象序列化,不仅序列化和反序化操作耗时,而且生成的字节数组也比较大。因此可以考虑换一种编解码技术,本人推荐使用fastjson,其不仅效率过,而且生成的json串体积小。其它优化措施,如果有想到,再补充

时间: 2024-08-02 11:03:54

java之XMemcached使用及源码详解的相关文章

深入Java基础(四)--哈希表(1)HashMap应用及源码详解

继续深入Java基础系列.今天是研究下哈希表,毕竟我们很多应用层的查找存储框架都是哈希作为它的根数据结构进行封装的嘛. 本系列: (1)深入Java基础(一)--基本数据类型及其包装类 (2)深入Java基础(二)--字符串家族 (3)深入Java基础(三)–集合(1)集合父类以及父接口源码及理解 (4)深入Java基础(三)–集合(2)ArrayList和其继承树源码解析以及其注意事项 文章结构:(1)哈希概述及HashMap应用:(2)HashMap源码分析:(3)再次总结关键点 一.哈希概

Java concurrent AQS 源码详解

一.引言 AQS(同步阻塞队列)是concurrent包下锁机制实现的基础,相信大家在读完本篇博客后会对AQS框架有一个较为清晰的认识 这篇博客主要针对AbstractQueuedSynchronizer的源码进行分析,大致分为三个部分: 静态内部类Node的解析 重要常量以及字段的解析 重要方法的源码详解. 所有的分析仅基于个人的理解,若有不正之处,请谅解和批评指正,不胜感激!!! 二.Node解析 AQS在内部维护了一个同步阻塞队列,下面简称sync queue,该队列的元素即静态内部类No

Spring IOC源码详解之容器依赖注入

Spring IOC源码详解之容器依赖注入 上一篇博客中介绍了IOC容器的初始化,通过源码分析大致了解了IOC容器初始化的一些知识,先简单回顾下上篇的内容 载入bean定义文件的过程,这个过程是通过BeanDefinitionReader来完成的,其中通过 loadBeanDefinition()来对定义文件进行解析和根据Spring定义的bean规则进行处理 - 事实上和Spring定义的bean规则相关的处理是在BeanDefinitionParserDelegate中完成的,完成这个处理需

IntentService源码详解

IntentService可以做什么: 如果你有一个任务,分成n个子任务,需要它们按照顺序完成.如果需要放到一个服务中完成,那么IntentService就会使最好的选择. IntentService是什么: IntentService是一个Service(看起来像废话,但是我第一眼看到这个名字,首先注意的是Intent啊.),所以如果自定义一个IntentService的话,一定要在AndroidManifest.xml里面声明. 从上面的"可以做什么"我们大概可以猜测一下Inten

butterknife源码详解

butterknife源码详解 作为Android开发者,大家肯定都知道大名鼎鼎的butterknife.它大大的提高了开发效率,虽然在很早之前就开始使用它了,但是只知道是通过注解的方式实现的,却一直没有仔细的学习下大牛的代码.最近在学习运行时注解,决定今天来系统的分析下butterknife的实现原理. 如果你之前不了解Annotation,那强烈建议你先看注解使用. 废多看图: 从图中可以很直观的看出它的module结构,以及使用示例代码. 它的目录和我们在注解使用这篇文章中介绍的一样,大体

《GIS软件ShapMap源码详解及应用》概述

我喜欢GIS二次开发,即使有的人看不起:我不懂开源GIS,只会点商业的GIS,有的人更加瞧不起.我认为,我不能改变现实这个环境,但可以创造一些价值.找到一本<GIS软件ShapMap源码详解及应用>来学习,我倒要看看开源GIS是什么样子. 当前GIS软件有商业GIS系统及开源GIS系统之分.GIS商用软件功能强 大,有完善的技术支持,提供封装好的.功能强大的类库,基于商用GIS库进 行的二次开发效率高.难度低.资源丰富.但对于小型GIS开发人员,商用 GIS价格过高,对于GIS学习者来说,由于

Shiro 登录认证源码详解

Shiro 登录认证源码详解 Apache Shiro 是一个强大且灵活的 Java 开源安全框架,拥有登录认证.授权管理.企业级会话管理和加密等功能,相比 Spring Security 来说要更加的简单. 本文主要介绍 Shiro 的登录认证(Authentication)功能,主要从 Shiro 设计的角度去看这个登录认证的过程. 一.Shiro 总览 首先,我们思考整个认证过程的业务逻辑: 获取用户输入的用户名,密码: 从服务器数据源中获取相应的用户名和密码: 判断密码是否匹配,决定是否

Guava Cache源码详解

目录 一.引子 二.使用方法 2.1 CacheBuilder有3种失效重载模式 2.2 测试验证 三.源码剖析 3.1 简介 3.2 源码剖析 四.总结 优点: 缺点: 正文 回到顶部 一.引子 缓存有很多种解决方案,常见的是: 1.存储在内存中 : 内存缓存顾名思义直接存储在JVM内存中,JVM宕机那么内存丢失,读写速度快,但受内存大小的限制,且有丢失数据风险. 2.存储在磁盘中: 即从内存落地并序列化写入磁盘的缓存,持久化在磁盘,读写需要IO效率低,但是安全. 3.内存+磁盘组合方式:这种

Android编程之Fragment动画加载方法源码详解

上次谈到了Fragment动画加载的异常问题,今天再聊聊它的动画加载loadAnimation的实现源代码: Animation loadAnimation(Fragment fragment, int transit, boolean enter, int transitionStyle) { 接下来具体看一下里面的源码部分,我将一部分一部分的讲解,首先是: Animation animObj = fragment.onCreateAnimation(transit, enter, fragm