Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘的负载。还有一个好处,Redis可以提供多种缓冲区同步硬盘的策略。
write操作会触发延迟写机制,Linux在内核提供页缓冲区用于提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制:缓冲区页空间写满或达到特定时间周期
fsync针对单个文件操作,做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化
AOF持久化开启且存在AOF文件时,优先加载AOF文件
写时复制父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的也创建副本,而子进程在fork操作过程中共享整个父进程内存快照
当开启AOF持久化时,常用的同步硬盘策略是everysec,对于这种方式,Redis使用另一条线程每秒执行fsync同步硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞。
Redis单线程架构导致无法充分利用CPU多核特性,通常的做法是在一台机器上部署多个Redis实例.
当从节点正在复制主节点时,如果出现网络中断或者命令丢失的情况,从节点会向主节点要求补发丢失的命令,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点。
主从节点建立复制后,他们之间维护着长连接并彼此发送心跳命令
会出现如下问题
特点:
Redis主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景是无法接受的,所以引入哨兵模式(Sentinel)
====================================================================================================
redis 3.x 是单线程: 1.数据结构简单 2避免锁的开销和上下文切换 3 可以有很高的qps
redis 6.x 之后告别了单线程,用一种全新的多线程来解决问题
由于是单线程所以每个命令都是排队执行的,当前一个命令是处理一个bigkey(删除一个非常大的对象,那么del命令就会造成redis主线程卡顿)的时候,会造成阻塞,特别是删除一个bigkey的时候,所以在redis 4.x版本后引入了多线程异步删除
怎么处理删除一个大key的问题?可以采用惰性删除
删除bigkey可以用 unlink 命令 (把删除操作交给了子线程异步来操作)
IO多路复用
这是IO模型中的一种,Reactor模式,IO多路复用简单来说就是通过监测文件的读写事件再通知线程执行相关操作,保证redis的非阻塞IO能顺利执行完成的机制
多路指的是多个socket连接
复用指的是复用一个线程,多路复用主要有三种技术:select,poll,epoll
IO的读与写本身是阻塞的,比如当socket中有数据时,redis会通过调用先将数据从内核态拷贝到用户态空间,再交给redis调用,而这个拷贝的过程是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
多线程化只是将网络IO的部分进行多线程,而执行操作还是只有一个主线程来执行。
redis6.x 多线程是默认关闭的,需要配置conf文件进行设置
问题:现在有50亿个电话号码,现有10万个电话号码,如何快速准确的判断这些电话号码是否存在?
布隆过滤器实际上是一个很长的二进制数组+一系列随机hash算法映射函数,主要用于判断一个元素是否存在集合中。高效的插入和查询,占用空间少,返回的结果是不确定性的。
一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候一定不存在(有一定的误差,hash冲突)
布隆过滤器可以添加元素,但是不能删除元素,因为删除元素会导致误判率增加
布隆过滤解决缓存穿透的问题:一般情况下,先查询缓存redis是否有该条数据,缓存中没有时,再查询数据库,当数据库中不存在该条数据时,每次查询都访问到数据库,这就是缓存穿透。在redis和mysql外面再套一层布隆过滤器用于判断是否存在该数据。
添加元素时:使用多个hash函数对key进行hash运算得到一个整数索引,对位数组长度进行取模运算得到一个位置。每个hash函数都会得到一个不同的位置,将这几个位置都设置为1就完成啦add操作。
查询key的时候:只要其中一位是0就表示这个key不存在,但如果都是1,不一定存在对应的key
为什么不能删除元素呢?
如果删除obj2,那么3号位会被设置为0,这时候会影响到obj1,布隆过滤器的误判率大概是0.03
缓存雪崩:redis主机挂了,redis全盘崩溃,比如缓存中有大量数据同时过期
解决办法:redis缓存集群的高可用,主从+哨兵或者redis cluster(redis集群)。开启redis持久化机制aof/rbd,尽快恢复缓存集群,降级和限流。
假设现在一秒有5k的请求,(用户发送一个请求,先查询本地缓存,然后在查询redis和mysql,查询出mysql中的结果写入redis和本地缓存),事前:要保证redis集群的高可用,假设这时候突然挂了一组,这时候要用到限流组件,只处理其中的2000个请求,剩余的3000个请求服务降级(服务中断请稍后重试,或者返回默认值),这时候这2000个请求直接访问mysql数据库,最后用redis持久化机制马上恢复缓存。
缓存穿透:redis没有,mysql也没有,一直查询到mysql,redis不起作用
处理办法:空对象缓存和布隆过滤器
空对象存储:redis和mysql都没有该查询数据的时候,我们就在redis中设置该查询数据为null,缺点是redis中没用的key会越来越多
缓存击穿:大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求打到数据库上面。简单的说就是热点key失效了,暴打mysql
处理办法:
可以用setnx 设置一个全局的key,设置为加锁,操作完成之后再del
分布式锁所具备的条件和刚需:
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求,防止关键业务出现并发攻击
定期删除策略是立刻删除和惰性删除的折中,但是定期删除的频度设置会导致其退化成前两种,所以有了缓存淘汰策略八种
在配置文件中
四个方面:LRU, LFU, random, ttl 分别对全部键和设置了过期时间的key
redis默认的是noevciction:不会驱逐任何key ,一般采用 allkeys-lru :即是对所有的key采用lru算法
set hello world 这个命令,因为redis是kv键值对的数据库,每个键值对都会有一个dicEntry来存储
redisObject对象
结构:包含 len,alloc,flags,buf[]四个字段
优点:
对于长度小于44的字符串,Redis对键值采用embstr,embstr表示:embedded string,表示嵌入式的string,从内存结构上来讲,即字符串sds与其对应的redisObject对象分配在同一连续的内存空间,字符sds嵌入在redisObject对象之中。这样连续紧凑之后,可以减少内存碎片。
如果是raw,则内存不再连续。一旦对key做修改,就直接改变为raw类型
底层由 ziplist+hashtable
由默认的 hash-max-ziplist-entries 和 hash-max-ziplist-value 两个参数
ziplist
ziplist压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间。即以部分读写性能为代价,来换取极高的内存空间利用率,因此只会用于字段个数比较少,且字段值也比较小的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。,也就是减少碎片。利用压缩来保证内存的连续。
ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和下一个链表节点的指针,而是存储上一个链表长度和当前节点长度。
对双写一致性的理解:
什么时候同步直写?
小数据,某条,某一小撮热点数据,要求立刻变更,可以前台服务降级一下,后台马上同步直写。
什么时候异步缓写?
1 正常业务,马上更新mysql,可以在业务上容许出现一个小时后redis起效。
2 出现异常后,不得不将失败动作重新修补。
数据库和缓存一致性的几种更新策略?
给缓存设置过期时间,是保证最终一致性的解决方案。
先更新mysql某件商品为99,然后更新redis为99,这时候如果redis出现异常了(或者redis更新操作慢了一点),导致redis还是100,最终导致mysql99,redis100,redis会读取到脏数据。
1 A线程先成功删除了缓存,然后去更新mysql,在更新mysql的过程中,线程B这时候突然来读取缓存数据。
2 此时redis里面的数据是空的,B线程来读取,先去读取redis里面的数据,此时出现2个问题:
2.1 此时B发现缓存失效了,会发生缓存击穿,直接访问到mysql上(mysql里面的还是旧数据),如果是高并发的场景会很危险。
2.2 B会把获取的旧值写回redis,也就是说A的工作无效了,等mysql更新完成,redis和mysql数据不一致。
处理方法:采用延迟双删策略。加上sleep时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后线程A再进行删除。所以线程A sleep时间就需要大于线程B读取数据再写入缓存的时间。由于sleep会导致业务吞吐量降低,可以再开一个守护线程去删除数据。
A线程更新数据库,还没来得及删除缓存,B就来读取了,这时候就会造成B线程读取到脏数据。
处理办法:
第2种方法读取到的是mysql中的脏数据,而第3种是从redis种读取旧数据,第3种方案更好,理由如下:
1 先删除缓存再更新数据库,有可能导致请求请求因缓存缺失而访问到数据库,给数据库带来压力,严重导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间不好设置
如果使用第三种方法,如果业务层要求必须读取一致性的数据,那么我们就需要再更细数据库时,先在redis缓存客户端暂存并发读请求,等数据库更新玩,缓存值删除后,再读取数据,从而保证数据一致性。
redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,一次放到文件事件分派器,事件分派器将事件分发给事件处理器。
左边有三个命令,分别是第一个用户建立连接,第二个用户写命令,第三个用户读命令。通过socket连接之后通过IO多路复用放入事件队列中,再通过事件派发器分发给不同的处理器。
redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或者输出都是阻塞的,所以IO操作一般情况下往往不能直接返回,这会导致某一文件的IO阻塞导致整个进程无法对其他客户提供服务,而IO多路复用就是为了解决这个问题而春夏的。
所谓的IO多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要select,poll,epoll来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
redis服务采用Reactor的方式来实现文件事件处理器(每一个网络连接其实对应一个文件描述符)Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器,它的组成结构为四个部分:
多个套接字
IO多路复用程序
文件事件分派器
事件处理器
因为文件事件分派器队列消费是单线程的,所以redis才叫做单线程模型。
IO:网络IO
多路:多个客户端连接(连接就是套接字描述符)
复用:复用一个或者多个线程,也就是说一个或一组线程处理多个TCP连接,使用单进场就能够实现同时处理多个客户端的连接
一句话:一个服务端进程可以同时处理多个套接字描述符,其发展可以分为:select->poll->epoll三个阶段。
同步与异步的理解:同步,异步的讨论对象是服务提供者,重点在于获得调用结果的消息通知方式上。
阻塞与非阻塞的理解:阻塞,非阻塞的讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其她事情。
当用户进程调用了recvfrom(recvfrom本函数用于从(已经连接)的套接口上接收数据,并捕获数据发送源的地址)这个系统调用,kernel(内核态)就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存中。然后kernel返回结果,用户进程才接触block的状态,重新运行起来。所以,BIO的特点就是在IO执行的两个阶段都被block。也就是用户态和内核态都在阻塞,BIO会阻塞在accept()函数,该函数表示监听。第二个地方阻塞是阻塞在read()函数,表示服务端等待客户端的发送数据。也就是说服务端会首先阻塞在监听函数accept,当有客户端连接过来,第二次阻塞在read函数等待客户端写数据。当阻塞在read的时候其他客户端无法与服务端建立连接。
上面的模型存在很大的问题,如果客户端与服务端建立了连接,如果这个连接的客户端迟迟不发数据,进程就会一直阻塞在read()方法上,这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好。
可以采用多线程来解决上述问题,我们用主线程来监听accept方法,当有一个客户端来连接,就开启一个子线程来处理对应的连接,也就是开启一个子线程来处理read方法。
采用多线程的方法,出现的问题就是开辟的线程太多,会大量消耗内存。
所以可以采用NIO(非阻塞式IO)
因为read方法阻塞了,所以要开辟多个线程,如果有什么方法能使read方法不阻塞,这样就不用开辟多个线程了,这就用到了另一个IO模型,NIO。
在NIO模式中,一切都是非阻塞的:
accept()方法是非阻塞的,如果没有客户端连接,就返回error
read()方法是非阻塞的,如果read()方法读取不到数据就返回error,如果读取到数据时只阻塞read()方法读取数据的时间。
在NIO模式中,只有一个线程:
当一个客户端与服务端连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,看这个socket的read()方法能否读取到数据,这样一个线程就能处理客户端的连接和读取了。
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程的角度讲,它发起一个read操作,并不需要等待,俄日是马上就得到一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后再返回。所以,NIO的特点是用户进程需要不断主动询问内核数据准备好了吗?
在非阻塞式IO模型中,应用程序把一个套接口设置为非阻塞,就是告诉内核,当所请求的IO操作无法完成时,不要将程序睡眠而是返回一个“错误”,应用程序基于IO操作函数将不断的轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。
上述方法会出现的问题:
1 如果socket一多,存放socket的数组就会越来越大
2 如果出现数组中大部分socket都是没有数据的,我也不得不去遍历,造成cpu资源的浪费。
3 而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核态的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大。
优点:不会阻塞在内核的等待数据过程,每次发起的IO请求可以立即返回,不用阻塞等待,实时性较好。
缺点:轮询将会不断地询问内核,者将占用大量的CPU时间,系统资源利用率较低,所以一般web服务器不使用这种IO模型。
结论:让linux内核搞定上述需求,我们将一批文件描述符(多个socket)通过一次系统调用传给内核,由内核去遍历,才能真正的解决这个问题。IO多路复用应用应运而生,也几即是将上述工作直接放进linux内核,不再两状态转换而是直接从内核获得结果,因为内核是非阻塞的。
IO多路复用就是我们说的select,poll,epoll,有些地方也称这种IO方式为事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
IO多路复用快的原因在于,操作系统提供了这样的系统调用,是的原来的while循环里多次系统调用,变成了一次系统调用+内核层遍历这些文件描述符
将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select,poll,epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
基于IO多路复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
reactor模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,reactor模式也叫dispatcher模式。即IO多路复用统一监听事件,收到事件后分发给某个进程,是编写高性能网络服务器的必备技术。
reactor模式中有2个关键组成:
所谓的IO多路复用机制指内核一旦发现进程指定的一个或多个IO条件准备读取,它就通知该进程,就是说通过一种机制,可以监听多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制使用需要select,poll,epoll来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接,当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,并开始业务处理。
select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
分析select函数的执行过程:
其实select就是在内核中运行accept,read等操作。首先会设置一个bitmap类型的rset,然后阻塞在select,当有数据的时候,我们就设置对应的rset位置为1,然后在一个for循环执行read等操作。
select其实就是把NIO中用户态要遍历的fd数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所以拷贝到内核态后,这样的遍历判断的时候就不用一直用户态和内核态频繁切换了
从代码中可以看出,select系统调用后,返回了一个置位后的rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率。
select函数的缺点:
select方式,即做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次select的系统调用+N次就绪状态的文件描述符的read系统调用)
解决的问题:
poll解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题
3. pollfds数组拷贝到了内核态,仍然有开销
4. poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历
epoll底层有三个函数:
epoll_create:创建一个epoll函数的句柄,int epoll_create(int size);
epoll_ctl:向内核添加,修改,或删除要监控的文件描述符
epoll_wait:发起了类似于select调用
epoll是非阻塞的
epoll执行流程:
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,
变成了一次系统调用 + 内核层遍历这些文件描述符。
epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小
2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket
在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈