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
包含一个 ziplist
,ziplist
中元素的个数最多为配置文件中 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 会将申请到的内存分成若干 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 的每个 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 是根据写入数据的 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 集群写入数据时,先会在客户端计算得到目标节点,然后直接将数据写入目标节点。在读取数据时也是一样,先计算得到目标节点,然后直接从目标节点读取数据。
⓶ Redis
Redis 支持服务端的集群策略。Redis 集群实现了分布式的存储,并且允许 SPOF(single point of failure)。Redis 集群没有中心节点,同时支持线性扩展。
Redis 集群的 key 分布在 16384 个不同的 slot 当中,这些 slot 分布在不同的 Redis 节点上。具体每个 key 应该分布在哪个 slot 通过计算 CRC16(key) % 16384 确定。
Redis 集群通过引入主从节点来保证数据的高可用性。每一个主节点分配两个从节点,这样,在主节点出故障时,Redis 集群会自动选择一个从节点成为新的主节点,同时另一个从节点可以继续复制主节点的数据。
⒌ 协议
Redis 使用 TCP 作为通信协议;Memcached 既可以使用 TCP 又可以使用 UDP。
⒍ 性能
Redis 的写入速度略慢于 Memcached,但读取速度以及内存的使用率方面都优于 Memcached。