Redis 笔记

本文详细介绍了Redis的数据结构、单线程模型的优势以及如何通过IO多路复用实现非阻塞I/O。重点讨论了Redis的持久化机制,包括RDB和AOF,以及它们的优缺点。此外,还阐述了Redis的主从复制、哨兵系统和集群的高可用性方案。最后,分析了Redis的删除策略、布隆过滤器以及缓存双写一致性更新策略。
摘要由CSDN通过智能技术生成

Redis

  • Redis数据结构

  • 在这里插入图片描述

为什么单线程还这么快

  • 第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
  • 第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如图26所示。
  • 在这里插入图片描述

bgsave的流程说明

  • 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RBD/AOF子进程,如果存在bgsave命令直接返回
  • 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞
  • 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
  • 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。
  • 子进程发送信号给父进程表示完成。
    在这里插入图片描述

RDB的优点

  • RDB是一个紧凑压缩的二进制文件。
  • Redis加载RDB恢复数据远远快于AOF的方式

RDB的缺点

  • RDB没法做到实时持久化

AOF

  • 以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的,默认不开启。
  • AOF的工作流程操作:命令写入(append),文件同步(sync),文件重写(rewrite),重启加载(load)
    在这里插入图片描述
  • 所有的写入命令会追加到aof_buf(缓冲区)中
  • AOF缓冲区根据对应的策略向硬盘做同步
  • 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的

AOF为什么把命令追加到aof_buf中?

Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘的负载。还有一个好处,Redis可以提供多种缓冲区同步硬盘的策略。

  • AOF缓冲区同步文件策略
  • always:命令写入aof_buf后调用系统fsync操作同步到AOF文件,fsync完成后线程返回
  • everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由专门线程每一秒调用一次,推荐使用
  • no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责

write和fsync说明:

write操作会触发延迟写机制,Linux在内核提供页缓冲区用于提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制:缓冲区页空间写满或达到特定时间周期
fsync针对单个文件操作,做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化

重写机制

  • AOF重写是把Redis进程内的数据转化为写命令同步到新AOF文件中
    在这里插入图片描述
  • 执行AOF重写请求
    如果当前进程正在执行AOF重写,请求不执行。
    如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行。
  • 父进程执行fork创建子进程
    主进程fork操作完成后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根据同步策略同步到硬盘。
    由于fork操作采用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,Redis使用"AOF重写缓冲区"保存这部分新数据,防止新AOF文件生成期间丢失这部分数据。
  • 子进程根据内存快照,按照命令合并规则写入到新的AOF文件
  • 新的AOF文件写入完成后,子进程发送信号给父进程
    父进程把AOF重写缓冲区的数据写入到新的AOF文件中
  • 使用新AOF文件替换老文件,完成AOF重写

AOF持久化开启且存在AOF文件时,优先加载AOF文件

写时复制父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的也创建副本,而子进程在fork操作过程中共享整个父进程内存快照

AOF追加阻塞

当开启AOF持久化时,常用的同步硬盘策略是everysec,对于这种方式,Redis使用另一条线程每秒执行fsync同步硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞。
在这里插入图片描述

  • 主线程负责写入AOF缓冲区
  • AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间
  • 主线程负责对比上次AOF同步时间
    如果距离上次同步成功时间在2秒内,主线程直接返回
    如果距离上次同步成功时间超过2秒,主线程阻塞,知道同步操作完成
  • everysec配置最多可能丢失2秒数据,不是1秒

Redis单线程架构导致无法充分利用CPU多核特性,通常的做法是在一台机器上部署多个Redis实例.

主从复制过程

在这里插入图片描述
通过复制机制,可以应用于读写分离,故障转移,实时备份

数据同步

  • 全量复制:一般用于初次复制场景,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销
  • 部分复制:用于处理在主从复制中因网络问题造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。(使用偏移量的算法)
    复制偏移量:参与复制的主从节点都会维护自身复制偏移量。主节点在处理完写入命令后,会把命令的字节长度做累加操作,从节点每秒上报自己的复制偏移量给主节点
    复制积压缓冲区:复制积压缓冲区是保存主节点上的一个固定长度队列,
    主节点运行ID:每个redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。运行ID主要作用是用来唯一识别Redis节点。如果只使用ip+端口的方式识别主节点,那么主节点重启变更了整体数据集(rdb,aof),从节点再基于偏移量复制数据是不安全的,因此当运行ID变化后从节点将做全量复制。

全量复制

全量复制是Redis主从第一次建立复制时必须经历的阶段。
在这里插入图片描述

  1. 发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点运行ID,所以发送psyn-1.
  2. 主节点根据psync-1解析出当前为全量复制,回复响应。
  3. 从节点接收主节点的响应数据保存运行ID和偏移量offset,
  4. 主节点执行bgsave保存RDB文件到本地
  5. 主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,无盘复制为了降低主节点磁盘开销,Redis支持无盘复制,生成的RDB文件不保存到硬盘而是直接通过网路发送给从节点。
  6. 对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端的缓冲区,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据的一致性
  7. 从节点接收完主节点传过来的全部数据后清空自身旧数据,
  8. 从节点清空数据后开始加载RDB文件
  9. 从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof

部分复制

在这里插入图片描述
当从节点正在复制主节点时,如果出现网络中断或者命令丢失的情况,从节点会向主节点要求补发丢失的命令,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点。

  1. 当主从节点之间网络中断,主节点会认为从节点故障并中断复制连接。
  2. 主从连接中断期,主节点会将响应的命令写入复制积压缓冲区
  3. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把他们当作psync参数发送给主节点,要求进行部分复制操作
  4. 主节点接收到psync命令后,首先核对参数运行ID是不是和自身一致,如果偏移量之后的数据存在缓冲区中,则对从节点发送响应,表示可以进行部分复制

心跳

主从节点建立复制后,他们之间维护着长连接并彼此发送心跳命令

  1. 主节点默认每隔10秒对从节点发送ping命令
  2. 从节点每隔一秒发送ack命令,给主节点上报自己当前的复制偏移量

读写分离

会出现如下问题

  • 复制数据延迟
  • 读到过期数据
  • 从节点故障
    复制数据延迟
    Redis复制数据的延迟由于异步复制特性是无法避免的,延迟取决于网络的带宽和命令阻塞情况,比如刚在主节点写入数据后立刻在从节点上读取可能获取不到。需要业务场景允许短时间内的数据延迟。可以编写外部监控程序监听主从节点的复制偏移量,当延迟较大时触发报警,或者通知客户端从主节点读取数据
    读到过期数据
    当主节点存储大量设置超时的数据时,如缓存数据,Redis内部需要维护过期数据删除策略。惰性删除定时删除
  • 惰性删除:主节点每次处理读取命令时,都会检查key是否超时,如果超时则执行del命令删除key对象。之后del命令也会异步发送给从节点,需要注意的是为了保证复制的一致性,从节点永远不会主动删除超时数据
  • 定期删除:Redis主节点在内部定时任务会循环采样一定数量的key,当发现采样的key过期时执行del命令,之后再同步给从节点。如果此时数据大量超时,主节点采样速度跟不上过期速度,且主节点没有读取过期key的操作,那么从节点将无法收到del,这时从节点可以读取到已超时的数据。Redis3.2通过从节点读取数据时检查key的过期时间来解决问题。
  • 从节点故障,需要在客户端维护可用从节点列表,当从节点故障立刻切换到其他节点上。
  • 在做读写分离之前,可以考虑使用分布式集群解决

ziplist

特点

  • 内部表现为数据紧凑排列的一块连续内存数组
  • 可以模拟双向链表结构,以O(1)的时间复杂度入队和出队
  • 新增删除操作涉及内存重新分配或释放,加大了操作的复杂性
  • 读写操作涉及复杂的指针移动,最坏时间复杂度为O(n2)
  • 适合存储小对象和长度有限的数据

主从复制的

  • 一旦主节点出现了故障,可以将从节点升级为主节点,并且保证数据尽量不丢失(主从复制是最终一致性)
  • 从节点可以扩展主节点的读能力,一旦主节点不能支撑住大并发量的读操作,从节点可以分担

哨兵

Redis主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景是无法接受的,所以引入哨兵模式(Sentinel)

  • Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行协商,当大多数Sentinel节点都认为主节点不可达时,他们会选出一个Sentinel节点完成故障转移的工作。

哨兵实现原理

三个定时监控任务

  1. 每隔10s,每个Sentinel节点会向主节点喝从节点发送info命令获取最新的拓扑结构
  2. 每隔2s,每个Sentinel节点会向Redis数据节点的hello频道上发送该Sentinel节点的判断以及当前Sentinel节点信息,同时每个Sentinel也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断,
  3. 每隔1s,每个Sentinel节点会向主节点,从节点,其余Sentinel节点发送一个ping命令做一次心跳检测,来确认这些节点当前是否可达。
  • 主观下线
    每个Sentinel节点会每隔1s对主节点,从节点,其他Sentinel节点发送ping命令做心跳检测,当这些节点超过一定的时间没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线,从字面意思也可以很容易看出主观下线是当前Sentinel节点的一家之言,存在误判的可能。
  • 客观下线
    当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过命令向其他Sentinel节点询问对主节点的判断,当超过一定的数量,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分Sentinel节点都对主节点做了同意的判定,那么这个判定就是客观的。
  • 领导者Sentinel节点选举
    假如Sentienl节点对于主节点已经做了客观下线,那么是不是就立即进行故障转移呢?当然不是,实际上故障转移的工作只需要一个Sentinel节点来完成即可,所以Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移。Redis使用了Raft算法实现领导者选举。
  1. 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点客观下线时,会向其他Sentinel节点发送命令,要求将自己设为领导者
  2. 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的命令,将同意该请求,否则拒绝
  3. 如果该Sentinel节点发现自己的票数已经大于一般了,那么它就将成为领导者
  4. 如果次过程没有选举出领导者,将进入下一个纪元(epo+1)

集群

数据分布理论

  • 节点取余分区
    使用特定的数据,如key,再根据节点数量N使用公式:hash(key)%N计算出hash值,这种方案存在一个问题:当节点数量变化时,如扩容或者收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
  • 一致性hash
    一致性hash实现思路是为系统中每个节点分配一个token,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该hash值的token节点。
    在这里插入图片描述这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性hash存在几个问题:
  1. 加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性hash常用于缓存场景。
  2. 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
  3. 普通的一致性hash分区在增加一倍或减去一半节点才能保证数据和负载的均衡
  • 虚拟槽分区
    Redis Cluser采用虚拟槽分区,所有的key根据hash函数映射到0-16383整数槽内,计算公式:slot=CRC16(key) & 16383.每一个节点负责维护一部分槽已经

====================================================================================================

1. redis的单线程与多线程

redis 3.x 是单线程: 1.数据结构简单 2避免锁的开销和上下文切换 3 可以有很高的qps
redis 6.x 之后告别了单线程,用一种全新的多线程来解决问题

2. redis单线程的问题

由于是单线程所以每个命令都是排队执行的,当前一个命令是处理一个bigkey(删除一个非常大的对象,那么del命令就会造成redis主线程卡顿)的时候,会造成阻塞,特别是删除一个bigkey的时候,所以在redis 4.x版本后引入了多线程异步删除
怎么处理删除一个大key的问题?可以采用惰性删除
删除bigkey可以用 unlink 命令 (把删除操作交给了子线程异步来操作)

3.redis单线程为什么快

  1. 采用内存
  2. 数据结构简单
  3. 多路复用和非阻塞I/O:redis使用IO多路复用功能来监听多个socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了IO阻塞操作
  4. 避免上下文切换和锁的开销

4.多线程IO多路复用

  • Unix网络编程中的五种IO模型:
  1. Blocking IO - 阻塞IO
  2. NoneBlocking IO - 非阻塞IO
  3. IO multiplexing - IO多路复用
  4. signal driven IO - 信号驱动IO
  5. asynchronous IO- 异步IO

IO多路复用
这是IO模型中的一种,Reactor模式,IO多路复用简单来说就是通过监测文件的读写事件再通知线程执行相关操作,保证redis的非阻塞IO能顺利执行完成的机制
多路指的是多个socket连接
复用指的是复用一个线程,多路复用主要有三种技术:selectpollepoll
IO的读与写本身是阻塞的,比如当socket中有数据时,redis会通过调用先将数据从内核态拷贝到用户态空间,再交给redis调用,而这个拷贝的过程是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
在这里插入图片描述
多线程化只是将网络IO的部分进行多线程,而执行操作还是只有一个主线程来执行。
在这里插入图片描述
redis6.x 多线程是默认关闭的,需要配置conf文件进行设置

布隆过滤器bloomfilter

问题:现在有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
处理办法

  1. 对于热点的key,不设置失效时间
  2. 设置互斥锁,多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住他,其他线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存,后面的线程进来发现已经有缓存了,就直接走缓存。

redis分布式锁

可以用setnx 设置一个全局的key,设置为加锁,操作完成之后再del
分布式锁所具备的条件和刚需:

  1. 独占性:onlyone,任何时刻只能有且仅有一个线程持有
  2. 高可用:在redis集群的环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
  3. 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案。
  4. 不乱抢:不能私下unlock别人的锁,只能自己加锁自己释放
  5. 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁

使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求,防止关键业务出现并发攻击

redis删除策略

  1. 立刻删除,立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值对会在过期后马上删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上cpu很忙的时候。会产生大量的性能消耗,同时也影响数据的读取操作。对cpu不友好但是对内存友好
  2. 惰性删除,有可能导致过期的键永远都没有被删除,所以需要定期删除
  3. 定期删除,定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对cpu的影响。周期性轮询redis库中的失效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度。

定期删除策略是立刻删除和惰性删除的折中,但是定期删除的频度设置会导致其退化成前两种,所以有了缓存淘汰策略八种
在配置文件中
四个方面:LRU, LFU, random, ttl 分别对全部键和设置了过期时间的key
redis默认的是noevciction:不会驱逐任何key ,一般采用 allkeys-lru :即是对所有的key采用lru算法

底层结构

set hello world 这个命令,因为redis是kv键值对的数据库,每个键值对都会有一个dicEntry来存储
在这里插入图片描述
redisObject对象
在这里插入图片描述

SDS简单动态字符串

在这里插入图片描述

结构:包含 len,alloc,flags,buf[]四个字段
优点

  1. 字符串的长度可以用O(1)的复杂度获取
  2. 当用append命令的时候会导致数组越界:空间预分配,len长度小于1M,那么将会额外分配与len相同长度的未使用空间。如果修改后长度大于1M,那么将分配1M空间。惰性空间释放,sds缩短时并不会回收空间,而是使用free字段将多出来的空间记录下来。如果后续有变更的操作,直接使用free中记录的空间,减少了内存的分配。
  3. 二进制安全,c语言中是用‘\0’为结束符,如果保存的字符中间出现该字符,那么无法保存完整的字符
    小于10000以内的是共用的int 对象

对于长度小于44的字符串,Redis对键值采用embstr,embstr表示:embedded string,表示嵌入式的string,从内存结构上来讲,即字符串sds与其对应的redisObject对象分配在同一连续的内存空间,字符sds嵌入在redisObject对象之中。这样连续紧凑之后,可以减少内存碎片。

在这里插入图片描述
如果是raw,则内存不再连续。一旦对key做修改,就直接改变为raw类型
在这里插入图片描述

hash

底层由 ziplist+hashtable
由默认的 hash-max-ziplist-entries 和 hash-max-ziplist-value 两个参数

ziplist

ziplist压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间。即以部分读写性能为代价,来换取极高的内存空间利用率,因此只会用于字段个数比较少,且字段值也比较小的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。,也就是减少碎片。利用压缩来保证内存的连续。
在这里插入图片描述
ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和下一个链表节点的指针,而是存储上一个链表长度和当前节点长度。
在这里插入图片描述

为什么有了双向链表还需要ziplist

  1. 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不尝试
  2. 链表在内存中一般是不连续的,遍历比较慢,而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素移动sizeof(int),而ziplist存储的每个节点的长度是不一样的。面对不同长度的节点又不能直接sizeof(entry),
  3. 头节点里有个参数len,不需要遍历双向链表获取长度

在这里插入图片描述

缓存双写一致性之更新策略探讨

对双写一致性的理解

  1. 如果redis中无数据那么数据库中的值应该是最新值
  2. 如果redis中有数据,需要和数据库中的值相同
    对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略。mysql改变,redis也改变。
  • 什么时候同步直写?
    小数据,某条,某一小撮热点数据,要求立刻变更,可以前台服务降级一下,后台马上同步直写。

  • 什么时候异步缓写?
    1 正常业务,马上更新mysql,可以在业务上容许出现一个小时后redis起效。
    2 出现异常后,不得不将失败动作重新修补。

  • 数据库和缓存一致性的几种更新策略?

给缓存设置过期时间,是保证最终一致性的解决方案。

  1. 先更新数据库,再更新缓存

先更新mysql某件商品为99,然后更新redis为99,这时候如果redis出现异常了(或者redis更新操作慢了一点),导致redis还是100,最终导致mysql99,redis100,redis会读取到脏数据。

  1. 先删除缓存,再更新数据库

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会导致业务吞吐量降低,可以再开一个守护线程去删除数据。

  1. 先更新数据库,在删除缓存

A线程更新数据库,还没来得及删除缓存,B就来读取了,这时候就会造成B线程读取到脏数据。

处理办法

第2种方法读取到的是mysql中的脏数据,而第3种是从redis种读取旧数据,第3种方案更好,理由如下:
1 先删除缓存再更新数据库,有可能导致请求请求因缓存缺失而访问到数据库,给数据库带来压力,严重导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间不好设置

如果使用第三种方法,如果业务层要求必须读取一致性的数据,那么我们就需要再更细数据库时,先在redis缓存客户端暂存并发读请求,等数据库更新玩,缓存值删除后,再读取数据,从而保证数据一致性。

redis为什么这么快?高性能设计之epoll和IO多路复用深度解析

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三个阶段。

同步与异步的理解:同步,异步的讨论对象是服务提供者,重点在于获得调用结果的消息通知方式上。
阻塞与非阻塞的理解:阻塞,非阻塞的讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其她事情。

  • BIO 阻塞IO

当用户进程调用了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 非阻塞IO

在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多路复用

IO多路复用就是我们说的select,poll,epoll,有些地方也称这种IO方式为事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
IO多路复用快的原因在于,操作系统提供了这样的系统调用,是的原来的while循环里多次系统调用,变成了一次系统调用+内核层遍历这些文件描述符

将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select,poll,epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。

  • 什么是reactor模式

基于IO多路复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

reactor模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,reactor模式也叫dispatcher模式。即IO多路复用统一监听事件,收到事件后分发给某个进程,是编写高性能网络服务器的必备技术。
在这里插入图片描述
reactor模式中有2个关键组成:

  1. Reactor: reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
  2. Handlers:处理程序执行IO事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际办理人。reactor通过调度适当的处理程序来响应IO事件,处理程序执行非阻塞操作。

在这里插入图片描述

  • select

所谓的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函数的执行过程:

  1. select是一个阻塞函数,当没有数据时,会一直阻塞在select那一行
  2. 当有数据时会将rset中对应的那一位置为1
  3. select函数返回,不再阻塞
  4. 遍历文件描述符数组,判断哪个fd被置位了
  5. 读取数据,然后处理

其实select就是在内核中运行accept,read等操作。首先会设置一个bitmap类型的rset,然后阻塞在select,当有数据的时候,我们就设置对应的rset位置为1,然后在一个for循环执行read等操作。

select其实就是把NIO中用户态要遍历的fd数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所以拷贝到内核态后,这样的遍历判断的时候就不用一直用户态和内核态频繁切换了

从代码中可以看出,select系统调用后,返回了一个置位后的rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率。
在这里插入图片描述
select函数的缺点:

  1. bitmap默认大小为1024,虽然可以调整但还是有限度的
  2. rset每次循环都必须重新置位为0,不可重复使用
  3. 尽管将rset从用户态拷贝到内核态,由内核态判断是否有数据,但是还是有拷贝的开销
  4. 当有数据时select就会返回,但是select函数并不知道哪个文件描述符有数据了,后面还需要再次对文件描述符数组进行遍历。效率比较低

select方式,即做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次select的系统调用+N次就绪状态的文件描述符的read系统调用)

  • poll方法
  1. 将五个fd从用户态拷贝到内核态
  2. poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置为POLLIN
  3. poll方法返回
  4. 循环遍历,查找哪个fd被置位为POLLIN了
  5. 将revents重置为0 便于复用
  6. 对置位的fd进行读取和处理

在这里插入图片描述

解决的问题:

  1. 解决了bitmap大小限制
  2. 解决了rset不可重用的情况

poll解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题
3. pollfds数组拷贝到了内核态,仍然有开销
4. poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历

  • epoll

epoll底层有三个函数:
epoll_create:创建一个epoll函数的句柄,int epoll_create(int size);
epoll_ctl:向内核添加,修改,或删除要监控的文件描述符
epoll_wait:发起了类似于select调用

epoll是非阻塞的
epoll执行流程:

  1. 当有数据的时候,会把相应的文件描述符“置位”,但是epoll没有revent标识位,所以并不是真正的置位,这时候会把有数据的文件描述符放到队首
  2. epoll会返回有数据的文件描述符的个数
  3. 根据返回的个数 读取前N个文件描述符即可
  4. 读取处理

在这里插入图片描述
在这里插入图片描述

  • 事件通知机制
  1. 当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中
  2. 网卡向cpu发起中断,让cpu先处理网卡的事
  3. 中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中

多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 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性能的瓶颈

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值