Redis 和 memcached 的比较

679 阅读7分钟

  Redis 和 Memcached 都属于内存数据存储系统,常被用于缓存数据库查询结果从而加快应用的响应时间。在实际应用中,仍然需要根据实际的应用场景以及二者各自的特定进行选择。

⒈ 数据结构

⓵ Memcached

  Memcached 的数据结构很单一,只能存储字符串类型的 key value 键值对。key 的长度不能超过 250B,value 的长度不能超过 1M。

  Memcached 不支持在服务端对 value 值进行操作。如果要对 value 进行操作,首先需要将 value 读取到客户端(get),然后再对 value 进行修改,修改完成之后再保存到服务端(set)。

  鉴于以上原因,Memcached 更适合存储只读数据。

⓶ Redis

  Redis 支持的数据类型多样,常用的有 string、hash、list、set、sorted set(zset) 等。虽然 Redis 也已 key value 键值对的方式存储,但 key 和 value 的最大长度可以达到 512 M(对于 list 、set 这种聚合类型,512 M 是针对其中的每个元素的长度的限制)。

  Redis 支持在服务端对数据进行操作。Redis 的每一种数据类型,都有丰富的命令可以对其进行操作(即使只是修改部分数据),与 Memcached 相比,这可以大大减小网络 I/O。

  Redis 常用数据类型介绍:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

/* The actual Redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

  Redis 底层数据结构,其中 type 为 value 的数据类型,而 encoding 为 Redis 底层实际存储 value 时使用的数据类型。

  • string

   string 是 Redis 中最常用的数据类型,其 encoding 格式默认为 raw,但如果调用 INCR 或 DECR 对其进行操作,那么 string 的 encoding 格式会转换为 int(前提是 value 可以转换成数值类型,否则会报错)。

  • hash

  hash 常用来存储 object 类型的数据,由于 hash 会在存储是对数据进行压缩,所以具有很高的内存利用率。另外,hash 支持对 object 中的单个属性进行操作,这样可以降低数据操作的复杂度,同时减少网络 I/O。

  hash 的底层 encoding 格式默认为 ziplist,当 hash 中存储的元素格式超过配置文件中 hash-max-ziplist-entries 所设置的值或 hash 中最长元素的长度超过配置文件中 hash-max-ziplist-value 所设置的值时,hash 的 encodig 格式会转换为 hash table。当 hash 的 encoding 格式为 ziplist 时,需要用两个连续的 ZIPlistEntry 分别存储 hash 的 key 和 value。

/* Each entry in the ziplist is either a string or an integer. */
typedef struct {
    /* When string is used, it is provided with the length (slen). */
    unsigned char *sval;
    unsigned int slen;
    /* When integer is used, 'sval' is NULL, and lval holds the value. */
    long long lval;
} ziplistEntry;
  • list

   list 的底层 encoding 格式为一个双向的 quicklist。双向的 quicklist 使得正向和反向的遍历都非常方便,但同时增加了内存的开销。

/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
 * 'count' is the number of total entries.
 * 'len' is the number of quicklist nodes.
 * 'compress' is: 0 if compression disabled, otherwise it's the number
 *                of quicklistNodes to leave uncompressed at ends of quicklist.
 * 'fill' is the user-requested (or default) fill factor.
 * 'bookmakrs are an optional feature that is used by realloc this struct,
 *      so that they don't consume memory when not used. */
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : QL_FILL_BITS;              /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporary decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

  quicklist 由一个个的 quicklistNode 组成,每一个 quicklistNode 包含一个 ziplistziplist 中元素的个数最多为配置文件中 list-max-ziplist-size 所设置的值。当 ziplist 中元素的数量超过配置文件中所设置的阈值时,就会创建一个新的 quicklistNode,然后在新创建的 quicklistNode 中创建新的 ziplist

  另外,每一个 quicklistNode 都会存储其所包含的 ziplist 中元素的个数,这样就为按照索引查找元素以及提供了非常大的便利。

  当往一个位于 quicklist 中间的 quicklistNode 中的 ziplist 中插入元素时,由于当前 ziplist 中元素的个数已经达到阈值,所以需要对当前的 ziplist 做拆分。由于这些拆分操作没有特殊的限制,所以 Redis 内部会尝试将这些拆分后的 ziplist 进行合并,但前提是合并之后的 ziplist 中的元素个数不能超过阈值。

  • set

  set 的底层 encoding 格式为 hash table,通过计算 key 的 hash 值,可以做到自动去重,同时还可以快速判断一个给定的 key 是否存在。有一种特殊情况:当 set 中的元素个数不超过配置文件中 set-max-intset-entries 所设置的值,并且每个元素都可以转换成十进制数值类型,同时这些数值的范围不超过 64 位有符号整型的范围,那么此时 set 的encoding 格式为 intset。

  • zset

  zset 底层 encoding 同时使用 hash table 和 skip list 同时实现。hash table 中保存元素和 score 的映射关系,skip list 则按照 score 从小到大的顺序存储元素。

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

  当 zset 中元素个数少于配置文件中 zset-max-ziplist-entries 所设置的值,同时任一个元素的长度小于配置文件中 zset-max-ziplist-value 所设置的值时,zset 底层 encoding 采用 ziplist 格式。此时,Redis 使用两个连续的 zlentry 节点来保存一个 zset 元素的完整信息,第一个节点存储元素的值,第二个节点存储 score 的值。

⒉ 内存管理

  Redis 和 Memcached 都使用 C 语言实现。在 C 语言中,内存的分配和释放通常采用 malloc/free 实现,但这种做法实际隐藏了一些问题:

  • 如果 malloc 和 free 的内存空间不一致,会导致内存泄漏
  • 频繁的调用会产生大量的内存片段,导致这些片段无法被回收以重复使用,降低了内存的利用率
  • 相对于函数调用,系统调用更消耗资源

  所以,为了提升内存管理效率,提高内存利用率,Redis 和 memcached 都设计了自己的内存管理机制。

⓵ Memcached 的内存管理机制

Memcached 内存管理

  Memcached 会将申请到的内存分成若干 size 不同的 chunk。size 相同的 chunk 会分组到同一个 slab 下。第一个 slab 下 chunk 的 size 默认为 48K(可以通过配置参数修改),之后每一个 slab 下的 chunk 的 size 都会在上一个 slab 下 chunk 的 size 的基础上乘 1.25 然后向上取整为 8 的整数倍。

  另外,slab 下的 chunk 是存在于 page 中的。每个 slab 下可以包含尽可能多的 page(通过启动参数设置),每个 page 的 size 为 1M,包含尽可能多的 chunk。同一个 slab 下的 page 中包含的 chunk 的数量相等。

  当向 Memcached 写入数据时,Memcached 首先会根据数据的 size 选择一个最合适的 slab,然后在该 slab 下找到一个空闲的 chunk 来存储数据。但如果所有的 chunk 都已经被写入数据,那么 Memcached 会根据 LRU 原则找到一个合适的 chunk 然后将旧数据清除并写入新数据。由于每个 chunk 的 size 固定,而存储的数据的 size 又不会刚好等于 chunk 的 size,所以 Memcached 的这种内存管理机制会造成空间浪费。

Memcached 的 LRU 机制

Memcached 的每个 slab 下会根据 chunk 中数据上次被使用的时间保存一个 LRU 列表。为了保证数据的一致性以及操作的原子性,Memcached 会对每次被操作的数据加锁。当根据 LRU 规则清除旧数据以存放新写入的数据时,如果被清除的数据处于锁定状态,那么 Memcached 会尝试清除 LRU 列表中下一个 chunk 中的数据。为了保证响应速度,Memcached 只会尝试 5 次,如果 LRU 列表上的前 5 个 chunk 都处于锁定状态,那么 Memcached 会返回 SERVER_ERROR Out of memory storing object

⓶ Redis 内存管理机制

Redis 内存管理机制

  Redis 不会提前分配内存。当有数据写入 Redis 时,Redis 是根据写入数据的 size 类申请内存,实际申请的内存空间会比写入数据的 size 多一个 PREFIX_SIZE 的大小。其中,PREFIX_SIZE 部分用来存储写入数据的 size,memory block 用来存储实际数据。

#if defined(__sun) || defined(__sparc) || defined(__sparc__)
#define PREFIX_SIZE (sizeof(long long))
#else
#define PREFIX_SIZE (sizeof(size_t))
#endif

  当 Redis 的内存使用达到设定的值时,Redis 需要清除一部分数据以保证写入操作可以正常执行,否则写入会报错。Redis 的内存清理策略有八种:

  • noeviction:不做清理,此时继续写入会报错
  • volatile-lru:设置了过期时间的 key,按照 LRU(least recently used) 规则清理
  • allkeys-lru:所有的 key 都按照 LRU 规则清理
  • volatile-lfu:设置了过期时间的 key,按照 LFU(least frequently used)规则清理
  • allkeys-lfu:所有 key 都按照 LFU 规则清理
  • volatile-random:随机清理设置了过期时间的 key
  • allkeys-random:随机清理 key
  • volatile-ttl:清理距离所设置的过期时间最近的 key

⒊ 持久化

  Memcached 不支持数据的持久化,Redis 支持两种方式的数据持久化:RDB 和 AOF。

  • RDB    Redis 在指定的时间周期内会对当前所存储的数据作一个快照然后写入 RDB 文件,这样,在 Redis 重启时数据可以很快恢复。但这种方式存在问题,即 Redis 只有在指定的周期内有足够数量的 key 的值被修改时才会触发。如果 Redis 崩溃,那么在上一次快照之后写入的数据无法被恢复。

  • AOF    AOF 以追加的方式只记录那些对 Redis 进行写操作的命令。所以,随着时间的增长,AOF 文件的 size 会越来越大。为此,Redis 提供了 AOF 的重写功能。重写之后的 AOF 文件里,Redis 中每一条记录只会有一条对应的操作命令。

⒋ 集群管理

⓵ Memcached

  Memcached 本身不支持分布式,Memcached 的分布式只能通过客户端的分布式算法实现。

Memcached 的分布式模型

  客户端在向 Memcached 集群写入数据时,先会在客户端计算得到目标节点,然后直接将数据写入目标节点。在读取数据时也是一样,先计算得到目标节点,然后直接从目标节点读取数据。

⓶ Redis

  Redis 支持服务端的集群策略。Redis 集群实现了分布式的存储,并且允许 SPOF(single point of failure)。Redis 集群没有中心节点,同时支持线性扩展。

   Redis 集群的 key 分布在 16384 个不同的 slot 当中,这些 slot 分布在不同的 Redis 节点上。具体每个 key 应该分布在哪个 slot 通过计算 CRC16(key) % 16384 确定。

  Redis 集群通过引入主从节点来保证数据的高可用性。每一个主节点分配两个从节点,这样,在主节点出故障时,Redis 集群会自动选择一个从节点成为新的主节点,同时另一个从节点可以继续复制主节点的数据。

Redis 分布式模型

⒌ 协议

  Redis 使用 TCP 作为通信协议;Memcached 既可以使用 TCP 又可以使用 UDP。

⒍ 性能

   Redis 的写入速度略慢于 Memcached,但读取速度以及内存的使用率方面都优于 Memcached。