2024年大数据最新两大数据库缓存系统实现对比_两大缓存(1),2024-2024历年字节跳动大数据开发面试真题解析

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

                                            // 可 以采用不同的存储方式
unsigned lru:REDIS_LRU_BITS; /\* lru time (relative to server.lruclock) \*/
int refcount;         // 引用计数
void *ptr;

} robj;


说到底redis还是一个key-value的数据库,不管它支持多少种数据结构,最终存储的还是以key-value的方式,只不过value可以是链表,set,sorted set,hash table等。和memcached一样,所有的key都是string,而set,sorted set,hash table等具体存储的时候也用到了string。 而c没有现成的string,所以redis的首要任务就是实现一个string,取名叫sds(simple dynamic string),如下的代码, 非常简单的一个结构体,len存储改string的内存总长度,free表示还有多少字节没有使用,而buf存储具体的数据,显然len-free就是目前字符串的长度。



struct sdshdr {
int len;
int free;
char buf[];
};


字符串解决了,所有的key都存成sds就行了,那么key和value怎么关联呢?key-value的格式在脚本语言中很好处理,直接使用字典即可,C没有字典,怎么办呢?自己写一个呗(redis十分热衷于造轮子)。看下面的代码,privdata存额外信息,用的很少,至少我们发现。 dictht是具体的哈希表,一个dict对应两张哈希表,这是为了扩容(包括rehashidx也是为了扩容)。dictType存储了哈希表的属性。redis还为dict实现了迭代器(所以说看起来像c++代码)。


哈希表的具体实现是和mc类似的做法,也是使用开链法来解决冲突,不过里面用到了一些小技巧。比如使用dictType存储函数指针,可以动态配置桶里面元素的操作方法。又比如dictht中保存的sizemask取size(桶的数量)-1,用它与key做&操作来代替取余运算,加快速度等等。总的来看,dict里面有两个哈希表,每个哈希表的桶里面存储dictEntry链表,dictEntry存储具体的key和value。


前面说过,一个dict对于两个dictht,是为了扩容(其实还有缩容)。正常的时候,dict只使用dictht[0],当dict[0]中已有entry的数量与桶的数量达到一定的比例后,就会触发扩容和缩容操作,我们统称为rehash,这时,为dictht[1]申请rehash后的大小的内存,然后把dictht[0]里的数据往dictht[1]里面移动,并用rehashidx记录当前已经移动万的桶的数量,当所有桶都移完后,rehash完成,这时将dictht[1]变成dictht[0], 将原来的dictht[0]变成dictht[1],并变为null即可。不同于memcached,这里不用开一个后台线程来做,而是就在event loop中完成,并且rehash不是一次性完成,而是分成多次,每次用户操作dict之前,redis移动一个桶的数据,直到rehash完成。这样就把移动分成多个小移动完成,把rehash的时间开销均分到用户每个操作上,这样避免了用户一个请求导致rehash的时候,需要等待很长时间,直到rehash完成才有返回的情况。不过在rehash期间,每个操作都变慢了点,而且用户还不知道redis在他的请求中间添加了移动数据的操作,感觉redis太贱了 :-D



typedef struct dict {
dictType *type; // 哈希表的相关属性
void *privdata; // 额外信息
dictht ht[2]; // 两张哈希表,分主和副,用于扩容
int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 记录当前数据迁移的位置,在扩容的时候用的
int iterators; /* number of iterators currently running */ // 目前存在的迭代器的数量
} dict;

typedef struct dictht {
dictEntry **table; // dictEntry是item,多个item组成hash桶里面的链表,table则是多个链表头指针组成的数组的指针
unsigned long size; // 这个就是桶的数量
// sizemask取size - 1, 然后一个数据来的时候,通过计算出的hashkey, 让hashkey & sizemask来确定它要放的桶的位置
// 当size取2^n的时候,sizemask就是1…111,这样就和hashkey % size有一样的效果,但是使用&会快很多。这就是原因
unsigned long sizemask;
unsigned long used; // 已经数值的dictEntry数量
} dictht;

typedef struct dictType {
unsigned int (*hashFunction)(const void *key); // hash的方法
void *(*keyDup)(void *privdata, const void *key); // key的复制方法
void *(*valDup)(void *privdata, const void *obj); // value的复制方法
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // key之间的比较
void (*keyDestructor)(void *privdata, void *key); // key的析构
void (*valDestructor)(void *privdata, void *obj); // value的析构
} dictType;

typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next;
} dictEntry;


有了dict,数据库就好实现了。所有数据读存储在dict中,key存储成dictEntry中的key(string),用void\* 指向一个redis object,它可以是5种类型中的任何一种。如下图,结构构造是这样,不过这个图已经过时了,有一些与redis3.0不符合的地方。  
 ![两大数据库缓存系统实现对比两大数据库缓存系统实现对比](https://mc.qcloudimg.com/static/img/96c9ca9e34f06930b8a38982e1a9a442/image.jpg "两大数据库缓存系统实现对比两大数据库缓存系统实现对比")


5中type的对象,每一个都至少有两种底层实现方式。string有3种:REDIS\_ENCODING\_RAW, REDIS\_ENCIDING\_INT, REDIS\_ENCODING\_EMBSTR, list有:普通双向链表和压缩链表,压缩链表简单的说,就是讲数组改造成链表,连续的空间,然后通过存储字符串的大小信息来模拟链表,相对普通链表来说可以节省空间,不过有副作用,由于是连续的空间,所以改变内存大小的时候,需要重新分配,并且由于保存了字符串的字节大小,所有有可能引起连续更新(具体实现请详细看代码)。set有dict和intset(全是整数的时候使用它来存储), sorted set有:skiplist和ziplist, hashtable实现有压缩列表和dict和ziplist。skiplist就是跳表,它有接近于红黑树的效率,但是实现起来比红黑树简单很多,所以被采用(奇怪,这里又不造轮子了,难道因为这个轮子有点难?)。 


hash table可以使用dict实现,则改dict中,每个dictentry中key保存了key(这是哈希表中的键值对的key),而value则保存了value,它们都是string。 而set中的dict,每个dictentry中key保存了set中具体的一个元素的值,value则为null。图中的zset(有序集合)有误,zset使用skiplist和ziplist实现,首先skiplist很好理解,就把它当做红黑树的替代品就行,和红黑树一样,它也可以排序。


怎么用ziplist存储zset呢?首先在zset中,每个set中的元素都有一个分值score,用它来排序。所以在ziplist中,按照分值大小,先存元素,再存它的score,再存下一个元素,然后score。这样连续存储,所以插入或者删除的时候,都需要重新分配内存。所以当元素超过一定数量,或者某个元素的字符数超过一定数量,redis就会选择使用skiplist来实现zset(如果当前使用的是ziplist,会将这个ziplist中的数据取出,存入一个新的skiplist,然后删除改ziplist,这就是底层实现转换,其余类型的redis object也是可以转换的)。 


另外,ziplist如何实现hashtable呢?其实也很简单,就是存储一个key,存储一个value,再存储一个key,再存储一个value。还是顺序存储,与zset实现类似,所以当元素超过一定数量,或者某个元素的字符数超过一定数量时,就会转换成hashtable来实现。各种底层实现方式是可以转换的,redis可以根据情况选择最合适的实现方式,这也是这样使用类似面向对象的实现方式的好处。


需要指出的是,使用skiplist来实现zset的时候,其实还用了一个dict,这个dict存储一样的键值对。为什么呢?因为skiplist的查找只是lgn的(可能变成n),而dict可以到O(1), 所以使用一个dict来加速查找,由于skiplist和dict可以指向同一个redis object,所以不会浪费太多内存。另外使用ziplist实现zset的时候,为什么不用dict来加速查找呢?因为ziplist支持的元素个数很少(个数多时就转换成skiplist了),顺序遍历也很快,所以不用dict了。


这样看来,上面的dict,dictType,dictHt,dictEntry,redis object都是很有考量的,它们配合实现了一个具有面向对象色彩的灵活、高效数据库。不得不说,redis数据库的设计还是很厉害的。


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


redis也支持expire time的设置,我们看上面的redis object,里面没有保存expire的字段,那redis怎么记录数据的expire time呢? redis是为每个数据库又增加了一个dict,这个dict叫expire dict,它里面的dict entry里面的key就是数对的key,而value全是数据为64位int的redis object,这个int就是expire time。这样,判断一个key是否过期的时候,去expire dict里面找到它,取出expire time比对当前时间即可。为什么这样做呢? 因为并不是所有的key都会设置过期时间,所以,对于不设置expire time的key来说,保存一个expire time会浪费空间,而是用expire dict来单独保存的话,可以根据需要灵活使用内存(检测到key过期时,会把它从expire dict中删除)。


redis的expire 机制是怎样的呢? 与memcahed类似,redis也是惰性删除,即要用到数据时,先检查key是否过期,过期则删除,然后返回错误。单纯的靠惰性删除,上面说过可能会导致内存浪费,所以redis也有补充方案,redis里面有个定时执行的函数,叫servercron,它是维护服务器的函数,在它里面,会对过期数据进行删除,注意不是全删,而是在一定的时间内,对每个数据库的expire dict里面的数据随机选取出来,如果过期,则删除,否则再选,直到规定的时间到。即随机选取过期的数据删除,这个操作的时间分两种,一种较长,一种较短,一般执行短时间的删除,每隔一定的时间,执行一次长时间的删除。这样可以有效的缓解光采用惰性删除而导致的内存浪费问题。


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



**3.redis数据库持久化**

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


**3.1 RDB持久化**  
 用户执行save或者bgsave的时候,就会触发RDB持久化操作。RDB持久化操作的核心思想就是把数据库原封不动的保存在文件里。


那如何存储呢?如下图, 首先存储一个REDIS字符串,起到验证的作用,表示是RDB文件,然后保存redis的版本信息,然后是具体的数据库,然后存储结束符EOF,最后用检验和。关键就是databases,看它的名字也知道,它存储了多个数据库,数据库按照编号顺序存储,0号数据库存储完了,才轮到1,然后是2, 一直到最后一个数据库。  
 ![两大数据库缓存系统实现对比两大数据库缓存系统实现对比](https://mc.qcloudimg.com/static/img/b19b6afaec790d33f67f0deba8049df6/image.jpg "两大数据库缓存系统实现对比两大数据库缓存系统实现对比")


每一个数据库存储方式如下,首先一个1字节的常量SELECTDB,表示切换db了,然后下一个接上数据库的编号,它的长度是可变的,然后接下来就是具体的key-value对的数据了。  
 ![两大数据库缓存系统实现对比两大数据库缓存系统实现对比](https://mc.qcloudimg.com/static/img/5610079e5e20a5e1cd051d8788a7ec8c/image.jpg "两大数据库缓存系统实现对比两大数据库缓存系统实现对比")



int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
/* Save the expire time */
if (expiretime != -1) {
/* If this key is already expired skip it */
if (expiretime < now) return 0;
if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
}

/\* Save type, key, value \*/
if (rdbSaveObjectType(rdb,val) == -1) return -1;
if (rdbSaveStringObject(rdb,key) == -1) return -1;
if (rdbSaveObject(rdb,val) == -1) return -1;
return 1;

}


由上面的代码也可以看出,存储的时候,先检查expire time,如果已经过期,不存就行了,否则,则将expire time存下来,注意,及时是存储expire time,也是先存储它的类型为REDIS\_RDB\_OPCODE\_EXPIRETIME\_MS,然后再存储具体过期时间。接下来存储真正的key-value对,首先存储value的类型,然后存储key(它按照字符串存储),然后存储value,如下图。  
 ![两大数据库缓存系统实现对比两大数据库缓存系统实现对比](https://mc.qcloudimg.com/static/img/01767f4398042842eae7a962ad6b7a3f/image.jpg "两大数据库缓存系统实现对比两大数据库缓存系统实现对比")


在rdbsaveobject中,会根据val的不同类型,按照不同的方式存储,不过从根本上来看,最终都是转换成字符串存储,比如val是一个linklist,那么先存储整个list的字节数,然后遍历这个list,把数据取出来,依次按照string写入文件。对于hash table,也是先计算字节数,然后依次取出hash table中的dictEntry,按照string的方式存储它的key和value,然后存储下一个dictEntry。 总之,RDB的存储方式,对一个key-value对,会先存储expire time(如果有的话),然后是value的类型,然后存储key(字符串方式),然后根据value的类型和底层实现方式,将value转换成字符串存储。这里面为了实现数据压缩,以及能够根据文件恢复数据,redis使用了很多编码的技巧,有些我也没太看懂,不过关键还是要理解思想,不要在意这些细节。


保存了RDB文件,当redis再启动的时候,就根据RDB文件来恢复数据库。由于以及在RDB文件中保存了数据库的号码,以及它包含的key-value对,以及每个key-value对中value的具体类型,实现方式,和数据,redis只要顺序读取文件,然后恢复object即可。由于保存了expire time,发现当前的时间已经比expire time大了,即数据已经超时了,则不恢复这个key-value对即可。


保存RDB文件是一个很巨大的工程,所以redis还提供后台保存的机制。即执行bgsave的时候,redis fork出一个子进程,让子进程来执行保存的工作,而父进程继续提供redis正常的数据库服务。由于子进程复制了父进程的地址空间,即子进程拥有父进程fork时的数据库,子进程执行save的操作,把它从父进程那儿继承来的数据库写入一个temp文件即可。在子进程复制期间,redis会记录数据库的修改次数(dirty)。当子进程完成时,发送给父进程SIGUSR1信号,父进程捕捉到这个信号,就知道子进程完成了复制,然后父进程将子进程保存的temp文件改名为真正的rdb文件(即真正保存成功了才改成目标文件,这才是保险的做法)。然后记录下这一次save的结束时间。


这里有一个问题,在子进程保存期间,父进程的数据库已经被修改了,而父进程只是记录了修改的次数(dirty),被没有进行修正操作。似乎使得RDB保存的不是实时的数据库,有点不太高大上的样子。 不过后面要介绍的AOF持久化,就解决了这个问题。


除了客户执行sava或者bgsave命令,还可以配置RDB保存条件。即在配置文件中配置,在t时间内,数据库被修改了dirty次,则进行后台保存。redis在serve cron的时候,会根据dirty数目和上次保存的时间,来判断是否符合条件,符合条件的话,就进行bg save,注意,任意时刻只能有一个子进程来进行后台保存,因为保存是个很费io的操作,多个进程大量io效率不行,而且不好管理。


**3.2 AOF持久化**  
 首先想一个问题,保存数据库一定需要像RDB那样把数据库里面的所有数据保存下来么?有没有别的方法?


RDB保存的只是最终的数据库,它是一个结果。结果是怎么来的?是通过用户的各个命令建立起来的,所以可以不保存结果,而只保存建立这个结果的命令。 redis的AOF就是这个思想,它不同RDB保存db的数据,它保存的是一条一条建立数据库的命令。


我们首先来看AOF文件的格式,它里面保存的是一条一条的命令,首先存储命令长度,然后存储命令,具体的分隔符什么的可以自己深入研究,这都不是重点,反正知道AOF文件存储的是redis客户端执行的命令即可。


redis server中有一个sds aof\_buf, 如果aof持久化打开的话,每个修改数据库的命令都会存入这个aof\_buf(保存的是aof文件中命令格式的字符串),然后event loop没循环一次,在server cron中调用flushaofbuf,把aof\_buf中的命令写入aof文件(其实是write,真正写入的是内核缓冲区),再清空aof\_buf,进入下一次loop。这样所有的数据库的变化,都可以通过aof文件中的命令来还原,达到了保存数据库的效果。


需要注意的是,flushaofbuf中调用的write,它只是把数据写入了内核缓冲区,真正写入文件时内核自己决定的,可能需要延后一段时间。 不过redis支持配置,可以配置每次写入后sync,则在redis里面调用sync,将内核中的数据写入文件,这不过这要耗费一次系统调用,耗费时间而已。还可以配置策略为1秒钟sync一次,则redis会开启一个后台线程(所以说redis不是单线程,只是单eventloop而已),这个后台线程会每一秒调用一次sync。这里要问了,RDB的时候为什么没有考虑sync的事情呢?因为RDB是一次性存储的,不像AOF这样多次存储,RDB的时候调用一次sync也没什么影响,而且使用bg save的时候,子进程会自己退出(exit),这时候exit函数内会冲刷缓冲区,自动就写入了文件中。


再来看,如果不想使用aof\_buf保存每次的修改命令,也可以使用aof持久化。redis提供aof\_rewrite,即根据现有的数据库生成命令,然后把命令写入aof文件中。很奇特吧?对,就是这么厉害。进行aof\_rewrite的时候,redis变量每个数据库,然后根据key-value对中value的具体类型,生成不同的命令,比如是list,则它生成一个保存list的命令,这个命令里包含了保存该list所需要的的数据,如果这个list数据过长,还会分成多条命令,先创建这个list,然后往list里面添加元素,总之,就是根据数据反向生成保存数据的命令。然后将这些命令存储aof文件,这样不就和aof append达到同样的效果了么?


再来看,aof格式也支持后台模式。执行aof\_bgrewrite的时候,也是fork一个子进程,然后让子进程进行aof\_rewrite,把它复制的数据库写入一个临时文件,然后写完后用新号通知父进程。父进程判断子进程的退出信息是否正确,然后将临时文件更名成最终的aof文件。好了,问题来了。在子进程持久化期间,可能父进程的数据库有更新,怎么把这个更新通知子进程呢?难道要用进程间通信么?是不是有点麻烦呢?你猜redis怎么做的?它根本不通知子进程。


什么,不通知?那更新怎么办? 在子进程执行aof\_bgrewrite期间,父进程会保存所有对数据库有更改的操作的命令(增,删除,改等),把他们保存在aof\_rewrite\_buf\_blocks中,这是一个链表,每个block都可以保存命令,存不下时,新申请block,然后放入链表后面即可,当子进程通知完成保存后,父进程将aof\_rewrite\_buf\_blocks的命令append 进aof文件就可以了。多么优美的设计,想一想自己当初还考虑用进程间通信,别人直接用最简单的方法就完美的解决了问题,有句话说得真对,越优秀的设计越趋于简单,而复杂的东西往往都是靠不住的。


至于aof文件的载入,也就是一条一条的执行aof文件里面的命令而已。不过考虑到这些命令就是客户端发送给redis的命令,所以redis干脆生成了一个假的客户端,它没有和redis建立网络连接,而是直接执行命令即可。首先搞清楚,这里的假的客户端,并不是真正的客户端,而是存储在redis里面的客户端的信息,里面有写和读的缓冲区,它是存在于redis服务器中的。所以,如下图,直接读入aof的命令,放入客户端的读缓冲区中,然后执行这个客户端的命令即可。这样就完成了aof文件的载入。



// 创建伪客户端
fakeClient = createFakeClient();

while(命令不为空) {
// 获取一条命令的参数信息 argc, argv

// 执行
fakeClient->argc = argc;
fakeClient->argv = argv;
cmd->proc(fakeClient);

}


整个aof持久化的设计,个人认为相当精彩。其中有很多地方,值得膜拜。



**4. redis的事务**

redis另一个比memcached强大的地方,是它支持简单的事务。事务简单说就是把几个命令合并,一次性执行全部命令。对于关系型数据库来说,事务还有回滚机制,即事务命令要么全部执行成功,只要有一条失败就回滚,回到事务执行前的状态。redis不支持回滚,它的事务只保证命令依次被执行,即使中间一条命令出错也会继续往下执行,所以说它只支持简单的事务。


首先看redis事务的执行过程。首先执行multi命令,表示开始事务,然后输入需要执行的命令,最后输入exec执行事务。 redis服务器收到multi命令后,会将对应的client的状态设置为REDIS\_MULTI,表示client处于事务阶段,并在client的multiState结构体里面保持事务的命令具体信息(当然首先也会检查命令是否能否识别,错误的命令不会保存),即命令的个数和具体的各个命令,当收到exec命令后,redis会顺序执行multiState里面保存的命令,然后保存每个命令的返回值,当有命令发生错误的时候,redis不会停止事务,而是保存错误信息,然后继续往下执行,当所有的命令都执行完后,将所有命令的返回值一起返回给客户。


redis为什么不支持回滚呢?网上看到的解释出现问题是由于客户程序的问题,所以没必要服务器回滚,同时,不支持回滚,redis服务器的运行高效很多。在我看来,redis的事务不是传统关系型数据库的事务,要求CIAD那么非常严格,或者说redis的事务都不是事务,只是提供了一种方式,使得客户端可以一次性执行多条命令而已,就把事务当做普通命令就行了,支持回滚也就没必要了。  
 ![两大数据库缓存系统实现对比两大数据库缓存系统实现对比](https://mc.qcloudimg.com/static/img/ce7e318f918cec4bad55b3137db4dabd/image.png "两大数据库缓存系统实现对比两大数据库缓存系统实现对比")


我们知道redis是单event loop的,在真正执行一个事物的时候(即redis收到exec命令后),事物的执行过程是不会被打断的,所有命令都会在一个event loop中执行完。但是在用户逐个输入事务的命令的时候,这期间,可能已经有别的客户修改了事务里面用到的数据,这就可能产生问题。


所以redis还提供了watch命令,用户可以在输入multi之前,执行watch命令,指定需要观察的数据,这样如果在exec之前,有其他的客户端修改了这些被watch的数据,则exec的时候,执行到处理被修改的数据的命令的时候,会执行失败,提示数据已经dirty。 这是如何是实现的呢? 原来在每一个redisDb中还有一个dict watched\_keys,watched\_kesy中dictentry的key是被watch的数据库的key,而value则是一个list,里面存储的是watch它的client。


同时,每个client也有一个watched\_keys,里面保存的是这个client当前watch的key。在执行watch的时候,redis在对应的数据库的watched\_keys中找到这个key(如果没有,则新建一个dictentry),然后在它的客户列表中加入这个client,同时,往这个client的watched\_keys中加入这个key。当有客户执行一个命令修改数据的时候,redis首先在watched\_keys中找这个key,如果发现有它,证明有client在watch它,则遍历所有watch它的client,将这些client设置为REDIS\_DIRTY\_CAS,表面有watch的key被dirty了。


当客户执行的事务的时候,首先会检查是否被设置了REDIS\_DIRTY\_CAS,如果是,则表明数据dirty了,事务无法执行,会立即返回错误,只有client没有被设置REDIS\_DIRTY\_CAS的时候才能够执行事务。 需要指出的是,执行exec后,该client的所有watch的key都会被清除,同时db中该key的client列表也会清除该client,即执行exec后,该client不再watch任何key(即使exec没有执行成功也是一样)。所以说redis的事务是简单的事务,算不上真正的事务。


以上就是redis的事务,感觉实现很简单,实际用处也不是太大。



**5. redis的发布订阅频道**

redis支持频道,即加入一个频道的用户相当于加入了一个群,客户往频道里面发的信息,频道里的所有client都能收到。


实现也很简单,也watch\_keys实现差不多,redis server中保存了一个pubsub\_channels的dict,里面的key是频道的名称(显然要唯一了),value则是一个链表,保存加入了该频道的client。同时,每个client都有一个pubsub\_channels,保存了自己关注的频道。当用用户往频道发消息的时候,首先在server中的pubsub\_channels找到改频道,然后遍历client,给他们发消息。而订阅,取消订阅频道不够都是操作pubsub\_channels而已,很好理解。


同时,redis还支持模式频道。即通过正则匹配频道,如有模式频道p*,* 1, 则向普通频道p1发送消息时,会匹配p*,*1,除了往普通频道发消息外,还会往p*,*1模式频道中的client发消息。注意,这里是用发布命令里面的普通频道来匹配已有的模式频道,而不是在发布命令里制定模式频道,然后匹配redis里面保存的频道。


实现方式也很简单,在redis server里面有个pubsub\_patterns的list(这里为什么不用dict?因为pubsub\_patterns的个数一般较少,不需要使用dict,简单的list就好了),它里面存储的是pubsubPattern结构体,里面是模式和client信息,如下所示,一个模式,一个client,所以如果有多个clint监听一个pubsub\_patterns的话,在list面会有多个pubsubPattern,保存client和pubsub\_patterns的对应关系。 同时,在client里面,也有一个pubsub\_patterns list,不过里面存储的就是它监听的pubsub\_patterns的列表(就是sds),而不是pubsubPattern结构体。



typedef struct pubsubPattern {
redisClient *client; // 监听的client
robj *pattern; // 模式
} pubsubPattern;


当用户往一个频道发送消息的时候,首先会在redis server中的pubsub\_channels里面查找该频道,然后往它的客户列表发送消息。然后在redis server里面的pubsub\_patterns里面查找匹配的模式,然后往client里面发送消息。 这里并没有去除重复的客户,在pubsub\_channels可能已经给某一个client发过message了,然后在pubsub\_patterns中可能还会给用户再发一次(甚至更多次)。 估计redis认为这是客户程序自己的问题,所以不处理。



/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
dictEntry *de;
listNode *ln;
listIter li;

/* Send to clients listening for that channel */
de = dictFind(server.pubsub_channels,channel);
if (de) {
list *list = dictGetVal(de);
listNode *ln;
listIter li;

    listRewind(list,&li);
    while ((ln = listNext(&li)) != NULL) {
        redisClient *c = ln->value;

        addReply(c,shared.mbulkhdr[3]);
        addReply(c,shared.messagebulk);
        addReplyBulk(c,channel);
        addReplyBulk(c,message);
        receivers++;
    }
}

/* Send to clients listening to matching channels */
if (listLength(server.pubsub_patterns)) {
listRewind(server.pubsub_patterns,&li);
channel = getDecodedObject(channel);
while ((ln = listNext(&li)) != NULL) {

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

图片转存中…(img-y9pmG8il-1715618242587)]
[外链图片转存中…(img-ais9q1PZ-1715618242587)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 27
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值