欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

全网最详细解释memcached中的flags flag 含义 有大用

前言

复习memcached中,发现很多人对set/add 语法中设置的 flags参数理解不是很透彻, 大家人云亦云。 查资料从来不对别人的材料加以自己的理解写出来,那其实有毛用。百度出来的文章如出一辙,完全是同一篇文章。总是说flags参数来表示是否压缩数据。。。。我怀疑很多人也真实人云亦云,自己没有思考过。

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错误或者致命错误。

原文内容

github原文: https://github.com/squarezhou/ruhoh-blog/blob/master/posts/php-memcache-extension-flags.md

###问题描述###

今天帮同事分析一段诡异的代码,代码本身很乱,加上关闭了错误输出,所以开始一直没找到问题。最后才定位到是Memcache扩展set函数flags参数设置问题,再后来结合Memcache扩展源码找出问题根本原因,并开启error_reporting验证了问题原因。
问题代码大概是这样的:

<?php
error_reporting(7);
$mc = new Memcache();
$mc->connect('127.0.0.1',11211);
$mc->set('key', 'value', true, 60);
sleep(1);
var_dump($mc->get('key'));

最终输出为bool(false),不是预期的string(5) "value"。这段代码其实有两个坑,一小一大。
小坑是同事告诉我error_reporting(7)与error_reporting(E_ALL)作用是相同的,由于我一直使用后者,也忘记了E_ALL常量的值,加上不清楚有没其他特别写法。这个坑我很快掉进去了,于是就以为所有的错误信息将会被输出。
大坑就是Memcache扩展set函数第3个参数的值,下面重点说这个。不看手册乱用害死人吧~

###Memcached的flags###

Memcached在存储数据时除了可以指定过期时间外,还支持flags参数。SET <key> <flags> <expiration time> <bytes>,第2个参数就是flags。
在Memcached 1.2.1之前为flags预留了16位,到了1.2.1以后预留了32位(4 bytes)。对于服务器端而言,并不清楚你设置这个标记的作用,只是在你取回数据的时候同时传回给客户端。因此客户端就可以利用这个值来标记数据是否是经过编码的、是压缩过的等,PHP Memcache扩展就是这么做的。

###Memcache扩展的flags###

Memcache扩展中定义了两个宏MMC_SERIALIZED和MMC_COMPRESSED,分别表示序列化和zlib压缩,但只有MMC_COMPRESSED对应值被同时定义为PHP常量。

宏定义

#define MMC_SERIALIZED 1
#define MMC_COMPRESSED 2

常量定义

REGISTER_LONG_CONSTANT("MEMCACHE_COMPRESSED", MMC_COMPRESSED, CONST_CS | CONST_PERSISTENT);

看到这里,其实我们隐约可以感觉到Memcache扩展是不支持在PHP中启用数据序列化的,事实正是如此。
当flags参数为MMC_COMPRESSED(2)时,扩展会对数据进行zlib压缩后再发送给服务端;在取回数据后再根据flags参数,对数据解压并返回给PHP。判断使用位与(&)。

存储压缩

if (flags & MMC_COMPRESSED) {
        unsigned long data_len;

        if (!mmc_compress(&data, &data_len, value, value_len TSRMLS_CC)) {
                /* mmc_server_seterror(mmc, "Failed to compress data", 0); */
                return -1;
        }

        /* was enough space saved to motivate uncompress processing on get */
        if (data_len < value_len * (1 - pool->min_compress_savings)) {
                value = data;
                value_len = data_len;
        }
        else {
                flags &= ~MMC_COMPRESSED;
                efree(data);
                data = NULL;
        }
}

取回解压

if ((*flags & MMC_COMPRESSED) && data_len > 0) {
        char *result_data;
        unsigned long result_len = 0;

        if (!mmc_uncompress(&result_data, &result_len, data, data_len)) {
                mmc_server_seterror(mmc, "Failed to uncompress data", 0);
                if (key) {
                        efree(*key);
                }
                efree(data);
                php_error_docref(NULL TSRMLS_CC, E_NOTICE, "unable to uncompress data");
                return 0;
        }

        efree(data);
        data = result_data;
        data_len = result_len;
}

但数据序列化却不一样,是否序列化不受flags参数控制。只有值类型不是string、long、double、bool类型,即array、object等类型时,才会对值序列化,而无视flags参数,当然这里序列化后会更新flags值确保取回时反序列化。但在取回数据时,扩展会严格根据flags值判断是否对数据反序列化。

存储序列化

default: {
        zval value_copy, *value_copy_ptr;

        /* FIXME: we should be using 'Z' instead of this, but unfortunately it's PHP5-only */
        value_copy = *value;
        zval_copy_ctor(&value_copy);
        value_copy_ptr = &value_copy;

        PHP_VAR_SERIALIZE_INIT(value_hash);
        php_var_serialize(&buf, &value_copy_ptr, &value_hash TSRMLS_CC);
        PHP_VAR_SERIALIZE_DESTROY(value_hash);

        if (!buf.c) {
                /* something went really wrong */
                zval_dtor(&value_copy);
                php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to serialize value");
                RETURN_FALSE;
        }

        flags |= MMC_SERIALIZED;
        zval_dtor(&value_copy);

        result = mmc_pool_store(
                pool, command, command_len, key_tmp, key_tmp_len, flags, expire,
                buf.c, buf.len TSRMLS_CC);
}

取回反序列化

/* not found */
if (result == 0) {
        ZVAL_FALSE(*return_value);
}
/* read "END" */
else if ((response_len = mmc_readline(mmc TSRMLS_CC)) < 0 || !mmc_str_left(mmc->inbuf, "END", response_len, sizeof("END")-1)) {
        mmc_server_seterror(mmc, "Malformed END line", 0);
        result = -1;
}
else if (flags & MMC_SERIALIZED ) {
        result = mmc_postprocess_value(return_value, value, value_len TSRMLS_CC);
}
else {
        ZVAL_STRINGL(*return_value, value, value_len, 0);
}

OK,问题来了:如果值类型为string、long、double、bool其中之一,并且在set时设置了flags为1、true、所有可以转换为1或&1>0的值时,扩展在存储时不会序列化,但在取回时尝试反序列化,这时会抛出一个"unable to unserialize data"的Notice。如果代码关闭了Notice输出或错误输出,那就像上面的代码一样,除了输出bool(false)什么也看不到。

###解决方案###

  • 使用Memcached扩展(无flags参数);

  • 修改扩展源码,在判断序列化前使用flags &= ~MMC_SERIALIZED强制取消序列化标识;

  • 封装或继承set函数,对上面4种类型判断自动修改flags值。


来自 https://blog.csdn.net/xyz_dream/article/details/90453222


【memcached】memcached中flags flag 字段的作用

我们一般只注意到memcached的数据结构是key,value,今天看memcached源代码的时候,盯上了flags,没看明白。后来问了一下同事,说PHP当中使用flags标记,标识memcached数据是否需要经过压缩处理。

例如:

bool Memcache::add ( string $key, mixed $var [, int $flag [, int $expire]] )

    flag
  • Use MEMCACHE_COMPRESSED to store the item compressed (uses zlib).

看了一下memcached的协议,是这样定义一个item的:

Each item sent by the server looks like this:

VALUE <key> <flags> <bytes> [<cas unique>]\r\n
<data block>\r\n

- <key> is the key for the item being sent

- <flags> is the flags value set by the storage command

- <bytes> is the length of the data block to follow, *not* including
its delimiting \r\n

- <cas unique> is a unique 64-bit integer that uniquely identifies
this specific item.

- <data block> is the data for this item.
在memcached1.2.1之前为flags预留了16位,到了1.2.1以后预留了32位。对于服务器端而言,并不清楚你设置这些标记的作用。它并不知道你的数据是压缩过的,还是序列化存储的。

其实这只是客户端自己的一种定义,我们看Pecl的memcached模块的源代码:

#define MMC_SERIALIZED 1
#define MMC_COMPRESSED 2

if (flags & MMC_COMPRESSED) {
        unsigned long data_len;

        if (!mmc_compress(&data, &data_len, value, value_len TSRMLS_CC)) {
            /* mmc_server_seterror(mmc, "Failed to compress data", 0); */
            return -1;
        }

        /* was enough space saved to motivate uncompress processing on get */
        if (data_len < value_len * (1 - pool->min_compress_savings)) {
            value = data;
            value_len = data_len;
        }
        else {
            flags &= ~MMC_COMPRESSED;
            efree(data);
            data = NULL;
        }
    }

当flags包含MMC_COMPRESSED就对数据进行压缩处理。

也就是说,如果我们自己写一个memcached的client,也可以定义出更多的格式,json,xml或者别的。

其它关于 flags的文章请看

http://www.cnblogs.com/sunli/archive/2009/03/18/1415168.htm


来自 https://blog.csdn.net/yanhui_wei/article/details/8138874


flag -- 诡异的memcache标记

引子   

     打从去年一路北漂,进入无人货架行业,业务需求漫天飘,最近总算把工作都规划齐整。回望过去一年多的时间里,诸多东西值得整理,memcache就是其中一个。

   看到java的工资高些,队伍中好些人都想学习java,美其名曰:技术多元化。奈何团队中并没有相关经验的人,也深知大家殷切的期盼,所以,只能先撸起袖子自己干,看看书、看看博客、看看视频,两个小项目就上线了,除memcache以外,过程还算顺利,于是就有了这篇文章。

       正值高考,突然感怀,当年的失利,让自己更加坚强。

                                 

 

  

 

背景

  因为目前大部分项目都是.net core ,使用了memcache做为缓存服务器,首先就是 spring boot 里集成 memcache(使用 spymemcached 客户端),集成过程就不说了,添加依赖,编写帮助类,通过 @Configuration 注入就可以了。

      如果以为这样就完了,那就没有这个文章了,真正的故事才刚刚开始.....

问题

   配置完成后,就开始读取已经有缓存,然后就提示:Failed to decompress data,如下图,返回的内容就是null,但是在命令行能读出来。另外,我们缓存的都是string,不会存在序列化的问题(一开始还真怀疑过java与.net  string 序列化,好傻好天真)

        

        因为一开始看上图是 warn,就没在意,于是开始了排除方法:

    1、java缓存,java 读取正常。

    2、java缓存,.net 读取正常。

        3、直接控制添加, java 读取正常。

    4、更换java 客户端为xmemcached

    5、还尝试了很多.....甚至自己又部署几个memcache 环境

        最后,得到一个结论:.net 缓存(使用的是 Enyim.Caching 客户端),java 无法正常获取。

    一个诡异的结论,咨询别人时,都说:memcache 与语言无关!

   

失落的解决方案

   尝试了很多次失败后,决定让他凉一凉。终究还是过不了内心的坎,感觉心中有一个东西,不得踏实,又不停的搜索,甚至还在阿里云里发了工单,一开始也怀疑是阿里云的服务器有问题(直接用的阿里的memcache),后来他们技术给我说了一堆

听不太明白的内容,大概是要用 string 开头的接口去读取。这时已经明白,不是读取不到,而是解码出错,返回null而已。

  再后来,就是一个叫flag 的参数引起了我的注意, 大意是说,不同客户端在缓存时,用了不同的flag 来标记,说什么 java 的是flag 32,.net 的是2之类的,只要修改.net 为32就可以了。 反正听起来就不靠谱,又到茫茫网络中去搜索.....

  又过了两天,感觉不能这么耗下去了,没有其他方案,想着,还是修改下 Enyim.Caching 源码试试看。接着 git clone 源码,很快定位到 flag 的地方 在 DefaultTranscoder.cs  74行左右,生成flag的代码如下

复制代码
        public static uint TypeCodeToFlag(TypeCode code)
        {
            return 32;

            //return (uint)((int)code | 0x0100);  //修改前
        }
复制代码

 

     其中,TypeCode 是系统中数据类型对应一个 enum,源码如下,其中 String的值为 18,

复制代码
namespace System
{
    //
    // Summary:
    //     Specifies the type of an object.
    [ComVisible(true)]
    public enum TypeCode
    {
        //
        // Summary:
        //     A null reference.
        Empty = 0,
        //
        // Summary:
        //     A general type representing any reference or value type not explicitly represented
        //     by another TypeCode.
        Object = 1,
        //
        // Summary:
        //     A database null (column) value.
        DBNull = 2,
        //
        // Summary:
        //     A simple type representing Boolean values of true or false.
        Boolean = 3,
        //
        // Summary:
        //     An integral type representing unsigned 16-bit integers with values between 0
        //     and 65535. The set of possible values for the System.TypeCode.Char type corresponds
        //     to the Unicode character set.
        Char = 4,
        //
        // Summary:
        //     An integral type representing signed 8-bit integers with values between -128
        //     and 127.
        SByte = 5,
        //
        // Summary:
        //     An integral type representing unsigned 8-bit integers with values between 0 and
        //     255.
        Byte = 6,
        //
        // Summary:
        //     An integral type representing signed 16-bit integers with values between -32768
        //     and 32767.
        Int16 = 7,
        //
        // Summary:
        //     An integral type representing unsigned 16-bit integers with values between 0
        //     and 65535.
        UInt16 = 8,
        //
        // Summary:
        //     An integral type representing signed 32-bit integers with values between -2147483648
        //     and 2147483647.
        Int32 = 9,
        //
        // Summary:
        //     An integral type representing unsigned 32-bit integers with values between 0
        //     and 4294967295.
        UInt32 = 10,
        //
        // Summary:
        //     An integral type representing signed 64-bit integers with values between -9223372036854775808
        //     and 9223372036854775807.
        Int64 = 11,
        //
        // Summary:
        //     An integral type representing unsigned 64-bit integers with values between 0
        //     and 18446744073709551615.
        UInt64 = 12,
        //
        // Summary:
        //     A floating point type representing values ranging from approximately 1.5 x 10
        //     -45 to 3.4 x 10 38 with a precision of 7 digits.
        Single = 13,
        //
        // Summary:
        //     A floating point type representing values ranging from approximately 5.0 x 10
        //     -324 to 1.7 x 10 308 with a precision of 15-16 digits.
        Double = 14,
        //
        // Summary:
        //     A simple type representing values ranging from 1.0 x 10 -28 to approximately
        //     7.9 x 10 28 with 28-29 significant digits.
        Decimal = 15,
        //
        // Summary:
        //     A type representing a date and time value.
        DateTime = 16,
        //
        // Summary:
        //     A sealed class type representing Unicode character strings.
        String = 18
    }
}
复制代码

 

         

  根据之前得到的结果,要把 .net 客户端的flag 设置成32,于是,直接返回32,代码生成上传,不试不知道,一试吓一跳,竟然正常了。java 能正常返回缓存内容了,如下图,正常打印了

  

       刚开始真是高兴了足足10秒中,毕竟尝试了很多次失败,但转念一想,现在所有的项目,都得去引用自己编译的这个版本,以后如果 Enyim.Caching 升级了,我还得去重新下载、编译,所有项目又要重新引用,想想就后怕!

       于是,第一次有了这样的感觉:问题解决了,但是很多失落!弄完回到家,看我一脸无趣,媳妇还安慰说:“今天没解决,明天再来,明天不行,后天再来,总会拨云见日的!”

 

升级版解决方案

  缺陷的解决方案,一直萦绕心头,挥之不去,于是,还是忍不住去查询新的方案,还特意发起了一个博问,不过就 dudu 回复了,虽然没有直接解决,也给了一些新的提示,并顺利的看到了 spymemcached 的源码。找到了

  解码的类 SerializingTranscoder.java ,对于 String 并未做处理,也没有解码的问题。 解码部分源码如下,可以看到,对于 String是直接调用  decodeString

复制代码
public Object decode(CachedData d) {
    byte[] data = d.getData();
    Object rv = null;
    if ((d.getFlags() & COMPRESSED) != 0) {
      data = decompress(d.getData());
    }
    int flags = d.getFlags() & SPECIAL_MASK;
    if ((d.getFlags() & SERIALIZED) != 0 && data != null) {
      rv = deserialize(data);
    } else if (flags != 0 && data != null) {
      switch (flags) {
      case SPECIAL_BOOLEAN:
        rv = Boolean.valueOf(tu.decodeBoolean(data));
        break;
      case SPECIAL_INT:
        rv = Integer.valueOf(tu.decodeInt(data));
        break;
      case SPECIAL_LONG:
        rv = Long.valueOf(tu.decodeLong(data));
        break;
      case SPECIAL_DATE:
        rv = new Date(tu.decodeLong(data));
        break;
      case SPECIAL_BYTE:
        rv = Byte.valueOf(tu.decodeByte(data));
        break;
      case SPECIAL_FLOAT:
        rv = new Float(Float.intBitsToFloat(tu.decodeInt(data)));
        break;
      case SPECIAL_DOUBLE:
        rv = new Double(Double.longBitsToDouble(tu.decodeLong(data)));
        break;
      case SPECIAL_BYTEARRAY:
        rv = data;
        break;
      default:
        getLogger().warn("Undecodeable with flags %x", flags);
      }
    } else {
      rv = decodeString(data);
    }
    return rv;
  }
复制代码

 

 

     decodeString 代码如下,可见并无特殊处理

复制代码
  /**
   * Decode the string with the current character set.
   */
  protected String decodeString(byte[] data) {
    String rv = null;
    try {
      if (data != null) {
        rv = new String(data, charset);
      }
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
    return rv;
  }
复制代码

 

     

      再细看 SerializingTranscoder.java 的处理逻辑,在解码之前,有压缩标志,以及 decompress() 方法, 这个方法在 BaseSerializingTranscoder.java 中,源代码如下,正好有,有一个 catch 会输出,最早看到的错误信息:Failed to decompress data

  getLogger().warn("Failed to decompress data", e); 找到了问题的发生地儿,离解决方案就不远了。 第一现场很重要。

 

复制代码
  /**
   * Get the object represented by the given serialized bytes.
   */
  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);
        rv=is.readObject();
        is.close();
        bis.close();
      }
    } catch (IOException e) {
      getLogger().warn("Caught IOException decoding %d bytes of data",
          in == null ? 0 : in.length, e);
    } catch (ClassNotFoundException e) {
      getLogger().warn("Caught CNFE decoding %d bytes of data",
          in == null ? 0 : in.length, e);
    } finally {
      CloseUtil.close(is);
      CloseUtil.close(bis);
    }
    return rv;
  }
复制代码

     

      既然问题出在“解压”这里,那为什么我把 flag 设置成32就可以了呢,再看源码,判断是否解压的如下:

      static final int COMPRESSED = 2;

 if ((d.getFlags() & COMPRESSED) != 0) {
  data = decompress(d.getData());
 }

.net 里默认是 18 | 0x0100 = 274
 274 &  2 = 2  不等于0,会去解压,然后出错了。

32 & 2 =0, 不解压,正常。

这里其实验证了,flag与客户端无关。压缩标志与数据类型有关。

   问题已经明确了,只要程序不走解压就是正常的,并且,这些参数,都是类内部的状态,外面无法修改,那可以扩展吗?使用自己的解码类来实现,肯定是可以的,看 SerializingTranscoder 与 BaseSerializingTranscoder 的继承关系就知道,

     再看  get 方法 memcachedClient.get(String key, Transcoder<T> tc),支持自定义  Transcoder, 接下来,问题就简单了,自定义一个 Transcoder 继承  BaseSerializingTranscoder 实现 Transcoder,不用解压,直接解码。

     最后,其实,我只是在  SerializingTranscoder  基础上,把 static final int COMPRESSED = 0,就可以了,都不解压。 获取代码如下

HMSerializingTranscoder transcoder = new HMSerializingTranscoder();
return memcachedClient.get(key,transcoder);

 

 

结语

  分析到此,问题明了,方案明确,水到渠成,问题解决了。在不修改第三方源码的基础上,通过扩展解决了,也不用担心第三方升级的问题了,这样就比第一种别扭的方案舒服多了。

  第一次感受到阅读源码,与深究一个问题的带来的收获 -- 杠杠的

 

   成为一名优秀的程序员!

来自 https://www.cnblogs.com/jijunjian/p/9152927.html


普通分类: