com.danga.MemCached 中key的中文问题

   在项目中用到了memcached 做缓存,在实际应用中发现spymemcached 客户端在网络状态比较差是(大概延迟60ms左右)时的读取超时现象比较严重,而且造成应用的内存被消耗尽了(用jmap 查看过,发现memcache 的客户端用到的异步线程类占用很大的内存,估计是由于超时不断创建的缘故)。于是便换了http://www.whalin.com/memcached 这个的客户端。。在更换过程中也出现了一些问题,由于是晚上才发现,害得我晚上因为这事睡都睡不着。。。想好一些调试的办法,然后在明早进行测试。。

 

问题一:当key 为中文的时候,value取错了。

   遇到这个问题时,由于对业务的理解,排除了是键重复的问题。由于是更换客户端后才出现的问题,也首选反应到是客户端的问题,但为了确定下,想在服务器端进行验证,然而在SecureCRT终端中输入不了中文,没办法在memcache服务器端进行测试。。在上线过程中也在测试机上测试,发现测试机上不会出现这个问题。。随后也反应到,中文经常是会涉及编码问题。。然后便查看了两边的操作系统默认编码,发现确实不一样。。线上服务器是默认的LANG=C,测试机是LANG=en_US.UTF-8。然后在测试机上也调成LANG=C,发现问题重现,大喜。。之后更加确定这个问题出现的原因。。然后便是对whalin memcache客户端源码进行研究了。

 

	private Object get(String cmd, String key, Integer hashCode, boolean asString) {

		if (key == null) {
			log.error("key is null for get()");
			return null;
		}

		try {
//注意这里,可以对key进行URLEncode
			key = sanitizeKey(key);
		} catch (UnsupportedEncodingException e) {
			log.error("failed to sanitize your key!", e);
			return null;
		}

		// get SockIO obj using cache key
		SchoonerSockIO sock = pool.getSock(key, hashCode);

		if (sock == null) {
			if (errorHandler != null)
				errorHandler.handleErrorOnGet(this, new IOException("no socket to server available"), key);
			return null;
		}

		String cmdLine = cmd + " " + key;

		try {
			sock.writeBuf.clear();
//cmdLine.getBytes()  这个是出错的关键
			sock.writeBuf.put(cmdLine.getBytes());
			sock.writeBuf.put(B_RETURN);
			// write buffer to server
			sock.flush();
//......
}

	//可以看出这里对key做了URLEncode ,当然这里要进行设定才会
	private String sanitizeKey(String key) throws UnsupportedEncodingException {
		return (sanitizeKeys) ? URLEncoder.encode(key, "UTF-8") : key;
	}

   由于我把key的编码给关了mcc1.setSanitizeKeys(false); 所以对中文不会进行URLEncode编码。。然后查看了Java API 发现了 cmdLine.getBytes()方法的描述是:

使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 
当此字符串不能使用默认的字符集编码时,此方法的行为没有指定。如果需要对编码过程进行更多控制,则应该使用 CharsetEncoder 类。

 关键字在于平台默认编码。。假如当用户输入中文时,是utf8编码,然后在getBytes方法的时候,不是用utf8解码,那就会出现问题了。我也在memcache 客户端中加入了一些调试代码后,再进行测试,发现中文打印的是“??”,每个中文解码由于解码不对称问题都统一转成一样的二进制编码。。。这就是原因所在了。。

 

解决办法:mcc1.setSanitizeKeys(true).但这种解决办法的缺点是由于对key做了URLEncode编码,在memecache 服务器中测试就比较困难了,因为我们也要首先把key转成URLEncode编码,然后在测试。

 

 

问题二:key中间出现空字符串,客户端一直未结束

   这个问题是在问题一的测试中突然发现的。。经代码调试发现阻塞在下面的方法中。。

 

	/**
	 * Constructor.
	 * 
	 * @param sock
	 *            {@link SchoonerSockIO}, read from this socket.
	 * @param limit
	 *            limited length to read from specified socket.
	 * @throws IOException
	 *             error happened in reading.
	 */
	public SockInputStream(final SchoonerSockIO sock, int limit) throws IOException {
		this.sock = sock;
		willRead(limit);
		sock.readBuf.clear();
//阻塞在这里。。这个通道处于阻塞模式
		sock.getChannel().read(sock.readBuf);
		sock.readBuf.flip();
	}

 

SocketChannel.read(ByteBuffer des)的JavaDoc的描述是

public abstract int read(ByteBuffer dst)
                  throws IOException
将字节序列从此通道中读入给定的缓冲区。 
尝试最多从该通道中读取 r 个字节,其中 r 是调用此方法时缓冲区中剩余的字节数,即 dst.remaining()。 

假定读取的字节序列长度为 n,其中 0 <= n <= r。此字节序列将被传输到缓冲区中,序列中的第一个字节位于索引 p 处,最后一个字节则位于索引 p + n - 1 处,其中 p 是调用此方法时缓冲区的位置。返回时,该缓冲区的位置将等于 p + n;其限制不会更改。 

读取操作可能不填充缓冲区,实际上它可能根本不读取任何字节。是否如此执行取决于通道的性质和状态。例如,处于非阻塞模式的套接字通道只能从该套接字的输入缓冲区中读取立即可用的字节;类似地,文件通道只能读取文件中剩余的字节。但是可以保证,如果某个通道处于阻塞模式,并且缓冲区中至少剩余一个字节,则在读取至少一个字节之前将阻塞此方法。 

可在任意时间调用此方法。但是如果另一个线程已经在此通道上发起了一个读取操作,则在该操作完成前此方法的调用被阻塞。 


 

然后我在初始化memcache 这个客户端的时候已经设定了超时时间为3秒pool.setSocketTO(3000),但为什么到了3秒后依然没有报超时错误,这个我也纳闷,暂时也想不到原因。。不知道谁知道不。。

 

在调试中也发现了出现这个的读取一直阻塞的原因是memcache 命令的组装未检查key中带有特殊字符,下面是源码中key的组装

		// build command
		StringBuilder command = new StringBuilder("sync ").append(key);
		command.append("\r\n");

 memcache 服务器端的协议也表明了,key 中不能有制表符和空白字符,并且长度不能超高250个字符。不然服务器端不会响应任何数据。。这个我也确实试了下,发现真没反应。。对memcache 服务器端表示不解。。

 

解决办法:和问题一的一样mcc1.setSanitizeKeys(true),对key进行URLEncode编码。。

 

问题三:批量获取接口取值和单个接口取值不一致

   这个问题还是上去后不就发现的。。批量接口(getMutil())的应用比较少,所以才迟几天发现。。初始以为是我的程序问题,担心了一场,后来慢慢调试发现一些规律,就是key的Encoder的问题的,在getMutil()里竟然不会去判断key是否要编码,都按照未编码的key进行获取,当然获取不了了,晕死。。源码就不发了,想看的自己可以去下载看下,另外批量获取接口是用NIO来实现的和单个获取的方法不一样,暂时也没去深究了。。。没想到的是用了这么久的客户端,竟然有这样的bug存在。。不过在官网上还是挺给力的,最近才更新了2.5.3版本解决了这个问题。。

   解决办法:更换2.5.3版本包。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章