PART1-1:为什么Redis是单线程的
网络IO和键值对读写是由一个线程来完成的
。这也是 Redis 对外提供键值存储服务的主要流程。Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实是由 其他的额外的
线程执行。Redis是单线程,通常是指在Redis 6.0之前
,其核心网络模型使用的是单线程(Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型))
高性能网络模式:Reactor 和 Proactor
【都是一种基于事件分发的网络编程模式,区别在于 Reactor 模式是基于待完成的 I/O 事件,而 Proactor 模式则是基于已完成的 I/O 事件。】
那么最直接的方式就是为每一条连接创建线程或者进程
【进程和线程的区别在于线程比较轻量级些,线程的创建和线程间切换的成本要小些】。处理完业务逻辑后,随着连接关闭后线程也同样要销毁了,但是这样不停地创建和销毁线程,不仅会带来性能开销,也会造成浪费资源,而且如果要连接几万条连接,创建几万个线程去应对也是不现实的。
创建一个线程池,将连接分配给线程,然后一个线程可以处理多个连接的业务
。有了池子之后不用频繁创建销毁了
,但是一个线程要处理多个连接的业务,线程在处理某个连接的 read 操作时,如果遇到没有数据可读,就会发生阻塞,那么线程就没办法继续处理其他连接的业务。
socket 默认情况是阻塞 I/O
),不过这种阻塞方式并不影响其他线程,更慢,所以为了防止线程并不知道当前连接是否有数据可读,从而需要每次通过 read 去试探从而有可能阻塞这种问题,我们使用I/O多路复用来实现只有当连接上有数据的时候,线程才去发起读请求
,其实就是 select/poll/epoll,内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。然后用一个系统调用函数来监听我们所有关心的连接,也就说可以在一个监控线程里面监控很多的连接。但是用 I/O 多路复用接口写网络程序开发效率不高,我觉得跟用汇编写应用层代码代码相比与java一样那种感觉。于是大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写
。这种封装后的模式叫做Reactor 模式,Reactor 模式也叫 Dispatcher 模式,指的是来了一个事件,Reactor 就有相对应的反应/响应,那不还是人家I/O多路复用三个哥们的工作原理嘛【 I/O 多路复用监听事件,收到事件后,根据事件类型将事件分配(Dispatch)给某个进程 / 线程去处理。】
】
//下面的方法不同版本的redis在src目录下的ae_epoll.c、ae_evport.c、ae_kqueue.c、ae_select.c代码文件中都有实现
static int aeApiCreate(aeEventLoop *eventLoop)
static int aeApiResize(aeEventLoop *eventLoop, int setsize)
static void aeApiFree(aeEventLoop *eventLoop)
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
监听和分发事件
,事件类型包含连接事件、读写事件;
Java 语言一般使用线程,比如 Nett
y;C 语言使用进程和线程都可以,例如 Nginx 使用的是进程,Memcache 使用的是线程】
Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型
】线程会被阻塞,一直等到内核数据准备好
,并把数据从内核缓冲区拷贝到应用程序的缓冲区中
,当拷贝过程完成,read 才会返回。阻塞等待的是内核数据准备好和数据从内核态拷贝到用户态这两个过程【如果 socket 设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问,而不做任何设置的话,默认是阻塞 I/O。无论 read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用。因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间
。】求在数据未准备好的情况下立即返回,可以继续往下执行
,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区
,read 调用才可以获取到结果非阻塞同步网络模式
【 来了事件操作系统通知应用进程,让应用进程来处理,快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快
】,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据
。
异步网络模式
, 【来了事件操作系统来处理,处理完再通知应用进程,快递员直接将快递送到你家门口,然后通知你
】 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做
,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。采用多线程,需要考虑多线程的系统设计问题以及系统代码的对共享资源的并发访问等问题,比较麻烦。在保证足够的性能表现之下,使用单线程可以保持代码的简单和可维护性。所以基于以上,Redis直接采用单线程的模式
。Redis 6.0(在网络模型中实现多线程 I/O )
。(Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上)【国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了】
Redis6.0引入多线程I/O,只是用来处理网络数据的读写和协议的解析(可以充分利用服务器 CPU 资源,目前主线程只能利用一个核+多线程任务可以分摊 Redis 同步 IO 读写负荷),而执行命令依旧是单线程
互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量
,要提升 Redis的性能有两个方向:
可以充分利用服务器 CPU 资源,目前主线程只能利用一个核。多线程任务可以分摊 Redis 同步 IO 读写负荷
PART1-2:为什么Redis是单线程的速度还快:Redis 为什么这么快?
所有操作都在内存上进行之外
避免了上下文的切换
Redis采用
IO 多路复用机制,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件
,不在网络I/O上浪费过多的时间。使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)
**。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现
。PART1-3-1:事件
事件的调度和执行由ae.c/aeProcessEvents函数负责,在实际中,处理已产生 文件事件的代码是直接写在aeProcessEvents函数里面的
因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些
。而文件事件就是服务器对套接字操作的抽象
。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
就会产生一个文件事件
。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。
给定的时间点执行
,而时间事件就是服务器对这类定时操作的抽象
。
一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值
:
如果事件处理器返回ae.h/AE_NOMORE,那么这个事件为定时事件
:该事件在达到一次之后就会被删除,之后不再到达。如果事件处理器返回一个非AE_NOMORE的整数值,那么这个事件为周期性时间
:当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之 后再次到达,并以这种方式一直更新并运行下去。比如说,如果一个时间事件的处理器返回整数值30,那么服务器应该对这个时间事件进行更 新,让这个事件在30毫秒之后再次到达。无序链表
中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器
。无序链表
,指的不是链表不按 ID排序,而是说,该链表不按when属性的大小排序。正因为链表没有按when属性进行排序
,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理
。所以使用无序链表来保存时间事件,并不影响事件执行的性能
。确保服务器可以长期、稳定地运行
,这些定期对自身的资源和状态进行检查和调整操作由redis.c/serverCron函数负责执行
(服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件)
PART1-3-2:Redis线程模型
也就是说文件事件处理器是基于Reactor模式实现的网络通信程序
。。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。原理是文件事件处理器采用IO多路复用机制同时监听多个 Socket
,根据 socket 上的事件来选择对应的事件处理器来处理这个事件
。通常设置一个主线程负责做 event-loop 事件循环和 I/O 读写,通过 select/poll/epoll_wait 等系统调用监听 I/O 事件,业务逻辑提交给其他工作线程去做
。让一个线程能服务于多个 sockets
。对Redis单线程Reactor模型的分析,我们知道 Redis的I/O线程除了在等待事件,其它的事件都是非阻塞的,没有浪费任何的CPU时间,这也是Redis能够提供高性能服务的原因
。文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接
,这保持了Redis内部单线程设计的简单性。
为套接字关联不同的事件处理器
。
将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生
。 I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。ae.c/aeCreateFileEvent函数接受一个套接字描述符、一个事件类型,以及一个事件处理器作为参数
,将给定套接字的给定事件加入到 I/O多路复用程序的监听范围之内,并对事件和事件处理器进行关联。Redis的I/O多路复用程序的所有功能都是通过包装常见的select、 epoll、evport和kqueue这些I/O多路复用函数库来实现的
,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、 ae_epoll.c、ae_kqueue.c,诸如此类。多个套接字的ae.h/AE_READABLE事件
和ae.h/AE_WRITABLE事件
,
同时监听套接字的AE_READABLE事件和AE_WRITABLE事件
,如果一个套接字同时产生了这两种事件,那 么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完之后,才处理AE_WRITABLE事件。(也就是说如果一个套接字又可读又可写的话,那么服务器将先读套接字,后写套接字。)程序会在编译时自动选择系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现
/* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
# ifdef HAVE_EVPORT
# include "ae_evport.c"
# else
# ifdef HAVE_EPOLL
# include "ae_epoll.c"
# else
# ifdef HAVE_KQUEUE
# include "ae_kqueue.c"
# else
# include "ae_select.c"
# endif
# endif
# endif
负责监听多个套接字
,并向文件事件分派器传送那些产生了事件的套接字将 socket 关联到相应的事件处理器
):文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接 字产生的事件的类型,调用相应的事件处理器
。它们定义了某个事件发生时,服务器应该执行的动作
。连接应答处理器
:如果是客户端要连接 Redis,那么为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器。
命令请求处理器
:如果是客户端要写数据到Redis(读、写请求命令),那么为了接收客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器。
命令回复处理器
:如果是客户端要从Redis 读数据,那么为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器。
服务器的监听套接字的 AE_READABLE事件应该正处于监听状态之下
,而该事件所对应的处理器为连接应答处理器
)当命令回复处理器将命令回复全部写入到套接字之后
,服务器就会解除客户端套接字的 AE_WRITABLE事件与命令回复处理器之间的关联)多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能
Redis的瓶颈不在CPU,而在内存和网络,内存不够可以增加内存或通过数据结构等进行优化 但Redis的网络IO的读写占用了发部分CPU的时间,如果可以把网络处理改成多线程的方式,性能会有很大提升
。也就是说Redis6.0版本引入多线程有两个原因【虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行
。因此,你也不需要担心线程安全问题。】
因为redis还是使用单线程模型来处理客户端的请求(执行命令还是使用单线程)
,只是使用多线程来处理数据的读写和协议解析
,所以是不会有线程安全的问题。Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题
PART1-3-3: 客户端&服务端
PART_客户端:通过使用由I/O多路复用技术实现的文件事件处理器
,Redis服务器使用单线程单进程的方式来处理命令请求
,并与多个客户端进行网络通信。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient结构(客户端状态
),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,源码如下:
/* With multiplexing we need to take per-client state.
* Clients are taken in a liked list.
*
* 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
*
* 多个客户端状态被服务器用链表连接起来。
*/
typedef struct redisClient {
// 客户端的套接字描述符。套接字描述符
int fd;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
// 客户端状态标志
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI*/
// 查询缓冲区
sds querybuf;
// 参数数量。argc属性则负责记录argv数组的长度。
int argc;
// 参数对象数组。服务器对客户端发来的命令请求的内容进行分析得出的命令参数。argv属性是一个数组,数组中的每个项都是一个字符串对象,其中 argv[0]是要执行的命令,而之后的其他项则是传给命令的参数。
robj **argv;
// 记录被客户端执行的命令。这个结构保存了命令的实现函数、命令的标志、命令应该给定的 参数个数、命令的总执行次数和总消耗时长等统计信息。
struct redisCommand *cmd, *lastcmd;
/* Response buffer */
// 回复偏移量。bufpos属性则记录了buf数组目前已使用的字节数量。
int bufpos;
// 回复缓冲区。buf是一个大小为REDIS_REPLY_CHUNK_BYTES字节的字节数组。REDIS_REPLY_CHUNK_BYTES常量目前的默认值为16*1024,也 即是说,buf数组的默认大小为16KB。
char buf[REDIS_REPLY_CHUNK_BYTES];
// 回复链表
list *reply;
// 当 server.requirepass 不为 NULL 时
// 代表认证的状态
// 0 代表未认证, 1 代表已认证
int authenticated; /* when requirepass is non-NULL */
// 创建客户端的时间。ctime属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒。CLIENT list命令的age域记录了这个秒数:
time_t ctime; /* Client creation time */
// 客户端最后一次和服务器互动的时间。lastinteraction属性记录了客户端与服务器最后一次进行互动(interaction)的时间,这里的互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。 lastinteraction属性可以用来计算客户端的空转(idle)时间,也即 是,距离客户端与服务器最后一次进行互动以来,已经过去了多少秒, CLIENT list命令的idle域记录了这个秒数:
time_t lastinteraction; /* time of the last interaction, used for timeout */
// 客户端的输出缓冲区超过软性限制的时间
time_t obuf_soft_limit_reached_time;
...
}
所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符
。目前Redis服务器会在两个地方用到伪客户端,一个用于载入AOF文件并还原数据库状态,而另一个则用于执行Lua脚本中包含的Redis命令。每个标志使用一个常量表示,一部分标志记录了客户端的角色),另一部分记录了客户端目前所处的状态
:
主服务器会成为从服务器的客户端
,而从服务器也会成为主服务器的客户端
。REDIS_MASTER标志表示客户端代表的是一个主服务器,REDIS_SLAVE标志表示客户端代表的是一个从服务器。并将得出的命令参数以及命令参数的个数分别保存到客户端状态的argv属性和argc属性
:当程序在命令表中成功找到argv[0]所对应的redisCommand结构时, 它会将客户端状态的cmd指针指向这个结构
:之后,服务器就可以使用cmd属性所指向的redisCommand结构,以及argv、argc属性中保存的命令参数信息,调用命令实现函数,执行客户端指定的命令。命令回复会被保存在客户端状态的输出缓冲区
里面,每个客户端都有两个输出缓冲区可用。当buf数组的空间已经用完,或者回复因为太大而没办法放进buf数组里面时,服务器就会开始使用可变大小缓冲区
。
/* With multiplexing we need to take per-client state.
* Clients are taken in a liked list.
*
* 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
*
* 多个客户端状态被服务器用链表连接起来。
*/
typedef struct redisClient {
...
//一个链表,保存了所有客户端的状态。Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历clients链表来完成:
list *clients; /* List of active clients */
...
}
将这个新的客户端状态添加到服务器状态结构clients链表的末尾
。PART_服务器:Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作
。举个例子,如果我们使用客户端执行命令:redis> SET KEY VALUE。那么从客户端发送SET KEY VALUE命令到获得回复OK期间,客户端和服务器共需要执行以下操作
:发送命令请求
SET KEY VALUE。
接收并处理客户端发来的命令请求
SET KEY VALUE, 在数据库中进行设置操作,并产生命令回复OK。
当下面四个操作都执行完了之后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器中取出并处理下一 个命令请求了
。
参数(保存在客户端状态的argv属性,参数个数(保存在客户端状态的argc属性))
),在命令表(command table)中查找参数所指定的命令,并将找到的**命令保存到客户端状态的cmd属性里面(服务器将执行命令所需的命令实现函数(保存在客户端状态的cmd属性))
**。(命令表将返回"set"键所对应的redisCommand结构,客户端状态的cmd指针会指向这个redisCommand结构)字典的键是一个个命令名字
,比 如"set"、“get”、"del"等等;而字典的值则是一个个redisCommand结构, 每个redisCommand结构记录了一个Redis命令的实现信息
/*
* Redis 命令
*/
struct redisCommand {
// 命令名字
char *name;
// 实现函数
redisCommandProc *proc;
// 参数个数
int arity;
// 字符串表示的 FLAG
char *sflags; /* Flags as string representation, one char per flag. */
// 实际 FLAG
int flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
// 指定哪些参数是 key
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// 统计信息
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
};
实现函数为setCommand
;命令的参数个数为-3
,表示命令接受三个或以上数量的参数;命令的标识为"wm"
,表示SET命令是一个写入命令,并且在执行这个命令之前,服务器应该对占用内存状况进行检查,因为这个命令可能会占用大量内存服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端 状态的argv属性和argv属性里面
,当服务器决定要执行命令时,它只要执行以下语句就可以了:(// client 是指向客户端状态的指针 ):client->cmd->proc(client);
当客户端的套接字变为可写状态时,命令回复处理器会将协议格式的命令回复"+OK\r\n"发送给客户端
),服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端
。 当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转
。
每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量
,并与stat_peak_memory保存的数值进行比较,如果当前使用的内存数量比stat_peak_memory属性记录的值要大,那么程序就将当前使 的内存数量记录到stat_peak_memory属性里面。serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作
服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程的ID,这两个属性也可以用于检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行
:
只要其中一个属性的值不为-1
,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:
如果rdb_child_pid和aof_child_pid两个属性的值都为-1
, 那么表示服务器没有在进行持久化操作,在这种情况下,程序执行以下三个检查:
struct redisServer {
...
//Redis服务器中有不少功能需要获取系统的当前时间,而每次获取 系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:
//保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;
//保存了毫秒级精度的系统当前UNIX时间戳
long long mstime;
// 最近一次使用时钟。服务器状态中的lruclock属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime属性、mstime属性一样。每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:当服务器要计算一个数据库键的空转时间(也即是数据库键对应的值对象的空转时间),程序会用服务器的lruclock属性记录的时间减去对象的lru属性记录的时间,得出的计算结果就是这个对象的空转时间:
unsigned lruclock:REDIS_LRU_BITS; /* Clock for LRU eviction */
// 已使用内存峰值
size_t stat_peak_memory; /* Max used memory record */
// AOF 文件的当前字节大小
off_t aof_current_size; /* AOF current size. */
int aof_rewrite_scheduled; /* Rewrite once BGSAVE terminates. */
// 负责进行 AOF 重写的子进程 ID
pid_t aof_child_pid; /* PID if rewriting process */
// 负责执行 BGSAVE 的子进程的 ID
// 没在执行 BGSAVE 时,设为 -1
pid_t rdb_child_pid; /* PID of RDB saving child */
// serverCron() 函数的运行次数计数器。服务器状态的cronloops属性记录了serverCron函数执行的次数:cronloops属性目前在服务器中的唯一作用,就是在复制模块中实现“每执行serverCron函数N次就执行一次指定代码”的功能,
int cronloops; /* Number of times the cron function run */
...
};
就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值
。 初始化server变量的工作由redis.c/initServerConfig函数完成。initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lua环境、共享 对象这些数据结构在之后的步骤才会被创建出来。 当initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段
——载入配置选项。服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件
,并根据用户设定的配置,对 server变量相关属性的值进行修改。服务器在载入用户指定的配置选项,并对server状态进行更新之后,服务器就可以进入初始化的第三个阶段——初始化服务器数据结构
。
服务器到现在才初始化数据结构的原因在于
,服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。如果在执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构
。为了避免出现这种麻烦的情况,服务器选择了将server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而initServer函数主要负责初始化数据结构
。并开始执行服务器的事件循环(loop)
。至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了。PART2-0:学技术还是不能忘了他的官方文档:http://www.redis.cn/documentation.html
降低通信成本,降低总RTT (Round Trip Time - 往返时间)
可以和redis建立一个连接,将多个命令一口气发送到服务器,而不用等待回复,最后在一个步骤中读取该答复
。PART2-1:Redis的应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜【通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜
】 等等
Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多
缓存与数据库,像咱们有时候用过单列集合或者map双列集合或者字节数组这种临时存个什么东西,他们起的作用不就是缓存的作用嘛。但是你要想把他们里面的东西持久化存储就得靠DB而不能靠这些伪缓存或者缓存了
缓存不是全量数据而DB是全量数据;缓存应该随着访问变化,缓存应该放请求的东西(热数据)
redis作为缓存:此时redis里面的数据必须随着业务变化,只保留热数据,因为内存大小是有限的(内存特大时还要啥DB)
Redis提供的有序集合数据类构
能实现各种复杂的排行榜应用。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景
。Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出
。Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可
我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点
)。不论是实现锁还是分布式锁,核心都在于“互斥”
。SETNX 命令是可以帮助我们实现互斥
。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法)。先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断【选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。】
。Redisson
Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制
。
Redis 还是有很多欠缺的地方比如消息丢失和堆积问题不好解决
。因此,我们通常建议是不使用 Redis 来做消息队列的,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka
。巨人的肩膀:
Redis设计与实现
https://www.javalearn.cn/
http://www.redis.cn/documentation.html:redis中文官方文档
javauide
https://twitter.com/alexxubyte/status/1498703822528544770
芋道源码老师的Dragonfly & Redis的速度问题、架构差异等讨论的文章
芋道源码老师关于Redis多线程架构的演进的文章