Redis 和 Memcached 有什么区别?

分类:编程技术 时间:2024-02-20 15:16 浏览:0 评论:0
0
小编给大家分享一下Redis和Memcached的区别。希望您读完这篇文章后能有所收获。我们一起来讨论一下吧!

Memcached和redis作为近年来最常用的缓存服务器,相信大家都不陌生。两年前我上学的时候,曾经读过他们的主要源码。现在写一篇笔记,从个人角度简单比较一下他们的实现方法。我只能用它作为评论。如果我的理解有错误,欢迎指正。

本文使用的架构类图片大部分来自网络。有些图片与最新实现有所不同,文章中已指出。

1.概述

要阅读一个软件的源代码,首先要了解该软件的用途。 memcached 和 redis 有何用途?众所周知,数据一般都是放在数据库中,但是查询数据比较慢。尤其是当用户较多时,频繁的查询会耗费大量的时间。怎么做?数据放在哪里才能快速查询?记忆中一定有这样的事。 Memcached和redis将数据存储在内存中,并以key-value的方式进行查询,可以大大提高效率。因此,它们一般被用作缓存服务器来缓存常用的数据。当需要查询时,直接从中获取,减少了数据库查询次数,提高了查询效率。

2.服务方式

memcached和redis如何提供服务?它们是独立的进程。如果需要,可以将它们变成守护进程。因此,如果我们的用户进程想要使用memcached和redis服务,就需要进行进程间通信。考虑到用户进程、memcached和redis不一定在同一台机器上,需要支持网间通信。因此,memcached和redis自身是网络服务器,用户进程通过网络与它们传输数据。显然,最简单、最常用的就是使用tcp连接。另外,memcached和redis都支持udp协议。并且当用户进程与memcached和redis在同一台机器上时,也可以使用unix域socket通信。

3.事件模型

让我们从它们的实现方式开始。首先,我们来看看他们的事件模型。

自从epoll问世以来,几乎所有的网络服务器都放弃了select和poll,转而采用epoll。 redis也是如此,只不过它还提供了select和poll的支持。你可以配置使用哪一个,但一般使用epoll。另外,对于BSD来说,还支持使用kqueue。 Memcached是基于libevent的,但是libevent底层也使用了epoll,所以可以认为它们都使用了epoll。 epoll的特点这里就不介绍了。有网上很多介绍文章。

它们都是使用epoll进行事件循环,但是redis是单线程服务器(redis也是多线程的,但是除了主线程之外,其他线程没有事件循环,只是执行一些后台存储工作),而memcached是多线程的。 redis的事件模型非常简单,只有一个事件循环,是一种简单的reactor实现。不过,redis事件模型有一个亮点。我们知道epoll是针对fd的,它返回的ready事件也只是fd。 redis中的fd是连接服务器和客户端的socket的fd,但是处理的时候需要根据这个fd如何找到具体的客户端信息呢?通常的处理方式是用红黑树保存fd和client信息,通过fd进行查找,效率为lgn。不过,redis 比较特殊。可以设置redis客户端数量上限,即可以知道redis打开的fd上限n 同时。并且我们知道进程的fd不会同时重复(fd只有关闭后才能恢复)。 (),所以redis使用数组,并使用fd作为数组的下标。数组的元素是客户端信息。这样就可以通过fd直接定位到客户端信息。搜索效率为O(1),这也消除了复杂度。红黑树的实现(我曾经用C写过一个网络服务器,因为想维护fd和connect的对应关系而不想自己写红黑树,所以就用了STL中的set ,导致项目变成了c++,最后项目是用g++编译的,不告诉你谁知道呢?)。显然,这种方法只能用于连接数上限已经确定且不会太大的网络服务器。像 nginx 这样的 HTTP 服务器不适合。 nginx只是写了自己的red-black 树。

Memcached是多线程的,采用master-worker方式。主线程监听端口,建立连接,然后依次分配给各个工作线程。每个从属线程都有一个事件循环,为不同的客户端提供服务。主线程和工作线程之间使用管道通信。每个工作线程都会创建一个管道,然后保存写端和读端,并将读端添加到事件循环中监听可读事件。同时,每个从属线程都有一个就绪连接队列。主线程连接后,会将连接的项放入这个队列中,然后向该线程的管道的写入端写入一个连接命令,这样事件循环中添加的管道的读取端就会准备好,从线程,解析命令,如果命令发现有连接,那么就会去自己的就绪队列中获取连接并处理。多线程的优点就是可以充分发挥多核的优势,但是写程序稍微麻烦一些。 Memcached 有各种用于线程同步的锁和条件变量。

4。内存分配

memcached和redis的核心任务都是操作内存中的数据,内存管理自然是核心内容。

首先看看他们是如何分配内存的。 Memcached有自己的内存池,这意味着它预先分配一大块内存,然后从内存池中分配内存。这样可以减少内存分配次数,提高效率。这也是大多数网络服务器的实现方式。但各个内存池的管理方式根据具体情况有所不同。 Redis没有自己的内存池,而是在使用的时候直接分配,即需要的时候分配。内存管理留给内核,只负责获取和释放(redis是single-t线程化并且没有自己的内存池。是不是感觉实现太简单了?这是因为它的重点是数据库模块)。不过,redis支持使用tcmalloc来替代glibc的malloc。前者是Google的产品,比glibc的malloc更快。

由于redis没有自己的内存池,所以内存申请和释放的管理就简单多了。只需malloc和free就可以直接使用,非常方便。 Memcached支持内存池,所以内存申请都是从内存池中获取,free也返回给内存池,所以需要很多额外的管理操作,实现起来很麻烦。后面会在memcached的slab机制中详细解释。分析。

5。数据库实现

接下来我们看看他们的核心内容,各自数据库的实现。

1. Memcached数据库实现

Memcached仅su支持key-value,即一个key只能对应一个value。它的数据也是以键值对的形式存储在内存中,并且使用了slab机制。

首先我们看一下memcached是如何存储数据的,即存储键值对。如下图所示,每个键值对都存储在一个item结构中,包括相关的属性和键值对。

项目存储键值对。当物品很多时,如何找到特定的物品是一个问题。 。所以memcached维护了一个哈希表,用于快速查找项目。 hash表采用开链方式(和redis一样)解决key冲突。每个哈希表桶存储一个链表。链表节点是项的指针。如上图所示,h_next指的是桶中链表的下一个节点。 。哈希表支持扩展(当项数大于桶数的1.5时扩展)。瑟尔e 是一个primary_hashtable 和一个old_hashtable。正常情况下使用primary_hashtable,但是扩展时,设置old_hashtable = Primary_hashtable,然后将primary_hashtable设置为新申请的。 hash表(桶数乘以2),然后将old_hashtable中的数据依次移动到新的哈希表中,并使用一个变量expand_bucket来记录移动了多少个桶。移动完成后,释放原来的old_hashtable(Redis也有两个哈希表,也被移动,但不是由后台线程完成,而是一次移动一个桶)。扩容操作由后台扩容线程完成。当需要扩展时,使用条件变量来通知。扩展完成后,会阻塞等待扩展的条件变量。这样,扩展时,可能会在primary_hashtable或old_hashtable中找到一个项。您需要比较其桶的位置和Expand_bucket 的大小来确定它位于哪个表中。

该项目分配在哪里?从板坯。如下图所示,memcached有很多slabclass,它们管理slab。每块板实际上都是树干的集合。真实的物品分配在后备箱中,每个后备箱分配一个物品。板中树干的尺寸是相同的。对于不同的板,树干的尺寸成比例增加。当您需要申请新物品时,请根据其尺寸选择行李箱。规则是比它大的最小的树干。这样,不同大小的item就被分配在不同的slab中,并由不同的slabclass来管理。这样做的缺点是会浪费一些内存,因为一个trunk可能比item还大,如图2所示。当分配100B的item时,选择112 trunk,但是会浪费12B,而这部分内存资源没有被使用。


如果是上图中,整个结构是这样的。 labclass 管理slab。一个slabclass有一个slab_list,它可以管理多个slab。同一slabclass中的slab的主干尺寸都是相同的。 labclass有一个指针槽,它保存已释放的未分配项(不是真正的释放内存,只是不再使用)。当有未使用的item时,将它们放入slot的头部,这样每次需要在当前slab中分配item时,都可以直接访问该slot,而不管该item是否已经被使用过未分配或释放。

然后,每个slabclass对应一个链表,有一个head数组和一个tail数组,分别存储链表的头节点和尾节点。链表中的节点是更改后的slabclass分配的项。新分配的项目放置在头部。链接列表中靠后的项目意味着它们还没有被en 使用了很长时间。当slabclass内存不足,需要删除一些过期的项时,可以从链表尾部删除。是的,这个链表就是为了实现LRU而设计的。仅仅依靠它是不够的,因为链表的查询是O(n),所以在定位项时,使用哈希表。这已经可用了。所有分配的项目都已经在哈希表中,因此哈希用于查找该项目。 ,然后链表可以存储最近使用的项的顺序,这也是lru的标准实现方法。

每次需要分配新的item时,找到slabclass对应的链表,从尾到前查找,看看该item是否已经过期。如果已经过期,则直接使用过期的物品作为新物品。如果没有过期,则需要从slab中分配trunk。如果slab用完,需要在slabclass中添加一个slab。

Memcached支持设置expiration时间,即过期时间,但是它内部并没有定期检查数据是否过期。相反,当客户端进程使用数据时,memcached会检查过期时间,如果过期则直接返回错误。这样做的好处是不需要额外的CPU来检查过期时间。缺点是过期数据可能很长时间不会被使用,不会被释放而占用内存。

Memcached是多线程的,只维护一个数据库,因此可能会有多个客户端进程操作同一份数据,这可能会导致问题。比如A改变了数据,然后B也改变了数据,那么A的操作就会被覆盖,而A可能不知道A的任务数据当前状态是他改变后的值,所以可能会出现问题。为了解决这个问题,memcached使用了CAS协议。简单来说,该项保存了一个64位的unsigned int值来标记数据的版本。每次更新(数据值修改),版本号增加,每次数据改变都需要比较。检查客户端进程发送的版本号与服务器端项目的版本号是否一致。如果一致,则可以进行更改操作。否则会提示脏数据。

上面是memcached如何实现键值数据库的介绍。

2. Redis数据库实现

首先,redis数据库功能更强大,因为与memcached只支持保存字符串不同,redis支持字符串、列表、集合、排序集、哈希表。 5个数据结构。比如存储一个人的信息,可以使用哈希表,以人的名字作为key,然后name super,age 24。通过key和name,可以得到name super,或者通过key和age,就可以得到年龄24了,这样当你只需要获取年龄的时候,就不需要r了取出人的全部信息,然后从里面找年龄,直接获取年龄即可,高效、方便。

为了实现这些数据结构,redis定义了一个抽象对象redis对象,如下所示。每个对象都有一个类型,一共有5种类型:字符串、链表、集合、有序集合、哈希表。同时,为了提高效率,redis为每种类型准备了多种实现方法,根据具体场景选择合适的实现方法。编码代表对象的实现方法。然后记录该对象的LRU,即最后一次被访问的时间。同时,redis服务器中会记录一个当前时间(近似值,因为这个时间只有在服务器进行自动维护时才会每隔一定时间更新一次)。两者之间的差异可以用来计算该对象有多久没有被访问过。然后redis对象中还有引用计数,用于共享对象并确定对象的删除时间。最后用一个void*指针来指向对象的真实内容。官方说法是,由于使用了抽象的redis对象,操作数据库中的数据要方便很多。所有redis object对象都可以统一使用。当需要区分对象类型时,根据类型进行判断。而且官方由于采用了这种面向对象的方式,redis代码看起来很像C++代码,但实际上都是用C编写的。

//#define REDIS_STRING 0 // String classType // #define REDIS_LIST 1 // 链表类型 // #define REDIS_SET 2 // 集合类型(无序),可以查找差集、并集等 // #define REDIS_ZSET 3 // 有序集合类型 // #define REDIS_HASH 4 // 哈希类型 // #define REDIS_ENCODING_RAW 0 /* 原始表示 */ //raw raw // #define REDIS_ENCODING_INT 1 /* Encoded 为整数 *///#define REDIS_ENCODING_HT 2 /* 编码为哈希表 *///#define REDIS_ENCODING_ZIPMAP 3 /* 编码为 zipmap *///#define REDIS_ENCODING_LINKEDLIST 4 /* 编码为常规链表 *///# define REDIS_ENCODING_ZIPLIST 5 /* 编码为ziplist *///#define REDIS_ENCODING_INTSET 6 /* 编码为intset *///#define REDIS_ENCODING_SKIPLIST 7 /* 编码为skiplist *///#define REDIS_ENCODING_EMBSTR 8 /* 嵌入式sds字符串编码*/ typedef struct redisObject { 无符号类型:4; // 对象的类型,包括 /* 对象类型 */ unsigned encoding:4; // 底层为了节省空间,一种类型的数据, //可以采用不同的存储方式 unsigned lru:REDIS_LRU_BITS; /* lru 时间(相对于 server.lruclock) */ int refcount; // 引用计数 void *ptr;} robj;

毕竟redis仍然是一个键值数据库,无论支持多少种数据结构,最终都是以键值格式存储的,但值可以是链表、集合、有序集合、hash table等。和memcached一样,所有的key都是字符串,字符串也用于集合、排序集、哈希表等具体存储。C中没有现成的字符串,所以redis的第一个任务是实现一个字符串,命名为sds(简单动态字符串)。下面的代码是一个非常简单的结构。 len 存储字符串的总内存长度,free 表示仍然可用。有多少字节未使用,buf存储具体数据。显然 len-free 是字符串的当前长度。

struct sdshdr { int len; int 自由; char buf[];};

字符串解析完毕,所有的key都保存为sds即可,但是key和value是怎么关联的呢?键值格式很容易在脚本语言中处理。直接用字典就可以了。 C没有字典。我应该怎么办?自己写一个(redis很热衷于造轮子)。看下面的代码,privdata存储了额外的信息并且很少使用d,至少我们发现了。 dicttht 是一个特定的哈希表。一个dict对应两个hash表。这是为了扩展(包括rehashidx用于扩展)。 dictType 存储哈希表的属性。 Redis 还实现了 dict 的迭代器(因此它看起来像 C++ 代码)。

哈希表的具体实现与mc类似。它也是采用开链的方式来解决冲突,但是其中使用了一些技巧。例如,使用dictType存储函数指针可以动态配置桶中元素的操作方法。再比如dicttht中保存的sizemask取size(桶数)-1,与key一起使用进行&运算,以代替求余运算,加快进程速度等。一般来说有两种哈希表,哈希表的每个桶存储一个dictEntry链表,dictEntry存储具体的键和值。

如前所述,两个 dicttht 的一个 dict 是为了扩展上(其实也有收缩)。通常,dict只使用dicttht[0]。当dict[0]中现有条目的数量达到桶数量的一定比例时,就会触发扩容和缩容操作。我们统称为重新哈希。此时,为dicttht[1]申请rehash大小的内存,然后将dicttht[0]中的数据移动到dicttht[1]中,并使用rehashidx记录已移动的桶的数量。当所有存储桶都被移动后,重新哈希就完成了。此时将dicttht[1]改为dicttht[0],将原来的dicttht[0]改为dicttht[1],并改为null。与memcached不同的是,这里不需要开后台线程来做,而是在事件循环中完成,并且rehash不是一次完成,而是分成多次。每次用户操作dict之前,redis都会移动一桶数据,直到rehash完成。这样,该动作就被分成多个小动作来完成,时间开销为rehash 均匀分配给每个用户操作。这避免了当请求导致rehash时用户需要等待很长时间,并且直到rehash完成才返回。 。然而,在rehash过程中,每个操作都会变慢一点,而且用户并不知道redis在他的请求中间添加了移动数据操作。感觉redis太便宜了:-D

typedef struct dict { dictType *type; // 哈希表的相关属性 void *privdata; // 附加信息 dicttht ht[2]; // 两个哈希表,primary 和 secondary ,用于扩展 int rehashidx; /* rehashing not in Progress if rehashidx == -1 */ // 记录当前数据迁移位置,扩展时使用 int iterators; /* 当前运行的迭代器数量 */ // 当前存在的迭代器数量} dict;typedef struct dicttht { dictEntry **table; // dictEntry 是项目。多个项在哈希桶中形成一个链表。表是一个由多个链表头指针组成的数组指针。无符号长尺寸; // 这是桶的数量。 // sizemask取size - 1,然后来一个数据 当使用计算出的hashkey时,让hashkey & sizemask决定它要放入的bucket的位置。 // 当size为2^n时,sizemask为1.. .111,与 hashkey % 大小相同。效果,但是使用 & 会快很多。这就是unsigned long sizemask的原因;无符号长期使用; // 已经有 dictEntry 值的数量} dicttht; typedef struct dictType { unsigned int (*hashFunction)(const void *key); // 哈希方法 void *(*keyDup)( void *privdata, const void *key); // 密钥复制方法 void *(*valDup)(void *privdata, const void *obj); // 值复制方法 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 键之间的比较 void (*keyDestructor)(void *privdata, void *key); // 销毁键 void (*valDestructor)(void *privdata, void * obj); //破坏值} dictType;typedef struct dictEntry { void *key; }联合{无效*val; uint64_t u64; int64_t s64; } v; struct dictEntry *next;} dictEntry;

有了dict,就可以轻松实现数据库了。所有读取并存储在diIn ct中的数据,key以key(字符串)的形式存储在dictEntry中,void*指向一个redis对象,可以是五种类型中的任意一种。如下图所示,结构是这样的,但是这个图已经过时了,和redis3.0有一些不一致。

5 种类型的对象中的每一种都至少有两个底层实现。字符串共有三种类型:REDIS_ENCODING_RAW、REDIS_ENCIDING_INT、REDIS_ENCODING_EMBSTR。链表包括:普通双向链表和压缩链表。压缩链表简单来说就是将数组转化为链表,连续的空间,然后通过存储字符串的大小信息来模拟链表。与普通链表相比,可以节省空间,但它作为副作用。由于是连续的空间,当改变内存大小时,需要重新分配,并且由于保存的是字符串的字节大小,因此可能会导致不断更新(具体实现请详细看代码)。 Set有dict和intset(用它来存储所有整数),sorted set有:skiplist和ziplist,hashtable实现有压缩list、dict和ziplist。 Skiplist 是一个跳过列表。它的效率接近红黑树,但是实现起来比红黑树简单很多,所以就采用了(奇怪,这里没有重新发明轮子,是不是因为这个轮子有点难) ?)。哈希表可以使用dict来实现。 dict中,每个dictentry中的key存储的是key(即哈希表中键值对的key),value存储的是value。它们都是字符串。对于set中的dict来说,每个dictentry中的key存储的是set中特定元素的值,值为nu二。图中的zset(有序集)是错误的。 zset是使用skiplist和ziplist实现的。首先,skiplist很容易理解,所以只需将其视为红黑树的替代品即可。和红黑树一样,它也可以排序。如何使用ziplist来存储zset?首先,在zset中,集合中的每个元素都有一个分数,用于排序。因此,在ziplist中,根据score,先存储该元素,然后存储其score,然后是下一个元素,最后是score。这是连续存储,因此在插入或删除时,需要重新分配内存。所以当元素数量超过一定数量,或者某个元素的字符数量超过一定数量时,redis会选择使用skiplist来实现zset(如果当前使用的是ziplist,则会取出ziplist中的数据out,存储到一个新的skiplist中,然后ziplist会被删除并改变,这是底层的转换,其他类型的redis对象也可以转换)。另外,ziplist是如何实现hashtable的呢?其实很简单,就是存储一个key,存储一个value,然后存储一个key,然后存储一个value。它仍然是顺序存储的,类似于zset实现,所以当元素超过一定数量,或者某个元素的字符数超过一定数量时,就会转换成哈希表来实现。各种底层实现方式是可以转换的,redis可以根据情况选择最合适的实现方式。这也是使用类似面向对象实现方法的好处。

需要指出的是,使用skiplist实现zset时,实际上使用的是一个dict,它存储的是相同的键值对。为什么?因为skiplist的搜索只有lgn(可能会变成n),而dict可以是O(1),所以用一个dict来加速搜索。由于skiplist和dict可以指向同一个redis对象,所以不会占用太多内存阿斯特德。另外,使用ziplist实现zset时,为什么不使用dict来加快查找速度呢?因为ziplist支持的元素数量较少(数量较多时转为skiplist),而且顺序遍历也很快,所以不需要dict。

从这一点来看,上面的dict、dictType、dictHt、dictEntry、redis对象都是很有思想的。他们共同努力实现了一个灵活、高效、面向对象的色彩数据库。不得不说redis数据库的设计还是很强大的。

与memcached不同的是,redis有多个数据库,默认有16个,编号为0-15。客户可以选择使用哪个数据库,默认使用0号数据库。不同数据库的数据是不共享的,即同一个key可以存在于不同的数据库中,但在同一个数据库中,key必须是唯一的。

Redis还支持过期时间的设置。我们看一下上面的redis对象。瑟尔e 没有保存过期的字段。那么redis是如何记录数据的过期时间的呢? Redis 向每个数据库添加另一个字典。这个字典称为过期字典。 dict项中的key是一对key,value是一个数据为64位int的redis对象。 thisint 是过期时间。这样,在判断某个key是否过期时,可以在expire dict中查找,取出expire时间,与当前时间进行比较。为什么要这样做?因为并不是所有的key都会设置过期时间,对于没有设置过期时间的key来说,保存过期时间会浪费空间。而是使用一个expire dict单独保存,可以根据需要灵活使用内存(检测到key过期时,就会从expire dict中删除)。

redis的expire机制是怎样的?与memcahed类似,redis也是惰性删除的,即在使用数据时,首先检查key是否过期,过期则删除,然后返回错误。单纯依靠laz如上所述,y删除可能会导致内存浪费,所以redis也有补充的解决方案。 redis中有一个定期执行的函数,称为servercron。这是维护服务器的功能。其中,过期数据将被删除。 ,注意不是全部删除,而是随机选取一定时间内每个数据库的expire dict中的数据。如果过期则删除,否则重新选择,直到指定时间到。即随机选择过期数据并删除。操作时间有两种,一种较长,另一种较短。一般是进行短期删除,长期删除则定期进行。这样可以有效缓解单独惰性删除带来的内存浪费问题。

以上就是redis数据的实现。与memcached不同的是,redis还支持数据持久化,下面介绍一下。

4.redis数据库持久化

赌注的最大区别redis和memcached的区别在于redis支持数据持久化。这也是很多人选择使用redis而不是memcached的最大原因。 redis的持久化分为两种策略,用户可以配置和使用不同的策略。

4.1 RDB持久化 当用户执行save或bgsave时,会触发RDB持久化操作。 RDB持久化操作的核心思想是将数据库完整地保存在文件中。

如何存储?首先存储一个REDIS字符串用于验证,表明它是一个RDB文件,然后保存redis版本信息,然后具体数据库,然后存储结束符EOF,最后使用校验和。关键是数据库。从它的名字就可以看出它存储了多个数据库。数据库按数字顺序存储。 0号数据库存完后,就轮到1号,然后是2号,以此类推。最后一个数据库。

各个数据库的存储方式如下低点。首先是一个1字节的常量SELECTDB表示已经切换了db,然后下一个是连接数据库的编号。它的长度是可变的,然后是具体的键值对的数据。

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime, long long now){ /* 保存过期时间 */ if (expiretime != -1) { /* 如果该密钥已经过期则跳过它 */ if (expiretime < now) return 0 ; if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) 返回 -1; if (rdbSaveMillisecondTime(rdb,expiretime) == -1) 返回 -1; /* 保存类型、键、值 */ if (rdbSaveObjectType(rdb,val) == -1) return -1; if (rdbSaveStringObject(rdb,key) == -1) 返回 -1; if (rdbSaveObject(rdb,val) == -1) 返回 -1;返回1; }

从上面的代码也可以看出,存储的时候,先检查过期时间。如果已经过期,就不要保存。否则,保存exp愤怒时间。注意expire是及时存储的。 time,其类型也先存储为REDIS_RDB_OPCODE_EXPIRETIME_MS,然后存储具体的过期时间。接下来存储真正的键值对,先存储值的类型,然后存储key(存储为字符串),然后存储value。

在rdbsaveobject中,val会根据不同的类型采用不同的存储方式。但从根本上来说,它会被转换成字符串来存储。例如,如果 val 是一个链接列表,那么将首先存储整个列表。字节数,然后遍历链表,取出数据,按照字符串顺序写入文件。对于哈希表来说,也是先计算字节数,然后依次取出哈希表中的dictEntry,将其key和value以字符串的形式存储,然后存储下一个dictEntry。简而言之,RDB的存储方式,对于一个键值对,会先存储过期时间(如果有的话),然后value的类型,然后存储key(字符串方式),然后根据value的类型和底层实现来存储。 ,将值转换为字符串进行存储。为了实现数据压缩和从文件中恢复数据,redis使用了很多编码技术,其中有些我不是很理解,但关键是理解思想,不要关心这些细节。

RDB 文件已保存。当redis重启时,数据库会根据RDB文件进行恢复。由于数据库编号、其包含的键值对以及每个键值对中值的具体类型、实现和数据都保存在RDB文件中,因此redis只需要顺序读取该文件,然后恢复对象就是这样。由于保存了过期时间,发现当前时间大于过期时间,即数据已超时,则不恢复该键值对。

保存RDB文件是一个浩大的工程,所以redislso提供了后台保存机制。即当执行bgsave时,redis会fork出一个子进程,让子进程执行保存的工作,而父进程则继续提供redis正常的数据库服务。由于子进程复制父进程的地址空间,即父进程fork时子进程拥有数据库,因此子进程执行保存操作,将从父进程继承的数据库写入临时文件。在子进程的复制过程中,redis会记录数据库的修改(脏)次数。当子进程完成时,SIGUSR1信号被发送到父进程。当父进程捕获到这个信号时,就知道子进程已经完成了复制。然后父进程将子进程保存的temp文件重命名为真正的rdb文件(即真正保存成功)。然后才改成目标文件,这才是安全的app蟑螂)。然后记录本次保存的结束时间。

这里有一个问题。子进程保存的过程中,父进程的数据库被修改了,而父进程只记录了修改次数(脏),并没有进行任何修正操作。看起来RDB保存的并不是实时数据库,有点朴实无华。不过后面要介绍的AOF持久化就解决了这个问题。

除了执行sava或bgsave命令外,客户还可以配置RDB保存条件。也就是在配置文件中进行配置。如果t时间内数据库被修改了脏次数,则会在后台保存。 redis在服务cron的时候,会根据脏项目的数量和最后一次保存的时间来判断是否满足条件。如果满足条件,就会执行bg save。注意,任何时候只能有一个子进程进行后台保存,因为e saving 是一个非常昂贵的IO操作。多个进程中的大量IO操作效率低且难以管理。

4.2 AOF持久化 首先思考一个问题。保存数据库是否需要像RDB一样保存数据库中的所有数据?还有其他办法吗?

RDB只保存最终的数据库,也就是一个结果。结果是怎么得来的?它是通过用户的各种命令建立的,因此不需要保存结果,而只保存创建结果的命令。 redis的AOF就有这个思想。与RDB不同的是,它保存的是数据库数据。它一一保存了创建数据库的命令。

我们先看一下AOF文件的格式。它一一存储命令。首先存储命令长度,然后存储命令。具体的分隔符大家可以自行深入研究。这不是重点。 ,不管怎样,只要知道AOF文件存储的是t执行的命令即可他是redis客户端。

redis服务器中有一个sds aof_buf。如果开启了aof持久化,那么每一条修改数据库的命令都会保存在这个aof_buf中(保存的是aof文件中命令格式的字符串),然后eventloop不循环一次,在server cron中调用flushaofbuf,写入将aof_buf中的命令写入aof文件(实际上是写入,实际写入的是内核缓冲区),然后清除aof_buf,进入下一个循环。这样就可以通过aof文件中的命令来恢复所有数据库的更改,达到保存数据库的效果。

需要注意的是,flushaofbuf中调用的write只是将数据写入内核缓冲区。文件的实际写入是由内核本身决定的,可能需要延迟一段时间。不过,redis支持配置。您可以在每次写入后配置同步。然后调用redis中的sync将内核中的数据写入到文件中。然而,这需要系统校准l 并且很耗时。仅此而已。还可以将策略配置为每秒同步一次,然后redis会启动一个后台线程(所以redis不是单个线程,只是单个eventloop),这个后台线程每秒都会调用sync。这里不得不问,为什么使用RDB时没有考虑sync呢?因为RDB是存储一次,不像AOF多次,所以RDB期间调用一次sync是没有效果的,而使用bg save时,子进程会自行退出(exit),此时在exit函数中会刷新缓冲区。区域,它会自动写入文件。

再看一遍,如果不想用aof_buf来保存每个修改命令,也可以用aof持久化。 Redis提供了aof_rewrite,它根据现有数据库生成命令,然后将命令写入aof文件中。很奇怪,对吧?是的,就是这么棒。执行aof_rewrite时,各个数据库使用redis变量,然后不同的c命令是根据键值对中特定类型的值生成的,例如列表,然后生成保存列表的命令,其中包含保存列表所需的信息。所需要的数据,如果列表数据太长,会被分成多个命令。首先创建列表,然后向列表添加元素。简而言之,保存数据的命令是根据数据逆向生成的。然后将这些命令存储在aof文件中。这样不就达到了和aofappend一样的效果了吗?

再进一步看,aof格式还支持后台模式。在执行aof_bgrewrite的时候,也会fork出一个子进程,然后让子进程执行aof_rewrite,将其复制的数据库写入到临时文件中,然后写入完毕后用新的编号通知父进程。父进程判断子进程的退出信息是否正确,然后将临时文件重命名为最终的aof文件。好吧,p来了问题。子进程持久化期间,父进程的数据库可能会被更新。如何通知子进程此更新?我们需要使用进程间通信吗?是不是有点麻烦?你认为redis 是做什么的?它根本不通知子进程。什么,没有通知?更新怎么样?子进程执行aof_bgrewrite过程中,父进程会将所有改变数据库的命令(添加、删除、修改等)保存在aof_rewrite_buf_blocks中。这是一个链接列表。每个块可以保存命令。当不够的时候,申请一个新的块,放到链表的末尾。当通知子进程保存完成后,父进程将aof_rewrite_buf_blocks命令追加到aof文件中。多么漂亮的设计啊,想想当我考虑使用进程间通信时,其他人已经用最简单的方法完美地解决了问题。有一个萨确实如此,设计越好,就越简单,而复杂的东西往往不可靠。

至于aof文件的加载,只是将aof文件中的命令一一执行即可。但是,考虑到这些命令是客户端发送给redis的命令,redis只是生成了一个假客户端。它不与redis建立网络连接,而是直接执行命令。首先要明确的是,这里的假客户端并不是真正的客户端,而是redis中存储的客户端信息,其中包含写缓冲区和读缓冲区,这些缓冲区存在于redis服务器中。因此,如下所示,直接读取aof命令,放入客户端的读缓冲区,然后执行客户端命令。这样就完成了aof文件的加载。

//创建假客户端 fakeClient = createFakeClient();while(命令不为空) { //获取a的参数信息argc,argv命令。 .. // 执行fakeC对象->argc = argc; fakeClient->argv = argv; cmd->proc(fakeClient);}

我个人觉得整个aof持久化设计还是蛮精彩的。值得膜拜的地方还有很多。

5。 Redis事务

redis比memcached更强大的另一个原因是它支持简单事务。简单来说,事务就是合并多个命令并一次性执行。对于关系数据库来说,事务还具有回滚机制,即如果所有事务命令执行成功,只要其中一个失败,就会回滚到事务执行前的状态。 Redis不支持回滚。它的事务仅确保命令按顺序执行。即使中间命令之一发生错误,也会继续执行,因此仅支持简单事务。

首先我们看一下redis事务的执行流程。首先执行multi命令启动transac然后输入要执行的命令,最后输入exec执行事务。 redis服务器收到multi命令后,会将对应客户端的状态设置为REDIS_MULTI,表示该客户端处于事务阶段,并在客户端的multiState结构体中维护该事务的具体命令信息(当然,会先检查命令是否可以识别),错误的命令不会保存),即命令的数量和具体命令。 redis收到exec命令后,会依次执行multiState中保存的命令,然后保存每个命令的返回值。当命令发生错误时,redis不会停止事务,而是保存错误信息,然后继续执行。当所有命令都执行完毕后,所有命令的返回值都会返回给客户端。为什么redis不支持回滚?我在网上看到的解释是该问题是由于客户端程序出现问题,所以不需要服务器回滚。同时不支持回滚,redis服务器运行效率高很多。在我看来,redis事务不是传统关系数据库中的事务,这对CIAD的要求非常严格。也就是说,redis 事务不是事务。它们只是为客户端提供了一种同时执行多个命令的方法。只需将事务视为普通命令即可,无需支持回滚。

我们知道redis是单个事件循环。当一个事物真正执行时(即redis接收到exec命令),事物的执行过程不会被中断,所有的命令都会在一个事件循环中执行。然而,当用户一一输入交易命令时,在此期间,其他客户可能修改了交易中使用的数据,这可能会导致问题。因此,redis还提供了watch命令。用户可以在进入multi之前执行watch命令并指定要观察的数据。这样,如果其他客户端在exec之前修改了观看的数据,那么在exec过程中,执行处理修改数据的命令时,就会执行失败,提示数据脏了。这是如何实现的?原来每个redisDb中都有一个dict Watched_keys。 Watched_kesy中的dictentry的key是被监视的数据库的key,value是一个列表,里面存储了监视它的客户端。同时每个客户端还有一个watched_keys,里面存储了客户端当前watch的key。执行watch时,redis会在相应数据库的watched_keys中查找key(如果没有,则创建一个新的dictentry),然后将客户端添加到其客户端列表中,同时将key添加到客户端的watched_keys中。当客户端执行修改数据的命令时,redis首先t 在 Watched_keys 中查找键。如果找到了,就证明有客户端在监视它,然后遍历所有监视它的csclient,将这些客户端设置为REDIS_DIRTY_CAS,表面上有watch的key都是脏的。当客户端执行事务时,会首先检查REDIS_DIRTY_CAS是否设置。如果是,则说明数据脏了,事务无法执行,立即返回错误。仅当客户端未设置 REDIS_DIRTY_CAS 时事务才能执行。需要指出的是,执行exec后,client的所有watch key都会被清除,db中该key的client列表也会被清除。也就是说,执行完 exec 后,客户端将不再监视任何键(即使 exec 没有成功执行也是如此)。因此,redis交易是简单交易,并不是真正的交易。

以上就是redis的事务。感觉实现起来非常简单nt并且在实践中不是很有用。

6。 Redis发布订阅频道

Redis支持频道,即用户加入频道就相当于加入了一个群组。客户发送到通道的信息,通道中所有信息的客户端都可以接收到。

实现也很简单,和watch_keys的实现类似。 redis 服务器中保存了 pubsub_channels 的字典。里面的key是通道的名称(显然必须是唯一的),value是一个链表。保存并添加该频道的客户端。同时,每个客户端都有一个pubsub_channels,保存了它所关注的频道。当用户向某个频道发送消息时,首先在服务器的 pubsub_channels 中找到发生变化的频道,然后遍历客户端并向其发送消息。订阅和取消订阅频道只是对pubsub_channels的操作,很容易理解。

同时,redis还支持rts 模式频道。即通过正则匹配通道,如果存在模式通道p,1,向普通通道p1发送消息时,会匹配p,1。除了向普通通道发送消息外,还会向 p,1 模式通道中的客户端发送消息。注意,这里我们使用release命令中的普通通道来匹配现有的模式通道,而不是在release命令中指定模式通道,然后匹配redis中保存的通道。实现也非常简单。 redis服务器中有一个pubsub_patterns的列表(这里为什么不使用dict?因为pubsub_patterns的数量一般都比较少,没有必要使用dict,一个简单的列表就可以了),里面存储的是pubsubPattern结构体,里面包含模式和客户端信息,如下图,一种模式一个客户端,所以如果有多个客户端监听一个pubsub_patterns,就会有多个pubsubPatterns列表中,保存了client和pubsub_patterns的对应关系。同时,客户端中也有一个pubsub_patterns列表,但里面存储的是它监听的pubsub_patterns列表(即sds),而不是pubsubPattern结构体。

typedef struct pubsubPattern { redisClient *client; // 监控客户端 robj *pattern; // Pattern} pubsubPattern;

当用户向某个频道发送消息时,首先会在redis服务器中的pubsub_channels中搜索该频道,然后将消息发送到其客户列表中。然后在redis服务器中的pubsub_patterns中搜索匹配的模式,然后将消息发送给客户端。此处不会删除重复的客户。一条消息可能已经在 pubsub_channels 中发送给客户端,然后可能会在 pubsub_patterns 中再次(甚至多次)发送给用户。估计redis认为这是客户端程序的问题本身,所以它不处理它。

/* 发布消息 */int pubsubPublishMessage(robj *channel, robj *message) { int receiveers = 0; dictEntry *de;列表节点 *ln; listIter li;/* 发送给监听该通道的客户端 */ de = dictFind(server.pubsub_channels,channel); if (de) { 列表 *list = dictGetVal(de);列表节点 *ln;列表Iter li; listRewind(列表,&li); while ((ln = listNext(&li)) != NULL) { redisClient *c = ln->value; addReply(c,shared.mbulkhdr[3]) ; addReply(c,shared.messagebulk); addReplyBulk(c,通道); addReplyBulk(c,消息);接收器++; } } /* 发送到监听匹配通道的客户端 */ if (listLength(server.pubsub_patterns)) { listRewind (server.pubsub_patterns,&li);通道 = getDecodedObject(通道); while ((ln = listNext(&li)) != NULL) { pubsubPattern *pat = ln->value; if (stringmatchlen((char*)pat->pattern->ptr, sdslen(pat->pattern->ptr), (char*)channel->ptr, sdslen(channel->ptr),0)) { addReply(帕特->客户端,共享.mbulk人类发展报告[4]); addReply(pat->client,shared.pmessagebulk); addReplyBulk(帕特->客户端,帕特->模式); addReplyBulk(pat->客户端,频道); addReplyBulk(pat->客户端,消息);接收器++; decrRefCount(通道);返回接收者; }

6.总结

总的来说,redis的功能比memcached多很多,实现也更复杂。不过memcached更专注于保存key-value数据(已经可以满足大部分使用场景),而redis则提供了更丰富的数据结构等功能。不能说redis比memcached好,但是从源码阅读的角度来看,redis可能更有价值。另外,redis3.0支持集群功能。这部分代码还没有研究过,稍后跟进。

读完这篇文章,相信您对Redis和Memcached的区别有了一定的了解。如果您想了解更多相关知识,请关注进入行业信息频道。感谢您的阅读。 !

1. 本站所有资源来源于用户上传或网络,仅作为参考研究使用,如有侵权请邮件联系站长!
2. 本站积分货币获取途径以及用途的解读,想在本站混的好,请务必认真阅读!
3. 本站强烈打击盗版/破解等有损他人权益和违法作为,请各位会员支持正版!
4. 编程技术 > Redis 和 Memcached 有什么区别?

用户评论