java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起

PART1-1:为什么Redis是单线程的

  • Redis单线程是指:
    • Redis的网络IO和键值对读写是由一个线程来完成的。这也是 Redis 对外提供键值存储服务的主要流程。Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实是由 其他的额外的 线程执行。
    • (指的是 Redis是单线程,通常是指在Redis 6.0之前,其核心网络模型使用的是单线程(Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型))
      • 高性能网络模式:Reactor 和 Proactor【都是一种基于事件分发的网络编程模式,区别在于 Reactor 模式是基于待完成的 I/O 事件,而 Proactor 模式则是基于已完成的 I/O 事件。】
        • Reactor:【具体,往下面翻翻】如果要让服务器服务多个客户端,那么最直接的方式就是为每一条连接创建线程或者进程【进程和线程的区别在于线程比较轻量级些,线程的创建和线程间切换的成本要小些】。处理完业务逻辑后,随着连接关闭后线程也同样要销毁了,但是这样不停地创建和销毁线程,不仅会带来性能开销,也会造成浪费资源,而且如果要连接几万条连接,创建几万个线程去应对也是不现实的
          • ----->为了解决这个问题可以使用资源复用的方式,创建一个线程池,将连接分配给线程,然后一个线程可以处理多个连接的业务有了池子之后不用频繁创建销毁了,但是一个线程要处理多个连接的业务,线程在处理某个连接的 read 操作时,如果遇到没有数据可读,就会发生阻塞,那么线程就没办法继续处理其他连接的业务。
            • ----->【没有用池子之前是一个连接对应一个线程时,线程一般采用「read -> 业务处理 -> send」的处理流程,如果当前连接没有数据可读,那么线程会阻塞在 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将不同的 I/O 多路复用函数封装成相同的 API 提供给上层使用,仅仅以单线程处理网络I/O,Redis的I/O多路复用模块提供的API如下:
                java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第1张图片
                //下面的方法不同版本的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)
                
        • Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成:
          • Reactor 负责 监听和分发事件,事件类型包含连接事件、读写事件;
            • Reactor 的数量可以只有一个,也可以有多个;
          • 处理资源池负责处理事件,如 read -> 业务逻辑 -> send
            • 处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程
        • 按照 Reactor 和处理资源池这两个核心部分可以有四种实现方案:【方案具体使用进程还是线程,要看使用的编程语言以及平台有关,Java 语言一般使用线程,比如 Netty;C 语言使用进程和线程都可以,例如 Nginx 使用的是进程,Memcache 使用的是线程
          • 单 Reactor 单进程 / 线程
            • 一般C 语言实现的是单 Reactor 单进程的方案,因为 C 语编写完的程序,运行后就是一个独立的进程,不需要在进程中再创建线程
              java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第2张图片
            • Java 语言实现的是单 Reactor 单线程的方案,因为 Java 程序是跑在 Java 虚拟机这个进程上面的,虚拟机中有很多线程,我们写的 Java 程序只是其中的一个线程而已
          • 单 Reactor 多进程 / 线程
            java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第3张图片
          • 多 Reactor 单进程 / 线程:没啥实际用处,PASS
            • 多 Reactor 单进程 / 线程实现方案相比单 Reactor 单进程 / 线程方案,不仅复杂而且也没有性能优势,因此实际中并没有应用
          • 多 Reactor 多进程 / 线程
            java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第4张图片
            • 要解决单 Reactor的问题,就是将单 Reactor实现成多 Reactor,这样就产生了第多 Reactor 多进程 / 线程的方案
        • Proactor:
          • Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式【Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型
            java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第5张图片
            • 阻塞 I/O:当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。阻塞等待的是内核数据准备好和数据从内核态拷贝到用户态这两个过程【如果 socket 设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问,而不做任何设置的话,默认是阻塞 I/O。无论 read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用。因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。】
              java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第6张图片
            • 非阻塞 I/O:非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果
              java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第7张图片
            • 异步I/O:
              • 异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待
                java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第8张图片
        • Reactor 和 Proactor 的区别:
          • Reactor 是非阻塞同步网络模式来了事件操作系统通知应用进程,让应用进程来处理,快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快】,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据
            • 事件就是有新连接、有数据可读、有数据可写的这些 I/O 事件
            • 处理包含从驱动读取到内核以及从内核读取到用户空间
          • Proactor 是异步网络模式, 【来了事件操作系统来处理,处理完再通知应用进程,快递员直接将快递送到你家门口,然后通知你】 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据
  • 在保证足够的性能表现之下,使用单线程可以保持代码的简单和可维护性:因为Redis是基于内存的操作(整个数据库加载是在内存中进行的),CPU不是Redis性能瓶颈(因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执行速度是非常快的),我不需要去切换时间片(多线程时会发生CPU上下文切换,很耗时)或者多核并行等。所以用单线程的就够了。
    java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第9张图片
    • 采用多线程,需要考虑多线程的系统设计问题以及系统代码的对共享资源的并发访问等问题,比较麻烦。在保证足够的性能表现之下,使用单线程可以保持代码的简单和可维护性。所以基于以上,Redis直接采用单线程的模式
    • Redis真正的性能瓶颈在于网络 I/O(真正能影响Redis的是机器的内存和网络带宽),也就是客户端和服务端之间的网络传输延迟,因此 Redis选择了单线程的 I/O 多路复用来实现它的核心网络模型
      • Redisv4.0(引入多线程处理异步任务)->Redis 6.0(在网络模型中实现多线程 I/O )。(Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上)【国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了】
        • Redis 6.0开启多线程后,是否会存在线程并发安全问题:从实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行redis的命令仍然是单线程顺序执行,所以我们不需要去考虑控制 Key、Lua、事务,LPUSH/LPOP 等等的并发及线程安全问题
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第10张图片
        • Redis 6.0为何引入多线程?
          • 因为Redis的网络 I/O 瓶颈已经越来越明显了,所以 Redis6.0引入多线程I/O,只是用来处理网络数据的读写和协议的解析(可以充分利用服务器 CPU 资源,目前主线程只能利用一个核+多线程任务可以分摊 Redis 同步 IO 读写负荷),而执行命令依旧是单线程
            在这里插入图片描述
            • 一个计算机中可以有多个redis进程,各个进程以port为区分
          • 随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:
            • 优化网络 I/O 模块:网络 I/O 的优化又可以分为两个方向:
              • 零拷贝技术或者 DPDK 技术:
                • 零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术
                • DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。
              • 利用多核优势:所以,最合适就是利用多核优势了。可以充分利用服务器 CPU 资源,目前主线程只能利用一个核。多线程任务可以分摊 Redis 同步 IO 读写负荷
            • 提高机器内存读写的速度:依赖于硬件的发展,暂时无解

PART1-2:为什么Redis是单线程的速度还快:Redis 为什么这么快?
java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第11张图片

  • 1:完全基于内存操作,所有操作都在内存上进行之外
    • Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)
  • 2:使用单线程模型来处理客户端的请求,避免了上下文的切换
    • 单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。
  • 3:Redis采用IO 多路复用机制,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
    • Linux的IO多路复用机制指单线程处理多个IO流,就是select/epoll 机制。在Redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给Redis线程处理,这就实现了一个Redis线程处理多个IO 流的效果
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第12张图片
  • 4:自身使用 C 语言编写,Redis使用了很多高效的数据结构,比如哈希表或者跳表,有很多优化机制,比如动态字符串sds

java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第13张图片

  • 5.Redis直接自己构建了 VM (虚拟内存)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
    • Redis的VM(虚拟内存)机制就是**暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)**。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
    • Redis提高数据库容量的办法有两种:
      • 一种是可以将数据分割到多个RedisServer上
      • 另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现

PART1-3-1:事件

  • Redis服务器是一个事件驱动程序
    • 事件的调度与执行:因为服务器中同时存在文件事件和时间事件两种事件类型,所以服 务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等。事件的调度和执行由ae.c/aeProcessEvents函数负责,在实际中,处理已产生 文件事件的代码是直接写在aeProcessEvents函数里面的
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第14张图片
    • 事件的调度和执行规则:
      • 1)aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询 (忙等待),也可以确保aeApiPoll函数不会阻塞过长时间
      • 2)因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时服务器就可以开始处理到达的时间事件了
      • 3)对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。比如说,在命令回复处理器将一个命令回复写入到客户端套接字时,如果写入字节数超过了一个预设常量的话,命令回复处理器就会主动用break跳出写入循环,将余下的数据留到下次再写;另外,时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行。
      • 4)因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些
    • Redis服务器需要处理以下两类事件:
      • 文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
        • 文件事件分为AE_READABLE事件(读事件)和AE_WRITABLE 事件(写事件)两类。
        • 每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件因为一个服务器通常会连接多个套接字所以多个文件事件有可能会并发地出现
          • 尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
            java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第15张图片
      • 时间事件(time event):Redis服务器中的一些操作(比如 serverCron函数)需要在给定的时间点执行而时间事件就是服务器对这类定时操作的抽象
        • Redis的时间事件分为以下两类: 一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值
          • 定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序X在当前时间的30毫秒之后执行一次。
            • 如果事件处理器返回ae.h/AE_NOMORE,那么这个事件为定时事件:该事件在达到一次之后就会被删除,之后不再到达。
          • 周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序Y每隔30毫秒就执行一次。
            • 如果事件处理器返回一个非AE_NOMORE的整数值,那么这个事件为周期性时间:当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之 后再次到达,并以这种方式一直更新并运行下去。比如说,如果一个时间事件的处理器返回整数值30,那么服务器应该对这个时间事件进行更 新,让这个事件在30毫秒之后再次到达。
        • 一个时间事件主要由以下三个属性组成:
          • id:服务器为时间事件创建的全局唯一ID(标识号)。ID号按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大。
          • when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间。
          • timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。
        • 时间事件的实现:服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第16张图片
          • 保存时间事件的链表为无序链表指的不是链表不按 ID排序,而是说,该链表不按when属性的大小排序。正因为链表没有按when属性进行排序,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理
          • 在目前版本中,正常模式下的Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件。在这种情况下,服务器几乎是将无序链表退化成一个指针来使 用,所以使用无序链表来保存时间事件,并不影响事件执行的性能
        • ae.c/aeCreateTimeEvent函数接受一个毫秒数milliseconds和一个时间事件处理器proc作为参数,将一个新的时间事件添加到服务器,这个新的时间事件将在当前时间的milliseconds毫秒之后到达,而事件的处理器为proc。
        • 持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行这些定期对自身的资源和状态进行检查和调整操作由redis.c/serverCron函数负责执行服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件
          • Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。在Redis2.6版本,服务器默认规定serverCron每秒运行10次,平均每间隔100毫秒运行一次。

PART1-3-2:Redis线程模型

  • Redis 6.0之前
    • Redis 是基于 reactor 模式开发了网络事件处理器(Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这个网络事件处理器叫做文件事件处理器(file event handler)。也就是说文件事件处理器是基于Reactor模式实现的网络通信程序。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。原理是文件事件处理器采用IO多路复用机制同时监听多个 Socket根据 socket 上的事件来选择对应的事件处理器来处理这个事件
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第17张图片
      • Reactor 模式本质上指的是使用 I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O) 的模式。
        • 通常设置一个主线程负责做 event-loop 事件循环和 I/O 读写,通过 select/poll/epoll_wait 等系统调用监听 I/O 事件,业务逻辑提交给其他工作线程去做
        • 非阻塞 I/O的核心思想是指避免阻塞在 read() 或者 write() 或者其他的 I/O 系统调用上【在 Reactor 模式中I/O 线程只能阻塞在 I/O multiplexing 函数上(select/poll/epoll_wait)】,这样可以最大限度的复用 event-loop 线程让一个线程能服务于多个 sockets。对Redis单线程Reactor模型的分析,我们知道 Redis的I/O线程除了在等待事件,其它的事件都是非阻塞的,没有浪费任何的CPU时间,这也是Redis能够提供高性能服务的原因
        • 除了只能使用一个CPU核心外,这个模型还有两个缺陷:
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第18张图片
          • 在value比较大的情况下会阻塞Redis服务
          • QPS难以更上一层楼
      • 虽然文件事件处理器以单线程方式运行,但通过使用IO多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性。
        • 文件事件处理器使用IO多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字 目前执行的任务为套接字关联不同的事件处理器
          • Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。 I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。
        • 当被监听的套接字准备好执行连接应答(accept)、读取 (read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
      • 文件事件处理器(file event handler)的结构包含了四个部分:
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第19张图片
        • 套接字:多个Socket(客户端连接)。Socket会产生AE_READABLE和AE_WRITABLE事件
          • ae.c/aeCreateFileEvent函数接受一个套接字描述符、一个事件类型,以及一个事件处理器作为参数,将给定套接字的给定事件加入到 I/O多路复用程序的监听范围之内,并对事件和事件处理器进行关联。
          • ae.c/aeDeleteFileEvent函数接受一个套接字描述符和一个监听事件 类型作为参数,让I/O多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联。
          • ae.c/aeGetFileEvents函数接受一个套接字描述符,返回该套接字正在被监听的事件类型:
            • 如果套接字没有任何事件被监听,那么函数返回AE_NONE。
            • 如果套接字的读事件正在被监听,那么函数返回 AE_READABLE。
            • 如果套接字的写事件正在被监听,那么函数返回 AE_WRITABLE。
            • 如果套接字的读事件和写事件正在被监听,那么函数返回AE_READABLE|AE_WRITABLE。
          • ae.c/aeWait函数接受一个套接字描述符、一个事件类型和一个毫秒数为参数,在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回。
          • ae.c/aeApiPoll函数接受一个sys/time.h/struct timeval结构为参数,并在指定的时间內,阻塞并等待所有被aeCreateFileEvent函数设置为监听 状态的套接字产生文件事件,当有至少一个事件产生,或者等待超时后,函数返回。
          • ae.c/aeProcessEvents函数是文件事件分派器,它先调用aeApiPoll函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件。
          • ae.c/aeGetApiName函数返回I/O多路复用程序底层所使用的I/O多路复用函数库的名称:
            • 返回"epoll"表示底层为epoll函数库,
            • 返回"select"表示底层为select函数库
        • IO 多路复用程序(支持多个客户端连接的关键):Redis的I/O多路复用程序的所有功能都是通过包装常见的select、 epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、 ae_epoll.c、ae_kqueue.c,诸如此类。
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第20张图片
          • I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件ae.h/AE_WRITABLE事件
            • I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,那 么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完之后,才处理AE_WRITABLE事件。(也就是说如果一个套接字又可读又可写的话,那么服务器将先读套接字,后写套接字。)
            • 这两类事件和套接字操作之间的对应关系如下:
              • 当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作,成为套接字变得可读),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作,称作有新的可应答套接字出现),套接字产生AE_READABLE事件。
              • 当套接字变得可写时(客户端对套接字执行read操作,称作套接字变得可写),套接字产生AE_WRITABLE事件。
          • Redis在I/O多路复用程序的实现源码中用#include宏定义了相应的规则,程序会在编译时自动选择系统中性能最高的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
          
          • I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字
        • 文件事件分派器(将 socket 关联到相应的事件处理器):文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接 字产生的事件的类型,调用相应的事件处理器
        • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器):Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求,也就是每个处理器对应不同的socket事件,
          • 服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作
          • 事件处理器包括:
            • 连接应答处理器:如果是客户端要连接 Redis,那么为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器。
              • networking.c/acceptTcpHandler函数是Redis的连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/accept函数的包装
                java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第21张图片
            • 命令请求处理器:如果是客户端要写数据到Redis(读、写请求命令),那么为了接收客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器。
              • networking.c/readQueryFromClient函数是Redis的命令请求处理器, 这个处理器负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read函数的包装。
                java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第22张图片
            • 命令回复处理器:如果是客户端要从Redis 读数据,那么为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器。
              • networking.c/sendReplyToClient函数是Redis的命令回复处理器,这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端,具体实现为unistd.h/write函数的包装。
                java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第23张图片
            • 复制处理器:当主服务器和从服务器进行复制操作时,主从服务器都需要关联 特别为复制功能编写的复制处理器。
      • IO多路复用:是五种 IO 模型的一种,有时也称为异步阻塞 IO,是基于经典的 Reactor 设计模式设计的
        • 多路复用主要有三种技术:
          • Select
          • Poll
          • Epoll:Epoll 是最新的也是目前最好的多路复用技术
      • 客户端与 Redis 通信的一次完整的流程:(一次完整的客户端与服务器连接事件)
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第24张图片
        • 多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件
        • Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。(意思就是:一个Redis服务器正在运作,那么这个服务器的监听套接字的 AE_READABLE事件应该正处于监听状态之下,而该事件所对应的处理器为连接应答处理器)
        • 如果一个客户端跟 Redis 发起连接,此时 Redis的监听套接字就会产生一个 AE_READABLE 事件并触发连接应答处理器执行。(由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件),这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket以及客户端状态,同时将这个 socket 的 AE_READABLE事件与命令请求处理器关联起来
        • 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联
        • 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。
        • 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。(当命令回复处理器将命令回复全部写入到套接字之后,服务器就会解除客户端套接字的 AE_WRITABLE事件与命令回复处理器之间的关联)
  • Redis 6.0
    • Redis6.0 之前为什么不使用多线程? 主要原因有 3 点:
      • 单线程编程容易并且更容易维护多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能
      • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
    • Redis在6.0的时候为什么又支持多线程了?
      • Redis的瓶颈不在CPU,而在内存和网络,内存不够可以增加内存或通过数据结构等进行优化 但Redis的网络IO的读写占用了发部分CPU的时间,如果可以把网络处理改成多线程的方式,性能会有很大提升。也就是说Redis6.0版本引入多线程有两个原因【虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。】
        • 1.充分利用服务器的多核资源
        • 2.多线程分摊 Redis 同步 IO 读写负荷
    • 开启关闭多线程机制:
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第25张图片
    • Redis 6.0 多线程的实现机制:
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第26张图片
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第27张图片
      • IO 线程要么同时在读 Socket,要么同时在写,不会同时读或写
      • IO 线程只负责读写 Socket 解析命令,不负责命令处理,执行命令依旧是单线程
    • redis 6.0之后又使用了多线程,不会有线程安全的问题吗?
      • 不会。因为redis还是使用单线程模型来处理客户端的请求(执行命令还是使用单线程)只是使用多线程来处理数据的读写和协议解析,所以是不会有线程安全的问题。
    • Redis 6.0 与 Memcached 多线程模型的对比:
      • 相同点:都采用了 Master 线程 -Worker 线程的模型。
      • 不同点:
        • Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。
        • Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题

PART1-3-3: 客户端&服务端

  • Redis服务器是典型的一对多服务器程序:Redis 是典型的 CS 架构(Client <—> Server)
    • 一个服务器可以与多个客户端建立网络连接
    • 每个客户端可以向服务器发送命令请求
    • 服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。【客户端通过 socket 与服务端建立网络通道然后发送请求命令,服务端执行请求的命令并回复。】

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;
    	...
    }
    
    • 客户端状态包含的属性可以分为两类:
      • 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性.
        • 套接字描述符:客户端状态的fd属性记录了客户端正在使用的套接字描述符。根据客户端类型的不同,fd属性的值可以是-1或者是大于-1的整数:执行CLIENT list命令可以列出目前所有连接到服务器的普通客户 端,命令输出中的fd域显示了服务器连接客户端所使用的套接字描述符:
          • 伪客户端(fake client)的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符。目前Redis服务器会在两个地方用到伪客户端,一个用于载入AOF文件并还原数据库状态,而另一个则用于执行Lua脚本中包含的Redis命令。
          • 普通客户端的fd属性的值为大于-1的整数:普通客户端使用套接字来与服务器进行通信,所以服务器会用fd属性来记录客户端套接字的描述符。因为合法的套接字描述符不能是-1,所以普通客户端的套接字描述符的值必然是大于-1的整数。
        • 名字:在默认情况下,一个连接到服务器的客户端是没有名字的。使用CLIENT setname命令可以为客户端设置一个名字,让客户端的身份变得更清晰。
        • 标志:客户端的标志属性flags记录了客户端的角色(role)(每个标志使用一个常量表示,一部分标志记录了客户端的角色),另一部分记录了客户端目前所处的状态
          • 一部分标志记录了客户端的角色:
            • 在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。REDIS_MASTER标志表示客户端代表的是一个主服务器,REDIS_SLAVE标志表示客户端代表的是一个从服务器。
            • REDIS_PRE_PSYNC标志表示客户端代表的是一个版本低于 Redis2.8的从服务器,主服务器不能使用PSYNC命令与这个从服务器进行同步。这个标志只能在REDIS_SLAVE标志处于打开状态时使用。
            • REDIS_LUA_CLIENT标识表示客户端是专门用于处理Lua脚本里面包含的Redis命令的伪客户端。
          • 另外一部分标志则记录了客户端目前所处的状态:
            • REDIS_MONITOR标志表示客户端正在执行MONITOR命令。
            • REDIS_UNIX_SOCKET标志表示服务器使用UNIX套接字来连接客户端。
            • REDIS_MULTI标志表示客户端正在执行事务。
          • flags属性的值可以是单个标志:flags =
          • 也可以是多个标志的二进制或,:flags = | | …
        • 输入缓冲区:客户端状态的输入缓冲区用于保存客户端发送的命令请求:输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但它的最大大小不能超过1GB,否则服务器将关闭这个客户端。
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第28张图片
        • 命令与命令参数:在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的argv属性和argc属性
        • 命令的实现函数:当服务器从协议内容中分析并得出argv属性和argc属性的值之后, 服务器将根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。当程序在命令表中成功找到argv[0]所对应的redisCommand结构时, 它会将客户端状态的cmd指针指向这个结构:之后,服务器就可以使用cmd属性所指向的redisCommand结构,以及argv、argc属性中保存的命令参数信息,调用命令实现函数,执行客户端指定的命令。
        • 输出缓冲区:执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用。当buf数组的空间已经用完,或者回复因为太大而没办法放进buf数组里面时,服务器就会开始使用可变大小缓冲区
          • 一个缓冲区的大小是固定的:固定大小的缓冲区用于保存那些长度比较小的回复,比如OK、简短的字符串值、整数值、错误回复等等。
          • 另一个缓冲区的大小是可变的:可变大小的缓冲区用于保存那些长度比较大的回复,比如一个非常长的字符串值,一个由很多项组成的列表,一个包含了很多元素的集合等等。
            • 可变大小缓冲区由reply链表和一个或多个字符串对象组成:通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区16KB大小的限制。
              • 理论上来说,这个缓冲区可以保存任意长的 命令回复。但是,为了避免客户端的回复过大,占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作。 服务器使用两种模式来限制客户端输出缓冲区的大小:
                • 硬性限制(hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
                • 软性限制(soft limit):如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态结构的obuf_soft_limit_reached_time属性记录下客户端到达软性限制的起始时间;之后服务器会继续监视客户端,如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端;相反地,如果输出缓冲区的大小在指定时间之内,不再超出软性限制,那么客户端就不会被关闭,并且obuf_soft_limit_reached_time属性的值也会被清零。
        • 身份验证:客户端状态的authenticated属性用于记录客户端是否通过了身份验证:如果authenticated的值为0(当客户端authenticated属性的值为0时,除了AUTH命令之外,客户端发送的所有其他命令都会被服务器拒绝执行),那么表示客户端未通过身份验证;如果 authenticated的值为1(这时客户端就可以像往常一样向服务器发送命令请求了),那么表示客户端已经通过了身份验证。
          • authenticated属性仅在服务器启用了身份验证功能时使用。如果服务器没有启用身份验证功能的话,那么即使authenticated属性的值为 0(这是默认值),服务器也不会拒绝执行客户端发送的命令请求。
      • 另外一类是和特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_keys属性等等
    /* 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 */
    	...
    }
    

    java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第29张图片

    • 服务器使用不同的方式来创建和关闭不同类型的客户端:
      • 创建普通客户端:如果客户端是通过网络连接与服务器进行连接的普通客户端,那么 在客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾
      • 关闭普通客户端:
        • 如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
        • 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭。
        • 如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭。
      • Lua脚本的伪客户端:服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中:lua_client伪客户端在服务器运行的整个生命期中会一直存在,只有服务器被关闭时,这个客户端才会被关闭。
      • AOF文件的伪客户端:服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,并在载入完成之后,关闭这个伪客户端。

PART_服务器:Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。

  • 一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。举个例子,如果我们使用客户端执行命令:redis> SET KEY VALUE。那么从客户端发送SET KEY VALUE命令到获得回复OK期间,客户端和服务器共需要执行以下操作
    java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第30张图片
    • 1)客户端向服务器发送命令请求SET KEY VALUE。
      • Redis服务器的命令请求来自Redis客户端,当用户在客户端中键入一个命令请求时,*客户端会将这个命令请求转换成协议格式(比如,这个协议格式:3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n),然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第31张图片
    • 2)服务器接收并处理客户端发来的命令请求SET KEY VALUE, 在数据库中进行设置操作,并产生命令回复OK。
      • 当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
        • 1)读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
          在这里插入图片描述
        • 2)对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性里面。
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第32张图片
        • 3)调用命令执行器,执行客户端指定的命令。当下面四个操作都执行完了之后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器中取出并处理下一 个命令请求了
          • 命令执行器要做的第一件事就是根据客户端状态的argv[0]参数(参数(保存在客户端状态的argv属性,参数个数(保存在客户端状态的argc属性))),在命令表(command table)中查找参数所指定的命令,并将找到的**命令保存到客户端状态的cmd属性里面(服务器将执行命令所需的命令实现函数(保存在客户端状态的cmd属性))**。(命令表将返回"set"键所对应的redisCommand结构,客户端状态的cmd指针会指向这个redisCommand结构)
            java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第33张图片
          • 命令表是一个字典,字典的键是一个个命令名字,比 如"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;
          };
          
          • 以SET命令为例:SET命令的名字为"set",实现函数为setCommand命令的参数个数为-3,表示命令接受三个或以上数量的参数;命令的标识为"wm",表示SET命令是一个写入命令,并且在执行这个命令之前,服务器应该对占用内存状况进行检查,因为这个命令可能会占用大量内存
          • 命令执行器要做的第二件事就是:在真正执行命令之前,程序还需要执行一些预备操作,从而确保命令可以正确、顺利地被执行
          • 命令执行器要做的第三件事就是:调用命令的实现函数。服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端 状态的argv属性和argv属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:(// client 是指向客户端状态的指针 ):client->cmd->proc(client);
            • 被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里面(buf属性和 reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。
          • 命令执行器要做的第四件事就是:在执行完实现函数之后,服务器还需要执行一些后续工作:比如:
            • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
            • 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面。
            • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将 刚刚执行的命令传播给所有从服务器。
            • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds属性,并将命令的redisCommand结构 的calls计数器的值增一。
    • 3)服务器将命令回复OK发送给客户端。
      • 命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时(当客户端的套接字变为可写状态时,命令回复处理器会将协议格式的命令回复"+OK\r\n"发送给客户端),服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。 当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。
    • 4)客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。
      • 当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看(假设我们使用的是Redis自带的redis-cli客户端)。继续以之前的SET命令为例子,当客户端接到服务器发来 的"+OK\r\n"协议回复时,它会将这个回复转成"OK\n",然后打印给用户看:
  • serverCron函数:Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转
    • serverCron函数默认会以每100毫秒一次的频率更新unixtime属 性和mstime属性,所以这两个属性记录的时间的精确度并不高:
      • 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上。
      • 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间。
    • serverCron函数默认会以每10秒一次的频率更新lruclock属性的值, 因为这个时钟不是实时的,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估算值。
    • serverCron函数中的trackOperationsPerSecond函数会以每100毫秒一 次的频率执行,这个函数的功能是以抽样计算的方式,估算并记录服务 器在最近一秒钟处理的命令请求数量,这个值可以通过INFO status命令 的instantaneous_ops_per_sec域查看:
    • 服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小:每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量,并与stat_peak_memory保存的数值进行比较,如果当前使用的内存数量比stat_peak_memory属性记录的值要大,那么程序就将当前使 的内存数量记录到stat_peak_memory属性里面。
    • serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会对一定数量的客户端进行以下两个检查:
      • 如果客户端与服务器之间的连接已经超时(很长一段时间里客户端和服务器都没有互动),那么程序释放这个客户端。
      • 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存。
    • serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作
    • 在服务器执行BGSAVE命令的期间,如果客户端向服务器发来 BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后。 服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了 BGREWRITEAOF命令:每次serverCron函数执行时,函数都会检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行,如果这两个命令都没在执行,并且aof_rewrite_scheduled属性的值为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令。
    • 服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程的ID,这两个属性也可以用于检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行
      • 每次serverCron函数执行时,程序都会检查rdb_child_pid和 aof_child_pid两个属性的值,只要其中一个属性的值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:
        • 如果有信号到达,那么表示新的RDB文件已经生成完毕(对于 BGSAVE命令来说),或者AOF文件已经重写完毕(对于 BGREWRITEAOF命令来说),服务器需要进行相应命令的后续操作, 比如用新的RDB文件替换现有的RDB文件,或者用重写后的AOF文件替 换现有的AOF文件。
        • 如果没有信号到达,那么表示持久化操作未完成,程序不做动作。
      • 如果rdb_child_pid和aof_child_pid两个属性的值都为-1, 那么表示服务器没有在进行持久化操作,在这种情况下,程序执行以下三个检查:
        • 1)查看是否有BGREWRITEAOF被延迟了,如果有的话,那么开始一次新的BGREWRITEAOF操作(这就是上一个小节我们说到的检 查)。
        • 2)检查服务器的自动保存条件是否已经被满足,如果条件满足, 并且服务器没有在执行其他持久化操作,那么服务器开始一次新的BGSAVE操作(因为条件1可能会引发一次BGREWRITEAOF,所以在这个检查中,程序会再次确认服务器是否已经在执行持久化操作了)。
        • 3)检查服务器设置的AOF重写条件是否满足,如果条件满足,并且服务器没有在执行其他持久化操作,那么服务器将开始一次新的BGREWRITEAOF操作(因为条件1和条件2都可能会引起新的持久化操 作,所以在这个检查中,我们要再次确认服务器是否已经在执行持久化操作了)。
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 */
	... 
};
  • 初始化服务器:一个Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等等
    • 初始化服务器的第一步:就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。 初始化server变量的工作由redis.c/initServerConfig函数完成。initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lua环境、共享 对象这些数据结构在之后的步骤才会被创建出来当initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段——载入配置选项。
    • 初始化服务器的第二步:在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对 server变量相关属性的值进行修改。服务器在载入用户指定的配置选项,并对server状态进行更新之后,服务器就可以进入初始化的第三个阶段——初始化服务器数据结构
      • 配置选项相关的服务器状态属性的情况是这样:
        • 如果用户为这些属性的相应选项指定了新的值,那么服务器就使用用户指定的值来更新相应的属性。
        • 如果用户没有为属性的相应选项设置新的值,那么服务器就沿用之前initServerConfig函数为属性设置的默认值。
    • 初始化服务器的第三步:初始化服务器数据结构(当初始化服务器进行到这一步,服务器将调用initServer函数,为下面提到的数据结构分配内存,并在有需要时,为这些数据结构设置或者关联初始化值。当initServer函数执行完毕之后,服务器将用ASCII字符在日志中打 印出Redis的图标,以及Redis的版本号信息:)。在之前执行initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构。
      • 不过除了命令表之外,服务器状态还包含其他数据结构。如:
        • server.clients链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个redisClient结构实例。
        • server.db数组,数组中包含了服务器的所有数据库。
        • 用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
        • 用于执行Lua脚本的Lua环境server.lua。
        • 用于保存慢查询日志的server.slowlog属性
      • 服务器到现在才初始化数据结构的原因在于,服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。如果在执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构为了避免出现这种麻烦的情况,服务器选择了将server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而initServer函数主要负责初始化数据结构
    • 初始化服务器的第四步:还原数据库状态:在完成了对服务器状态server变量的初始化之后,服务器需要载入 RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
      • 根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同: 当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长
        • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态。
        • 相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态。
    • 初始化服务器的第五步:在初始化的最后一步,服务器将打印出以下日志:[5244] 21 Nov 22:43:49.084 * The server is now ready to accept connections on port。并开始执行服务器的事件循环(loop)。至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了。

PART2-0:学技术还是不能忘了他的官方文档:http://www.redis.cn/documentation.html

  • 管道【redis中文官方文档中的管道】降低通信成本,降低总RTT (Round Trip Time - 往返时间)
    • 客户端和服务器通过网络进行连接。这个连接可以很快(loopback接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。这个时间被称之为 RTT (Round Trip Time - 往返时间)。redis管道)用来在一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就 可以和redis建立一个连接,将多个命令一口气发送到服务器,而不用等待回复,最后在一个步骤中读取该答复
    • (运维那边搞的)管道用在了,Redis冷启动,有些时候,Redis实例需要装载大量用户在短时间内产生的数据,数以百万计的keys需要被快速的创建,也就是大量数据插入(mass insertion)
  • Redis发布订阅:【redis官方文档中的发布订阅】
    • 发布者发布的消息分到不同的频道,不需要知道什么样的订阅者订阅。订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道什么样的发布者发布的。这种发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑。
    • 发布订阅可以解决实时消息发布通知
      • 历史消息,全量历史消息数据用关系数据库存,但是用redis缓存来解决数据的读请求。用redis的sorted-set可以实现历史消息数据请求,比如最近三天的历史消息,
      • redis整个实现实时消息发布订阅+历史消息查询存储(比如最近三天消息查询)
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第34张图片
  • redis的事务:【redis中文官方文档的事务】
    • MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务可以一次执行多个命令。MULTI 命令用于开启一个事务,它总是返回 OK 。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。另一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。
      • 多个client的谁的exec先到达redis谁的exec先执

PART2-1:Redis的应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜【通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜 等等
java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第35张图片

  • Redis用作缓存:
    • 缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多
      • Redis用做缓存后,一般持久化就RDB就够用,因为缓存只是存储热数据,全量数据在缓存背后的巨人DB哪里存着
      • 缓存与数据库,像咱们有时候用过单列集合或者map双列集合或者字节数组这种临时存个什么东西,他们起的作用不就是缓存的作用嘛。但是你要想把他们里面的东西持久化存储就得靠DB而不能靠这些伪缓存或者缓存了
        • 缓存不是全量数据而DB是全量数据;缓存应该随着访问变化,缓存应该放请求的东西(热数据)
        • redis作为缓存:此时redis里面的数据必须随着业务变化,只保留热数据,因为内存大小是有限的(内存特大时还要啥DB)
  • Redis用作DB,一般会使用AOF来进行全量数据的持久化存储,当然,4.0以后也是混合了,所以性能还行。
  • Redis用作排行榜:
    • 很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等Redis提供的有序集合数据类构能实现各种复杂的排行榜应用
  • Redis用作计数器:
    • 什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景
  • Redis用作分布式会话:
    • 集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
  • Redis用作社交网络:
    • 点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出
  • Redis用作最新列表:
    • Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可
  • Redis用作消息系统:
    • 消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比
  • Redis如何实现分布式锁?【通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁】来自“悟空聊架构”老师的关于分布式锁实现的文章(通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点)。不论是实现锁还是分布式锁,核心都在于“互斥”
    java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第36张图片
    • 捡田螺的小男孩老师关于Redis 实现分布式锁的 7 种方案
      • Java基基关于分布式锁的具体实现
    • 在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多
      • 常见的分布式锁有哪些解决方案?实现分布式锁目前有三种流行方案,即基于关系型数据库、Redis、ZooKeeper 的方案
        • 基于关系型数据库,如MySQL 基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等
          在这里插入图片描述
        • 基于Redis实现:芋道源码老师的关于Spring Boot中加一个注解实现 Redis 分布式锁
          在这里插入图片描述
        • 基于zookeeper
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第37张图片
    • 在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法)。先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
      • 加锁&释放锁
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第38张图片
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第39张图片
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第40张图片
        • 为了误删到其他的锁,这里我们 建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断【选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。】
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第41张图片
        • 为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 key(也就是锁) 设置一个过期时间
          在这里插入图片描述
        • 如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。所以出现了现成的解决方案:Redisson
          • Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。Redisson 中的分布式锁自带自动续期机制,它提供了一个专门用来监控锁的 Watch Dog( 看门狗),如果操作共享资源的还未完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放
            java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第42张图片
            • RedLock官方推荐,Redisson完成了对Redlock算法封装【Redlock是一种算法,Redlock也就是 Redis Distributed Lock,可用实现多节点Redis的分布式锁。】
              java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第43张图片
              • RedLock的原理:
                java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第44张图片
            • Redis 如何解决集群情况下分布式锁的可靠性?javaGuide老师文章,极力推举
      • 锁超时:锁超时指的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程就别进来
        • 所以,setnx的key必须用【expire key 30】设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx不支持超时参数,所以需要额外指令
      • 通过上述setnx 、del和expire实现的分布式锁还是存在着一些问题:
        • SETNX 和 EXPIRE 非原子性:假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第45张图片
        • 锁误解除:如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第46张图片
        • 超时解锁导致并发:如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
          java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第47张图片
        • 不可重入:当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。
        • 无法等待锁释放:上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。
          在这里插入图片描述
      • 如果在setnx之后执行expire.之前进程意外crash或者要重启维护了,那会怎么样?
        • set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!
  • 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
    • Lua脚本和 MySQL数据库的存储过程比较相似,他们执行一组命令,所有命令的执行要么全部成功或者失败,以此达到原子性。也可以把Lua脚本理解为,一段具有业务逻辑的代码块
    • Lua本身就是一种编程语言,虽然redis 官方没有直接提供限流相应的API,但却支持了 Lua 脚本的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一
      java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第48张图片
  • 消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制
    • Redis 5.0 新增加的一个数据结构 Stream 可以用来做消息队列,Stream 支持:
      • 发布 / 订阅模式;按照消费者组进行消费;消息持久化( RDB 和 AOF)
    • 不过,和专业的消息队列相比,Redis 还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议是不使用 Redis 来做消息队列的,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka
    • javaGuide老师关于消息队列
  • 分布式缓存常见的技术选型方案有哪些?
    • 分布式缓存的话,比较老牌同时也是使用的比较多的还是 Memcached 和 Redis。不过,现在基本没有看过还有项目使用 Memcached 来做缓存,现在分布式缓存都是直接用 Redis【腾讯也开源了一款类似于 Redis 的分布式高性能 KV 存储数据库,基于知名的开源项目 RocksDB 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型,名为 Tendis。】
    • Redis 和 Memcached 的区别和共同点:
      • 共同点 :
        • 都是基于内存的数据库,一般都用来当做缓存使用
        • 都有过期策略,并且两者的性能都非常高。
      • 区别
        java基础巩固-宇宙第一AiYWM:为了维持生计,Redis基础Part6(Redis的应用场景、Redis是单线程的&速度还快、Redis线程模型:Reactor模式、事件、发布订阅、管道)~整起_第49张图片

巨人的肩膀:
Redis设计与实现
https://www.javalearn.cn/
http://www.redis.cn/documentation.html:redis中文官方文档
javauide
https://twitter.com/alexxubyte/status/1498703822528544770
芋道源码老师的Dragonfly & Redis的速度问题、架构差异等讨论的文章
芋道源码老师关于Redis多线程架构的演进的文章

你可能感兴趣的:(持久层,从哪里来,存哪里去,怎么存,存多久,redis,redis应用场景,redis线程模型,redis单线程)