Python写入Memcached但Java读不到
—flags是何方神圣

本文主要介绍自己第一次使用Memcached时的奇葩遭遇,由于自己是使用Memcached的小白,第一次遇到这种问题,感觉很是难想到最根本的原因,在这里记录一下,希望能帮到用Python和Java同时读写Memcached时遇到同样问题的同学。对于这个问题其实弄懂了很简单,网上也有很多对于这个问题的解释,总感觉都写的不太透彻,这里会从问题根源以及最佳实践给大家做个简单介绍,从而让大家能选择更适合自己的解决方案。

Python写入Memcached但Java读不到
今个是新中国成立70年的国庆日,本文作者满怀欣喜地全程观看了国庆阅兵仪式。在为新中国取得了这么多举世瞩目的成就的同时,程序猿直男总是要犯病,该总结的问题还是要及时总结的。下面将给大家讲讲我前不久遇到的一个十分诡异的事情,希望能遇到同样问题的童鞋有所帮助。

问题背景

公司领导想做一个日志解析展示的程序,至于技术选型就不细说了,日志分析使用Python,数据临时存储使用Memcached,数据展示使用Java作为后端提供API给前端展示。

问题现象

为了大家也能更好的理解Python写Memcached但Java读不到的这个问题的情景,这里先给大家复现一下。下面将从Memcached安装、Python客户端&代码、Java客户端&代码、以及最后复现出的问题现象进行描述。

Memcached环境

在Linux环境下安装Memcached的步骤如下

# 1.安装libevent库
yum install libevent libevent-devel
# 2.下载最新版本
wget http://memcached.org/latest
# 3.解压源码
tar -zxvf memcached-1.x.x.tar.gz
# 4.进入目录
cd memcached-1.x.x
# 5.配置
./configure --prefix=/usr/local/memcached
# 6.编译
make
# 7.安装
sudo make install
# 8.启动
/usr/local/memcached/bin/memcached -d -u root
# 9.测试连接
telnet 192.168.37.131 11211
# 命令行响应如下内容则说明连接正常
# Trying 192.168.37.131...
# Connected to 192.168.37.131.
# Escape character is '^]'.

Python环境

Python客户端

出现此问题时使用的Python客户端为python-memcached,下面为使用pip安装的命令

pip install python-memcached==1.53

Python写入Memcached的代码

# write-into-memcache-opration.py
import memcache

mc = memcache.Client(['192.168.37.131:11211'],debug=1)
mc.set("site","blog.chuangzhi8.com") # Python向Memcached写入key为site的值
site = mc.get("site") # 从Memcached读取已经写入的内容
print(site) # 打印读取内容

Java环境

Java客户端

出现此问题时用的Java客户端为Memcached-Java-Client,以下为使用maven安装的依赖版本

<dependency>
<groupId>com.whalin</groupId>
<artifactId>Memcached-Java-Client</artifactId>
<version>3.0.2</version>
</dependency>

Java读取Memcached的代码

// MemcachedJavaClientTest.java
package com.haozi.demo;

import com.whalin.MemCached.MemCachedClient;
import com.whalin.MemCached.SockIOPool;

public class MemcachedJavaClientTest {
/**
* 此方法主要演示使用Memcached-Java-Client Java客户端从Memcached读取数据
*
*/
public static void main(String[] args) {
//initialize the SockIOPool that maintains the Memcached Server Connection Pool
String[] servers = { "192.168.37.131:11211" };
SockIOPool pool = SockIOPool.getInstance("Test1");
pool.setServers(servers);
pool.setFailover(true);
pool.setInitConn( 10 );
pool.setMinConn( 5 );
pool.setMaxConn( 250 );
pool.setMaintSleep( 30 );
pool.setNagle( false );
pool.setSocketTO( 3000 );
pool.setAliveCheck( true );
pool.initialize();
//Get the Memcached Client from SockIOPool named Test1
MemCachedClient mcc = new MemCachedClient("Test1");
System.out.println(mcc.get("site"));// 从Memcached读取Python写入的key为site的值

}
}

复现问题

执行Python代码向Memcached写入数据

python write-into-memcache-opration.py

执行上面命令后,结果如下图:
Python写入Memcached

命令行验证数据是否写入

命令行读取Memcached
从上图可看出数据已经写入。

执行Java代码从Memcached读取Python写入的数据

# 编译
javac MemcachedJavaClientTest.java
# 执行
java MemcachedJavaClientTest

执行上面命令后,结果如下图:
Java读取Memcached
从上图看出Java并没有读取到Memcached中key为site的数据,而是返回null。
从上面python读取Memcached到数据和Memcached命令行中读取到的数据均可证明:key为site的数据确实已经写入到Memcached里了,但为啥Java客户端读取不到呢?难道见鬼了?这到底是咋回事?

问题现象总结

使用Python的python-memcached客户端向Memcached写入一个value为字符串的值,但使用Java的Memcached-Java-Client读取Python刚写入的字符串值却反回为null。

问题排查

对于这个问题可能的原因有:

  • key过期了,java读不到(经测试,key未过期)
  • Java代码读取有问题,写入应该有问题(经测试,Java可正常写入,同时在Java写入之后Java可以读取到对应key的内容)
  • Python客户端、Java客户端、Memcached的版本不匹配(经更换版本,此问题排除)
  • Memcached有特殊的读写规范(下文详细分析)
  • 等等其他问题(暂时没想到)
    经过这个步骤的排查,这一问题感觉更诡异了,竟然Java代码没问题并且Java可以读到Java写入的数据,难道Python和Java读写Memcached相互不兼容,这也太可怕了吧!还没见过这样因为语言而相互不兼容的数据库呢。

问题原因

显然我现在已经黔驴技穷了,但问题还得解决,继续硬着头皮继续百度&Google吧。经过一番折腾所有搜索的线索均指向了flags这个东西,这是个啥呢,那就先来深入了解下这个flags再说。

Memcached中的flags含义

flags这个参数其实是让客户端给自己的字符串数据打一个label标记罢了,就这么简单。由于memcached存放的数据类型只有string一种,那么memcached不知道你存的数据到底是序列化的字符串还是普通字符串,那么你设置一个flags,等你使用get拿到string的时候,也把这个flags给你,让你自己判断进行处理。目的就是为了方便各个语言版本的客户端例如 Java Python PHP等等,可以通过存储序列化后的字符串,达到存放数据的目的。 这样的话客户端可以根据自己给字符串设置的flags,判断例如是否要反序列化等等操作。 对于调用memcached的客户端库是透明的,用户无感的,感觉就是使用set/add把一个变量存进去, 后面使用get获取这个变量就行了。其实客户端库底层就是通过这个flags帮你实现的,不过各个语言的实现可能不同,但是原理是一样
例如php的memcache扩展,底层就帮我们把这个实现细节屏蔽了。通过add/set存对象(object),数组(array)都会自动序列化, 取数据会反序列化。 但是对于string int double bool只是就直接存了。 所以其实在memcache扩展中,我们一般情况下直接把flags设置false即可。实在是用到flags的话,只能填一些参数 如 ture false null 以及压缩常量MEMCACHE_COMPRESSED表示压缩数据,其他值可能会报warning错误或者致命错误。
从以上对flags含义的描述,大致可猜测到Python想Memcached写入字符串但Java读取不到可能是两种语言使用的客户端实现读写字符串时使用的flags不同,导致一种语言写入另一种语言读取解析不出来的。为了进一步验证确实是flags的原因,下文将从不同语言客户端实际写入和源码两方面验证这一问题。

不同语言客户端写入验证

  • Python客户端写入时的flags
    Python客户端写入Memcached时的flags
  • Java客户端写入时的的flags
    Java客户端写入Memcached时的flags

从上面两个图中可以看出使用Python和Java客户端写入Memcached时使用的flags确实不同,Python使用的flags是0,Java使用的flags是32,进而可以猜测得知不同语言的客户端在反序列化时具体要转换成哪种类型的数据是要通过写入时设置的flags来判断的。接下来为了验证这一结论,下文将从不同的语言的客户端的源码实现中来验证。

不同语言客户端源码验证

接下来将贴一些客户端的实现源码来分析从Memcached中读取的数据是怎样通过不同的flags值来反序列化出不同类型的数据的。

Python客户端写入源码

下面将列出Python客户端python-memcached写入Memcached是源代码,来验证写入

# 在set()方法中主要调用了 _val_to_store_info(self, val, min_compress_len)方法将即将存储到Memcached中的数据进行转换并生成对应的flags值
# @see /usr/lib/python2.7/site-packages/memcache.py
def _val_to_store_info(self, val, min_compress_len):
"""
Transform val to a storable representation, returning a tuple of the flags, the length of the new value, and the new value itself.
"""
flags = 0 # 默认为0即对应字符串的flags值
if isinstance(val, str):
pass
elif isinstance(val, int): ##
flags |= Client._FLAG_INTEGER
val = "%d" % val
# force no attempt to compress this silly string.
min_compress_len = 0
elif isinstance(val, long):
flags |= Client._FLAG_LONG
val = "%d" % val
# force no attempt to compress this silly string.
min_compress_len = 0
else:
flags |= Client._FLAG_PICKLE
file = StringIO()
if self.picklerIsKeyword:
pickler = self.pickler(file, protocol = self.pickleProtocol)
else:
pickler = self.pickler(file, self.pickleProtocol)
if self.persistent_id:
pickler.persistent_id = self.persistent_id
pickler.dump(val)
val = file.getvalue()

lv = len(val)
# We should try to compress if min_compress_len > 0 and we could
# import zlib and this string is longer than our min threshold.
if min_compress_len and _supports_compress and lv > min_compress_len:
comp_val = compress(val)
# Only retain the result if the compression result is smaller
# than the original.
if len(comp_val) < lv:
flags |= Client._FLAG_COMPRESSED
val = comp_val

# silently do not store if value length exceeds maximum

从上面python源码可以看出,对于字符串类型的数据在写入Memcached时,客户端python-memcached使用的flags是0,这和Memcached命令行get数据得到的flags是一致的。

Java客户端读取源码

接下来,咱们看一下如何通过flags来反序列化数据的,下面代码为Memcached Client for Java客户端读取序列化后对象的过程

// @see com.schooner.MemCached.AscIIClient.java
private Object get(String cmd, String key, Integer hashCode, boolean asString) {
// ......
// get SockIO obj using cache key
// ......

// get result code
// ......
// Then analysis the return metadata from server,including key,
// ......
/*
* Critical block to parse the response header.
*/
// ......
Object o = null;
// we can only take out serialized objects(开始反序列化读取到的数据)
if (dataSize > 0) {
if (NativeHandler.isHandled(flag)) {
byte[] buf = input.getBuffer();
if ((flag & F_COMPRESSED) == F_COMPRESSED) {
GZIPInputStream gzi = new GZIPInputStream(new ByteArrayInputStream(buf));
ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.length);
int count;
byte[] tmp = new byte[2048];
while ((count = gzi.read(tmp)) != -1) {
bos.write(tmp, 0, count);
}
// store uncompressed back to buffer
buf = bos.toByteArray();
gzi.close();
}
if (primitiveAsString || asString) {// 明确指定按字符串处理,则直接反序列化为字符串
o = new String(buf, defaultEncoding);
} else
// decoding object
o = NativeHandler.decode(buf, flag);//根据flag解析读取到的数据(即反序列化)
} else if (transCoder != null) {
InputStream in = input;
if ((flag & F_COMPRESSED) == F_COMPRESSED)
in = new GZIPInputStream(in);
// decode object with default transcoder.
o = transCoder.decode(in);
}
}
return o;

}

从上面反序列化的流程可以看到主要是通过NativeHandler.decode(buf, flag)进行反序列化的,接下来在看下这个函数的代码

// @see com.schooner.MemCached.NativeHandler.java
/**
* Decodes byte array using memcache flag to determine type.
* 使用Memcached flag对字节数组解码以确定数据类型
* @param b
* @param marker
* @return
* @throws UnsupportedEncodingException
*/
public static Object decode(byte[] b, int flag) throws UnsupportedEncodingException {

if (b.length < 1)
return null;

if ((flag & MemCachedClient.MARKER_BYTE) == MemCachedClient.MARKER_BYTE) //flag=1时表示字节类型
return decodeByte(b);

if ((flag & MemCachedClient.MARKER_BOOLEAN) == MemCachedClient.MARKER_BOOLEAN) //flag=8192时表示布尔类型
return decodeBoolean(b);

if ((flag & MemCachedClient.MARKER_INTEGER) == MemCachedClient.MARKER_INTEGER) //flag=4时表示整类型
return decodeInteger(b);

if ((flag & MemCachedClient.MARKER_LONG) == MemCachedClient.MARKER_LONG) //flag=16384时表示Long类型
return decodeLong(b);

if ((flag & MemCachedClient.MARKER_CHARACTER) == MemCachedClient.MARKER_CHARACTER) //flag=16时表示字符类型
return decodeCharacter(b);

if ((flag & MemCachedClient.MARKER_STRING) == MemCachedClient.MARKER_STRING) //flag=32时表示字符串类型
return decodeString(b);

if ((flag & MemCachedClient.MARKER_STRINGBUFFER) == MemCachedClient.MARKER_STRINGBUFFER) //flag=64时表示StringBuffer类型
return decodeStringBuffer(b);

if ((flag & MemCachedClient.MARKER_FLOAT) == MemCachedClient.MARKER_FLOAT) //flag=128时表示float类型
return decodeFloat(b);

if ((flag & MemCachedClient.MARKER_SHORT) == MemCachedClient.MARKER_SHORT) //flag=256时表示short类型
return decodeShort(b);

if ((flag & MemCachedClient.MARKER_DOUBLE) == MemCachedClient.MARKER_DOUBLE) //flag=512时表示double类型
return decodeDouble(b);

if ((flag & MemCachedClient.MARKER_DATE) == MemCachedClient.MARKER_DATE) //flag=1024时表示日期类型
return decodeDate(b);

if ((flag & MemCachedClient.MARKER_STRINGBUILDER) == MemCachedClient.MARKER_STRINGBUILDER) //flag=2048时表示StringBuilder类型
return decodeStringBuilder(b);

if ((flag & MemCachedClient.MARKER_BYTEARR) == MemCachedClient.MARKER_BYTEARR) //flag=4096时表示字节数组类型
return decodeByteArr(b);

return null;
}

从上面的代码可以看出当flags为32时,Java版Memcached Client for Java客户端会将从Memcached读取的数据反序列化为字符串。这一结论也和Memcached 命令行中得到Java版客户端将字符串写入Memcached时生成的flags是一样的。

修改Python写入Memcached时使用的flags验证Java是否可读

为了进一步验证确实是由于python客户端和java客户端在读写字符串时使用的flags不同导致的这个问题,现在对Python客户端python-memcached进行临时修改来再次实验使用Python写入Java是否能读取到数据?修改的代码如下

# memcache.py是修改后的Python源码,memcache.py.bak是原始文件。(为了保证上面复现问题用到的Python代码不修改仍能使用,此处将python读取时用的flags一并改成32)
[root@centos7-1 ~]# diff /usr/lib/python2.7/site-packages/memcache.py /usr/lib/python2.7/site-packages/memcache.py.bak
760c760
< flags = 32
---
> flags = 0
1014c1014
< if flags == 32 or flags == Client._FLAG_COMPRESSED:
---
> if flags == 0 or flags == Client._FLAG_COMPRESSED:

接下来执行python write-into-memcache-opration.py,正常写入,接下来执行java MemcachedJavaClientTest,读取也正常了。这下所有的疑惑都烟消云散,就是由于不同的语言的客户端在实现序列化和反序列化时使用flags代表的数据类型不一样或者使用的flags的值不同导致

原因总结(重点)

从上面使用Python和Java分别写入Memcached再通过Memcached命令行get数据验证以及查看Python客户端写入和Java客户端读取的源码可以知道,Python客户端python-memcached读写Memcached中的字符串类型数据时flags使用的是0;Java客户端Memcached Client for Java读写Memcached中的字符串类型数据时flags使用的是32,所以造成了Python写入Memcached后Java却读取不到的问题,更确切地说,Java实际已经读取到了Python写入Memcached的数据但根据flags为0解析读到的数据时,因为没有flags=0的情况而导致解析数据报错,从而最终得到null。

解决方案

在考虑可行性方案时首先我们要了解下市面上已有客户端对flags的实现情况(仅列出了读写字符串类型时的flags,至于其他类型的,哈哈,别犯懒,自己补充吧),然后再根据实际情况决定具体的合适的可行方案。

不同语言的客户端

Python的客户端

名称特点flags含义
python-memcachedpython编写、
对序列化和反序列化有灵活的方法、
不实现”noreply”、
比pylibmc和pymemcache慢
flags=0表示字符串
umemcacheC++语言实现(安装报错)
pylibmcC语言实现、快速、
实现一致的哈希、
不能访问“noreply”标志、
非常依赖libmemcached
flags=0表示字符串
pymemcache支持”noreply”访问(提到写入速度)、
模块化和简单的序列化和反序列化的方法
flags=0表示字符串

Java的客户端

名称特点flag含义
Memcached Client for Java版本较早、
应用广泛、
运行较稳定、
使用阻塞IO、
不支持CAS操作
flags=32表示字符串
SpyMemcached使用了concurrent和nio 读取速度更快、
支持异步、
支持CAS操作、
稳定性不好
flags=0表示字符串
XMemcached使用NIO 效率高、
在数据小时比SpyMemcached更优秀
flags=0表示字符串

从上面两个表格罗列的Python、Java客户端使用flags定义来看,对于字符串类型的数据来说,只有Java客户端Memcached Client for Java在存储字符串类型的数据时flags使用的32,哇哦,咋这么悲催,这么巧的诡异事情咋让本尊遇到了,如果上来就使用的另外两种Java客户端也就没这么大费周折了,当然祸兮福所倚,如果没有这么折腾也就没有对Memcached对这么深刻的理解了也就没有这篇文章的分享了(来碗鸡汤:平时开发中,大家不要苦恼那些遇到的坑,没有这些坑的历练可能我们也很难有坚实的进步)。
说到这里,其实对于本文作者已经有了合适的解决方案,因为作者开发的项目主要在Memcached中存储字符串类型的数据,所以最合适最明智的做法就是更换Java客户端来适应Python客户端,这样对开发的项目也更有普适性。但为了脑洞大开下,并且本文中并没有讨论除了字符串类型之外的数据类型,so下面简单介绍下其他可行的方案,希望能给大家抛砖引玉。

可行方案分析

这一小节将给大家简单介绍下,对于不同客户端使用不同flags值表示不同类型数据,这里给大家罗列一些可行的解决方案,希望对有同样问题的童鞋有帮助。

修改客户端源码

对于修改客户端源码,上面【修改Python写入Memcached时使用的flags验证Java是否可读】小节中已经使用过了,由于修改Python源码比较简单,所以大家也可以尽量修改Python源码,当然也可以修改Java源码进行编译,来让用到的客户端能相互识别所存储的数据类型。
对于修改源码这种方案,有个尴尬的问题时,我们修改源码后需要上传自己的Python/Java包到远程库,这显然是不方便也不利于部署,除非自己搭建了专用的包管理库。

更换客户端(使用flags定义值含义一致的客户端)

这种方案需要针对项目中使用到数据类型来寻找对flags实现一致的客户端,这种方案应该是最方便也最可靠的可行方案。为了安全起见,这需要对使用到的所有数据类型进行读写测试,来保证方案的全面适用。

重写decode()解码流程

这个方案更适用于对Java更熟悉的童鞋,大致看下源码不难看出通过flags解码的流程主要在SerializingTranscoder.java类中decode()方法中,具体的代码这里就不给贴了,可以重写下这个方法来实现同Python客户端一致的flags实现。(如果对重写解码流程有兴趣,可以参考下文章 flag – 诡异的memcache标记

总结

对于Python写入Memcached但Java读不到这个问题的分析就到这里了,总结一句,这个问题就是由于不同客户端使用不同的flags值进行序列化&反序列化导致的。弦外音,对于平时感觉很诡异的问题,主要的原因还是对使用的一些工具不求甚解的原因,网上的讲解可能也不太透彻,当然也包括本文,希望遇到类似问题的童鞋还是要多静下心来寻找问题的蛛丝马迹,主要多从基础问题基础概念上入手逐步深入,这样才能拨开云雾。

浩子淘天下 wechat
扫码关注我
鼓励原创