拿图就走系列之《Redis 核心技术与实战》

发布于:2022-11-01 ⋅ 阅读:(3234) ⋅ 点赞:(2)

welcom to redis

内存数据库

文章内容全部来自极客时间 蒋德钧老师的 《Redis 核心技术与实战》

我仅仅做了文章内容的笔记和整理工作,强烈推荐用个新用户七天会员去看一下蒋德钧老师的文章,包括评论区里面有很多厉害的人,都值得学习

一、Redis的数据结构

  • 单元素操作:指每一种集合类型对某个单个数据实现crud。

    • 正常单个元素:采用哈希表操作,复杂度O(1)

    • 集合支持对多个元素操作,复杂度增长为O(N)

  • 范围操作:指集合类型的遍历操作,可以返回集合中所有数据

    • 复杂度O(N),耗时
  • 统计操作:对集合中的元素个数进行记录

    • 采用 压缩列表、双向链表、整数数组,时间复杂度 O(1)
  • 特殊记录:利用压缩列表和双向链表记录头尾偏移量的特点,实现快速操作O(1)

五种数据形式

  • string:简单动态字符串

  • list:双向链表 压缩列表

  • hash:压缩列表 哈希表

  • Sorted Set:压缩列表 跳表

  • set:哈表表 整数数组

底层实现结构

  • 简单动态字符串

  • 双向链表

  • 压缩列表

  • 哈希表

  • 跳表

  • 整数数组

键值使用什么结构组织

小总结:redis采用哈希表作为键值对存储的结构,由于会产生哈希冲突,于是采用拉链哈希解决,由于拉链哈希会导致查询速度降低,于是采用rehash的方法,进行扩容减少冲突,由于一次性拷贝会占用线程,造成阻塞,于是采用渐进式rehash

哈希表,表中元素称为哈希桶,每个哈希桶保存了键值对数据

哈希桶中的元素保存的是指向具体值的指针,entry指向实际的key value

优点:查询复杂度是O(1),只需要计算哈希值 就能找到桶的位置

缺点:哈希冲突和rehash

  • 哈希冲突:当存入哈希表的数据不断增多,就会产生多个key落在了同一个哈希桶内

    • 解决办法:拉链哈希。指同一个哈希桶中的多个元素用一个链表保存,之间一次用指针连接

    • 问题:当拉链的长度越来越长,会导致查询的速度降低,所以哈希表会做rehash的操作

  • rehash:增加现有的哈希桶数量,让增多的元素在更多桶之间保持分散保存,降低一个桶内拉链的长度

    • 实际操作:redis默认使用了两个全局哈希表,表1和表2

      • 普通哈希:表1数据过多时,表二分配更大的空间,表1的数据重新映射并拷贝到表2,释放表1的空间,当表2满时,同样操作,如此反复

        • 缺点:大量数据拷贝会造成线程阻塞
      • 渐进式rehash:将表二分配更大空间后,采用【当redis每次处理一个请求时,就从哈希表第一个索引开始,将索引位置的所有entry拷贝到哈希表2中,如此渐进的过程】,解决了一次大量拷贝数据的开销,分摊到了多次请求,保证数据的快速访问

集合类型的底层数据结构

整数数组和压缩列表复杂度高于链表仍被使用的原因

  • 在于二者是非常紧凑的数据结构,所占内存比链表更少,可以提高内存的利用率;

  • 同时对CPU高速缓存支持更友好,redis设计时,采集合数据元素较少时,默认采用内存紧凑排列方式存储,利用高速缓存不会降低访问速度,当数据元素超过设定阈值,避免复杂度过高转为哈希表和跳表存储

压缩列表

类似于数组,只是在压缩列表头有三个字段zlbytes(列表长度)、zltail(列表尾偏移)和zllen(列表中entry的个数),尾部还有一个zlend(列表结束)

查找头尾时具有O(1)的复杂度,其他为O(N)

跳表

在链表的基础下增加多级索引,通过索引位置的几个跳转实现了快速定位

复杂度O(logN)

二、高性能IO模型

Redis的单线程:指的是Redis的网络IO键值对读写是由一个线程完成,这也是Redis对外提供键值存储服务的主要流程

其他功能(持久化、异步删除、集群数据同步等):是通过额外的线程执行

为什么采用单线程

多线程会产生开销

多线程编程模式面临的共享资源的并发访问控制问题:共享资源会被多个线程同时访问,为了保证共享资源的正确性,会采用额外机制(如锁),带来额外开销,为了避免并发访问控制出现问题,降低系统代码的易调试性和可维护性,于是采用单线程

为什么单线程那么快

  • 采用哈希表和跳表这种高效的数据结构

  • 多路复用机制:使其在网络IO操作中能并发处理大量客户端请求,实现高吞吐率

基本IO模型与阻塞点

基本IO操作

  • bind/listen:金婷客户端请求

  • accept:和客户端建立连接

  • recv:从socket中读取请求

  • parse:解析客户端发送请求

  • get(或其他键值数据操作):根据请求类型读取键值数据

  • send:向socket写回数据,返回结构给client

存在的阻塞点

  • accept:监听连接请求,但未成功建立连接,会阻塞在accept()函数,导致无法建立连接

  • recv:获得数据时,数据一直未到达,也会阻塞在recv()函数

发生线程阻塞就无法处理其他客户端请求,但是socket本身支持非阻塞模式

非阻塞模式

accept时:当监听一直未有连接到达时,线程可以返回处理其他操作,而不用等待

同理,recv也可以不用一直等待

但是redis需要一个机制去监听套接字,在有数据到达时通知Redis

IO多路复用

  • linux中指一个线程处理多个IO流程,就是select/epoll机制

  • 在Redis单线程情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字,内核会一直监听套接字的连接请求或数据请求,一旦有请求到达,就会交给Redis线程处理

  • select/epoll机制提供了基于事件的回调机制,即针对不同事件发生,调用相应的处理函数

    • 回调机制:当select/epoll监听到FD(监听套接字)上有请求到达时,就会触发相应的事件。事件会被放进一个事件队列,单线程对该事件队列进行不断处理,就实现了基于事件的回调

Redis单线程处理IO的请求性能瓶颈

1、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。

耗时的操作包括以下几种:

  • a、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;

  • b、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;

  • c、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;

  • d、淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;

  • e、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能; f、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;

2、并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。

三、AOF日志

实现持久化的目的:防止服务器宕机,内存中的数据全部丢失

为什么不从后端数据库恢复数据

  • 频繁访问给数据库带来巨大的压力

  • 从慢数据库中读取数来,性能不比Redis

持久化的两大机制

  • AOF日志

  • RDB快照

AOF日志如何实现

AOF是写后日志:即在Redis命令执行,把数据写入内存后,再记录日志

为什么不是写前日志:因为AOF不会对命令进行语法检查,一旦命令出错,则会记录错误的命令,写后可以避免这种事情的发生;并且不会阻塞当前的写操作

AOF记录的内容:Redis收到的每一条指令,以文本形式保存

潜在风险:由于写回磁盘的时机产生

  • 由于是写后操作,不会阻塞当前命令,但是可能由于AOF在主线程中执行,日志写入磁盘时,会造成写盘很慢,导致后续的操作无法执行,给下一个操作带来阻塞风险

  • 命令执行完直接宕机,命令和数据由丢失的风险

三种写回策略

appendfsync配置项的三个可选值

  • Always:同步写回:每个写命令执行完,立刻将日志同步写回磁盘

    • 优点:做到基本不丢数据

    • 缺点每个命令后都有慢速的落盘操作,影响主线程性能

  • Everysec:每秒写回:每个写命令执行完,先将日志写到AOF文件的内存缓冲区每隔一秒把缓冲区的内容写入磁盘

    • 优点:性能适中,因为每一秒的写回一次的频率

    • 缺点:如果上一秒发生宕机,未落盘的命令操作会丢失

  • No:操作系统控制写回:每个写命令执行完,先将日志写到AOF文件的内存缓存区中,由操作系统决定何时将缓存区内容写回磁盘

    • 优点:性能好,写回到缓存区后,就可以继续执行后续命令

    • 缺点:落盘时机不在Redis手中,只有AOF记录没有写回磁盘,宕机后数据就会丢失

AOF重写机制

触发条件:手动发送bgrewriteaof指令或配置自动触发

流程:Redis会先从主线程中fork一个子线程,用于重写;之后用第一日志记录主线程新的写操作;之后用第二个日志(重写日志)进行新文件替换旧文件

随着AOF文件的不断增大,会导致性能问题,采取一定的控制手段,就是AOF重写机制

AOF重写机制:Redis根据数据库的现状创建一个新的AOF文件,读取数据库中的所有键值对,根据每一个键值对用一条命令记录它的写入

优势:具有多变一的功能,将旧日志文件的多条命令合并成一条命令;重写时,根据键值对的最新状态,为他生成对应的写入命令,因此一个键值对只需要一条命令,减少了多次重复修改带来的空间浪费

AOF重写阻塞

AOF日志由主线程写回,而重写过程是由后台子进程bgrewriteaof完成的,这样避免了主线程阻塞

重写过程:一个拷贝,两处日志

  • 一个拷贝

    每次重写时,主线程fork出后台的子进程,并将主线程的页表拷贝给子进程(子进程复制父进程页表(虚拟内存和物理内存的映射索引表),共享访问父进程的内存数据,),然后子进程在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志

  • 两处日志

    • 第一处日志:在主线程未阻塞,处理新来的操作时,如果有写操作,第一处日志就是正在使用的AOF日志,Redis会把操作写到日志的缓冲区,这样即便宕机日志也是齐全的,用于恢复

    • 第二处日志:AOF重写日志,操作会被写到重写日志的缓冲区,这样重写日志就不会丢失最新的操作,等到拷贝数据的所有操作记录重写完成后,重写日志记录的最新操作也会写入新的AOF文件,以保证数据库最新的状态记录。此时可用新的AOF文件替代旧的文件。

AOF重写的潜在风险

  • Redis采用fork子进程重写AOF文件

    • fork的瞬间一定会阻塞主线程,而且并不是一次拷贝所有的数据给子进程。

    • 为了避免一次拷贝大量内存数据给子进程造成的长时间阻塞问题,fork采用由系统提供的写实复制机制

    • fork子进程需要拷贝必要的数据结构:其中一项就是拷贝内存页面(虚拟内存和物理内存的映射索引表)这个过程会大量消耗cpu资源,拷贝完成之前进程会阻塞,阻塞时间取决于实例的内存大小

  • AOF重写为什么不复用本身的日志

    • 父子进程写同一个文件会产生竞争问题,控制竞争必然会导致影响父进程的性能

    • 如果重写失败,将会污染原本的AOF文件,无法做恢复使用。重写一个新文件,如果失败,可以直接删除,等待重写成功后再替换旧文件

写时复制

写时复制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思 想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他 们会相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真 正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保 持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资 源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份 资源。

四、内存快照RDB

由于AOF日志记录的是操作命令,每次恢复时需要逐一将操作执行,如果日志非常多,Redis就会变得缓慢,进而影响性能

为了解决宕机后恢复慢的问题,另一种持久化机制内存快照被推出

RDB(Redis DataBase):把某一时刻的状态以文件的形式写到磁盘,记录的是某一个时刻的数据而不是操作,这样恢复时就会直接读写文件入内存,快速恢复

使用RDB的两个关机问题

  • 对哪些数据做快照--执行效率问题

  • 做快照时,数据还能被crud么?--Redis是否被阻塞,能否正常处理请求

给哪些内存数据做快照

为了保证提供所有数据的可靠性,Redis执行全局快照,将内存中的所有数据都记录在磁盘中。与此同时,如果数据过大,RDB过大,往磁盘上写的时间开销会增大,所有我们要避免在主线程操作

Redis提供了两种命令生成RDB文件

  • save:在主线程执行,会导致阻塞

  • bgsave:创建一个子进程,专门用于写入RDB文件(默认配置)

借助bgsave创建子线程的方式,就可以在提供数据的可靠性的同时,避免了对Redis性能的影响

快照时数据可以修改吗

为了保证快照的完整性,主线程只能处理读操作,因为不能修改正在执行快照的数据;但是如果暂停写操作,也是不能被接受的,所以会借助操作系统提供的写时复制技术,在执行快照的时候,正常处理写操作。

  • 在主线程进行读操作时,和子进程互不影响

  • 在主线程进行写操作时,修改的数据就会被复制一份,生成副本,然后主线程修改数据副本,同时子进程可以继续把未修改的数据写入RDB文件

多久一次快照

可以每秒一做么?

可以,理论上时间越短,发生宕机时丢失的数据越少;但是频繁的执行全量快照会造成两方面的开销

  • 频繁的将全量数据写入磁盘,会加大磁盘压力,多个快照竞争有限的磁盘带宽,会导致前面还没结束,后面又开始做了,造成恶性循环

  • fork子线程的过程本身会阻塞主线程,主线程的内存越大,阻塞时间越长;如果频繁fork就会频繁阻塞主线程,所以在Redis中有一个子进程执行,就不会启动第二个子进程

对于时间管控导致的开销,Redis提供了增量快照

增量快照:在一次全量快照后,后续的快照只对修改的数据进行快照记录。因此我们需要记住哪些数据被修改(记录过程将使用额外的元数据信息去记录,带来额外的空间开销),如果更改的数据过多,导致额外的开销过大,将会浪费内存

混合使用AOF和RDB

为了解决两者存在的性能和空间浪费的问题,Redis4.0提出了混合使用

定义:RDB以一定的频率执行,在两次快照之间用AOF日志记录期间内的所有命令操作

五、数据同步

Redis提供主从库模式保证数据副本的一致,主从库之间采用读写分离的方式

  • 读操作:主库从库都可以接受

  • 写操作:主库先执行,主库将写操作同步给从库

为什么采用读写分离?

  • 答:为了保证数据一致,如果给不同的Redis实例都发送不同的写请求,那么不同实例的副本就会不一致,那么就可能读取到旧的值;如果用加锁、协商等方式,会带来巨大的开销。所以采用读写分离,数据修改只会在主库上进行,不用协调其他实例,主库有新的数据后,会同步给从库,保证了主从一致

主从库间如何进行第一次同步

在从库上指定主库 relicaof ip地址 端口号,这样主从库便有了

建立连接的三个阶段

  • 主从库建立连接协商的过程,为全量复制做准备。

    • 主库从库建立连接,并告诉主库即将进行同步,主库确认回复后,就可以开始同步

    • 具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。 runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。 offset,此时设为 -1,表示第一次复制。 主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。 这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

  • 主库将所有数据同步给从库,从库收到后,在本地完成数据加载

    • 具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。

    • 从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。

    • 在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。

  • 主库将第二阶段收到的新命令再发送给从库。

    • 主库完成RDB文件发送后,就会发送replication buffer中的修改操作发给从库,从库在执行操作

主从级联模式

由于主库需要完成两个耗时工作:生成RDB文件和传输RDB文件

如果从库过多,都需要主库进行全量复制的话,就会导致主库忙于生成RDB文件,机械能数据全量同步,fork操作也会阻塞主线程正常的处理请求,从而影响主库响应速度,并且传输RDB文件也会占用主库网络带宽,造成资源使用压力,所以需要新的模式

主从从模式,将主库生成RDB文件和传输的压力,以级联的方式分散到从库上

  • 部署集群时,我们可以手动选择一个内存资源高的从库,用于级联其他从库,然后再选取一些其他从库,在这个从库下面执行命令,建立起主从关系,这样就不需要这些其他从库和主库交互,从而减轻主库压力

当从主库完成全量复制后,会一直维护一个网络连接,主库会把后续的命令同步到从库,这个过程也叫基于长连接的命令传播,避免频繁建立连接的开销 --带来的问题:网络断连或者阻塞了要怎么办?

主从库间网络断连怎么办

增量复制:当主从库断连后,主库会将断连期间的收到的写操作写入replication buffer,同时写入repl_backlog_buffer(是一个环形缓冲区,主库会记录自己写到的位置,从库记录自己已经读到的位置)缓冲区中。主库从库分别有自己的偏移量(主:master_repl_offset 从:slave_repl_offset)去记录写操作的命令。

主从库的连接恢复之后

  • 从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库

  • 主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。 在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。

  • 此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。

  • 就像刚刚示意图的中间部分,主库和从库之间相差了 put d e 和 put d f 两个操作,在增量复制时,主库只需要把它们同步给从库,就行了。

repl_backlog_buffer用于主从间的增量同步,是为了从库断开之后,找到主从之间的差异数据而设计的环形缓冲区,避开了全量同步的开销。如果从库长时间断连,主库的写命令将会覆盖,从库只能做全量同步

replication buffer用于主节点和各个从节点的数据批量交互。为了找到主从之间的数据差距后,发送给从库。Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。

主从全量同步使用RDB而不使用AOF的原因

RDB文件是压缩的二进制数据,文件很小

AOF是每一次写操作的命令,还包含同一个key的多个冗余操作

传输时:RDB文件可以降低对主库网络带宽的消耗,从库加载因为文件小会加快读取,并且因为RDB是二进制数据,从库可以安装RDB协议直接解析还原数据;而AOF需要依次重放每个命令,过程冗长,恢复速度慢

六、哨兵机制

为什么要有太监,因为怕皇上挂了没人通知!!!

如果主库发生宕机,那么从库无法进行数据同步,而且写操作也会中断

于是Redis需要找一个从库当新的主库,这个时候就有了三个问题

  • 如何判断主库真的挂了

  • 选择哪个从库当主库

  • 怎么把新的主库信息通知给从库和客户端

这个时候哨兵机制就发挥作用了

基本流程

主要负责三个任务

  • 监控

    • 哨兵运行时,周期的给所有主从库发送PING命令,判断是否挂了

    • 如果在规定的时间内主从库没有响应,就标记为下线状态(主库下线会开始自动切换主库的流程)

  • 选主

    • 从众多从库里,按照一定规则选择一个实例,作为新的主库
  • 通知

    • 将新主库的连接消息发送给其他从库,让他们执行relicaof命令,和主库建立连接,并进行数据复制,并将连接信息通知给客户端,让他发送请求到新的主库上

主观下线和客观下线

主观下线:哨兵发现主从库的响应超时,那么便会标记为主观下线

客观下线:由于单个哨兵检测有误判,导致不必要的开销,于是搭建哨兵集群进行多次判断,只有大多数哨兵实例都判断主观下线,则标记为客观下线,开始主从切换流程

如何选定新主库

筛选+打分:根据一定的筛选条件,把不符合条件的从库去掉,然后按照一定的规则,给剩下的从库逐个打分,打分高的为新主库

筛选条件

  • 判断之前的网络连接状态,如果断连超过一定的阈值,证明从库网络状态不好,则筛选掉。可以根据配置项down-after-milliseconds*10来判断,如果在这个时间内主从都没有联系上,就可以认为主从节点断连,超过10次则不适合当新主库

打分分三轮

  • 优先级最高的从库得分高

    • 根据slave-priority配置项,设置不同的优先级
  • 和旧主库同步程度最接近的从库得分高

    • 就是master_repl_offset 和 slave_repl_offset 之间的差距接近的
  • ID号小的从库得分高

    • 默认编号,当优先级和复制进度相同情况下,ID号最小的从库得分最高

哨兵的使用

      a:主库下线,**可读不可写**,写失败的时间=哨兵切换主从的时间+客户端感知新主库时间
        b:主库下线无感知,需要客户端与哨兵配合改造:
                  1:哨兵主动通知:哨兵需要将最新的主库地址写入自己的pubsub中,客户端需要订阅这个pubsub,当这个pubsub有数据时,客户端就能感知到
                  2:客户端主动获取:客户端不将主从库写死,而是从哨兵集群中获取,从而始终获取最新的主从地址
        c:集群分片模式的Redis集群,可以不使用哨兵机制

哨兵的使用:(感谢 @Kaito 大神简洁明了,无私的分享) a:主库下线,可读不可写,写失败的时间=哨兵切换主从的时间+客户端感知新主库时间 b:主库下线无感知,需要客户端与哨兵配合改造: 1:哨兵主动通知:哨兵需要将最新的主库地址写入自己的pubsub中,客户端需要订阅这个pubsub,当这个pubsub有数据时,客户端就能感知到 2:客户端主动获取:客户端不将主从库写死,而是从哨兵集群中获取,从而始终获取最新的主从地址 c:集群分片模式的Redis集群,可以不使用哨兵机制

七、哨兵集群

部署多个实例,形成了一个哨兵集群

哨兵之间本身是没有关系的,全靠着发布/订阅机制才完成了相互的通信

基于pub/sub机制的哨兵集群组成

只要哨兵和主库建立起了连接,就可以在主库上发布消息(如自己的连接信息),同时也可以从主库上订阅消息,获得其他哨兵发布的连接信息,这样他们之间就可以互相通信了。

pub/sub机制为了对消息进行分类的管理,设置了频道,当消息类别相同时就属于同一个频道。只有订阅了同一个频道的应用,才能通过发布消息进行信息交换

在主从集群上哨兵互相通信的频道是:__sentinel__hello频道

哨兵如何知道从库的连接信息

通过哨兵向主库发送INFO命令完成的,主库收到这个命令就会返回从库列表给哨兵,接着哨兵就可以根据连接信息连接上从库,从而进行监控

基于pub/sub机制的客户端事件通知

客户端可以订阅哨兵的指定频获得redis主从库的信息

由哪个哨兵执行主从切换

谁发现谁就发起投票流程,谁获得的票多谁就是哨兵Leader,由哨兵Leader负责主从切换

Leader选举是否成功,与网络通信状况有关,网络拥塞会导致选举失败,重新选举

八、切片集群

九、旁路缓存

旁路缓存:读取缓存、读取数据、更新缓存的操作都需要在应用程序中完成

缓存的特征

  • 在一个层次化的系统中,缓存一定是一个快速子系统,数据存在缓存中,能避免每次从慢速子系统中存取数据

  • 缓存系统的容量大小总是小于后端慢速系统的,不可能把所有数据都放在缓存中

Redis缓存处理请求的两种情况

缓存命中:Redis中有相应的数据,直接读取,性能很快

缓存缺失:Redis中没有相应的数据,从后端数据库中读取,性能变慢,而且一旦发生缓存缺失,为了让后续请求能从Redis中读取,我们还需要将缺失的数据写入Redis,这叫缓存更新

缓存的类型

只读缓存

只进行数据读取的操作

如果发生缓存缺失,应用会把这些数据从数据库中读出来并写到缓存中

这样更新数据的好处时,最新的数据都在数据库中,数据库提供可靠性保障的,不会有丢失风险

读写缓存

除了读请求还有写请求也会发到缓存中,在缓存中对数据进行修改

在更新数据时,最新的操作是在Redis中,如果宕机,内存中的数据就会丢失

为了根据业务应用对数据可靠性和缓存性能的不同要求,有两种策略

  • 同步直写:优先保证数据可靠性

    • 写请求发生时,会同时发送给缓存和数据库进行处理,等到二者都写完数据,再返回,这样即使Redis宕机,最新数据库仍保存在后端数据库中。

    • 优点:保证了数据的可靠性

    • 缺点:数据库处理写请求较慢,缓存较快,增加了缓存的响应延迟

  • 异步写回:优先提供快速响应

    • 有限考虑响应延迟,所有请求都在缓存中处理,等到这写数据要被淘汰时,再写回后端数据库

    • 优点:数据处理在缓存中,处理的块

    • 缺点:数据没有写回到数据库,有丢失风险

只读缓存和同步直写策略的读写缓存的区别

1、使用只读缓存时,是先把修改写到后端数据库中,再把缓存中的数据删除。当下次访问这个数据时,会以后端数据库中的值为准,重新加载到缓存中。

  • 优点是,数据库和缓存可以保证完全一致,并且缓存中永远保留的是经常访问的热点数据

  • 缺点是每次修改操作都会把缓存中的数据删除,之后访问时都会先触发一次缓存缺失,然后从后端数据库加载数据到缓存中,这个过程访问延迟会变大

2、使用读写缓存时,是同时修改数据库和缓存中的值

  • 优点是,被修改后的数据永远在缓存中存在,下次访问时,能够直接命中缓存,不用再从后端数据库中查询,这个过程拥有比较好的性能,比较适合先修改又立即访问的业务场景

  • 缺点是在高并发场景下,如果存在多个操作同时修改同一个值的情况,可能会导致缓存和数据库的不一致。

3、当使用只读缓存时,如果修改数据库失败了,那么缓存中的数据也不会被删除,此时数据库和缓存中的数据依旧保持一致。

而使用读写缓存时,如果是先修改缓存,后修改数据库,如果缓存修改成功,而数据库修改失败了,那么此时数据库和缓存数据就不一致了。如果先修改数据库,再修改缓存,也会产生上面所说的并发场景下的不一致。

只读缓存是牺牲了一定的性能,优先保证数据库和缓存的一致性,它更适合对于一致性要求比较要高的业务场景。

数据库和缓存一致性要求不高,或者不存在并发修改同一个值的情况,那么使用读写缓存就比较合适,它可以保证更好的访问性能

十、替换策略

缓存数据的淘汰机制(也叫缓存替换机制)

  • 根据一定的策略,筛选出对应用访问来说不重要的数据

  • 将数据从缓存中删除

设置多大的缓存容量合适

需要结合应用数据实际访问特征和成本开销综合考虑

一般情况下,遵循八二原理(20%的数据贡献了80%的访问,被称为长尾效应),当80%提供的访问量更多时,叫做重尾效应。

推荐缓存容量设置为总数据量的15%-30%,兼顾访问性能和内存空间开销

Redis中的淘汰策略

八种淘汰策略

  • 默认情况下,Redis使用内存空间超过maxmemory值时,并不会淘汰数据,也就是noeviction策略。也就意味着一旦缓存写满,再有新的请求来时,也不会提供服务,而是报错。这种策略不进行数据淘汰,不会腾出新的空间,所以不用在Redis缓存中

  • volatile-ttl 在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。

  • volatile-random 就像它的名称一样,在设置了过期时间的键值对中,进行随机删除

  • volatile-lru 会使用 LRU 算法筛选设置了过期时间的键值对。

  • volatile-lfu 会使用** LFU 算法**选择设置了过期时间的键值对。

  • allkeys-random 策略,从所有键值对中随机选择并删除数据;

  • allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。

  • allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。

LRU算法

正常使用的是哈希表+双向链表的数据结构进行处理

由于链表的使用会带来额外的开销,大量数据移动链表时也会带来开销

所以Redis采用了时间戳的方式,会默认记录每个数据最近一次访问的时间戳,然后在淘汰数据时,随机选择N个数据作为候选集合,之后比较N个数据的lru字段,把最小的从缓存中淘汰出去

如何处理被淘汰的数据

如果是干净的数据直接 删除,如果是脏数据我们需要写回数据库

如何判断一个数据是否是脏数据

  • 干净数据:一直没有被修改,后端数据库里的数据也是最新值,在淘汰时可以直接删除

  • 脏数据:曾经被修改过,和后端数据库总的数据不一致。如果不把脏数据写回到后端数据库,数据的最新值会丢失,会影响正常使用

Redis在用作缓存时,使用只读缓存或读写缓存的哪种模式?

1、只读缓存模式:每次修改直接写入后端数据库,如果Redis缓存不命中,则什么都不用操作,如果Redis缓存命中,则删除缓存中的数据,待下次读取时从后端数据库中加载最新值到缓存中。

2、读写缓存模式+同步直写策略:由于Redis在淘汰数据时,直接在内部删除键值对,外部无法介入处理脏数据写回数据库,所以使用Redis作读写缓存时,只能采用同步直写策略,修改缓存的同时也要写入到后端数据库中,从而保证修改操作不被丢失。但这种方案在并发场景下会导致数据库和缓存的不一致,需要在特定业务场景下或者配合分布式锁使用。

当一个系统引入缓存时,需要面临最大的问题就是,如何保证缓存和后端数据库的一致性问题,最常见的3个解决方案分别是Cache Aside、Read/Write Throught和Write Back缓存更新策略。

1、Cache Aside策略:就是文章所讲的只读缓存模式。读操作命中缓存直接返回,否则从后端数据库加载到缓存再返回。写操作直接更新数据库,然后删除缓存。这种策略的优点是一切以后端数据库为准,可以保证缓存和数据库的一致性。缺点是写操作会让缓存失效,再次读取时需要从数据库中加载。这种策略是我们在开发软件时最常用的,在使用Memcached或Redis时一般都采用这种方案。

2、Read/Write Throught策略:应用层读写只需要操作缓存,不需要关心后端数据库。应用层在操作缓存时,缓存层会自动从数据库中加载或写回到数据库中,这种策略的优点是,对于应用层的使用非常友好,只需要操作缓存即可,缺点是需要缓存层支持和后端数据库的联动。

3、Write Back策略:类似于文章所讲的读写缓存模式+异步写回策略。写操作只写缓存,比较简单。而读操作如果命中缓存则直接返回,否则需要从数据库中加载到缓存中,在加载之前,如果缓存已满,则先把需要淘汰的缓存数据写回到后端数据库中,再把对应的数据放入到缓存中。这种策略的优点是,写操作飞快(只写缓存),缺点是如果数据还未来得及写入后端数据库,系统发生异常会导致缓存和数据库的不一致。这种策略经常使用在操作系统Page Cache中,或者应对大量写操作的数据库引擎中。

除了以上提到的缓存和数据库的更新策略之外,还有一个问题就是操作缓存或数据库发生异常时如何处理?例如缓存操作成功,数据库操作失败,或者反过来,还是有可能会产生不一致的情况。

比较简单的解决方案是,根据业务设计好更新缓存和数据库的先后顺序来降低影响,或者给缓存设置较短的有效期来降低不一致的时间。如果需要严格保证缓存和数据库的一致性,即保证两者操作的原子性,这就涉及到分布式事务问题了,常见的解决方案就是我们经常听到的两阶段提交(2PC)、三阶段提交(3PC)、TCC、消息队列等方式来保证了,方案也会比较复杂,一般用在对于一致性要求较高的业务场景中

十一、缓存异常

缓存和数据库的数据不一致

数据一致性

  • 缓存中有数据:必须和数据库中的值相同

  • 缓存中没有数据:数据库的值必须是最新值

缓存雪崩

大量的应用请求无法在Redis缓存中进行处理,紧接着将大量请求发送到数据库层,导致数据库压力激增

一般由两个原因导致

  • 由于缓存中有大量数据同时过期,导致大量请求无法处理

    • 解决方式:可以避免给大量的数据设置相同的过期时间,可以给每个的随机时间设置成随机的过期时间

    • 还可以通过服务降级

      • 当业务访问非核心数据时,暂时停止从缓存查找这些数据,而是直接返回预定义信息、空值、或者错误信息

      • 当业务访问核心数据时,允许查询缓存,如果缓存缺失,也可以继续查询数据库

  • Redis缓存实例发生故障宕机,无法处理请求,会导致大量请求堆积到数据库中

    • 解决办法:在业务系统中实现服务熔断或请求限流机制

      • 服务熔断:暂停业务对缓存系统的接口访问(就是不让redis处理了),等到redis恢复了,再允许发送请求到缓存系统

      • 缓存限流:通过限制入口允许每秒进入的请求,拦截多余请求,防止并发压力给到数据库

      • 两种办法都是在发生雪崩后在处理,只是降低了尽可能影响

    • 还可以通过提前预防的方式

      • 通过主从节点构建Redis缓存高可靠集群,当主节点故障时,还可以通过主从切换,继续提供服务
  • 一致性hash环的集群特性导致,集群中某个主从节点挂掉了,请求分散到其他集群,但是量极大,把其他集群也都冲垮了

    • 解决方法:如果场景冷热分明,不建议使用一致性hash环的集群,直接使用逻辑分组,挂掉的暂时挂掉,后续人工恢复

缓存击穿

指针对某个访问非常频繁的热点数据的请求,无法在缓存中处理,于是大量请求来到了后端数据库,于是数据库压力倍增,导致影响数据库处理其他请求

通常是因为热点数据过期时间到了,进而失效

解决方法:热点数据不设置过期时间对读数据做预判(如主动给某些热数据做过期时间延期操作)

缓存穿透

指的是访问的数据既不在缓存中,也不在数据库中,导致请求访问缓存时,发生缓存缺失,访问数据库时,数据库也没有,数据库无法读取数据写入缓存,于是大量数据来到数据库,缓存成了摆设,缓存和数据库的压力都增大了

通常发生的情况有两种

  • 业务层操作:缓存中和数据库中的数据被误删了

  • 恶意攻击:专门访问数据库中没有的数据

解决办法

  • 缓存空值或缺省值

  • 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在

  • 在请求入口的前端进行请求检测

是否可以采用服务熔断、服务降级、请求限流的方法来应对缓存穿透问题?

我觉得需要区分场景来看。 如果缓存穿透的原因是恶意攻击,攻击者故意访问数据库中不存在的数据。这种情况可以先使用服务熔断、服务降级、请求限流的方式,对缓存和数据库层增加保护,防止大量恶意请求把缓存和数据库压垮。在这期间可以对攻击者进行防护,例如封禁IP等操作。 如果缓存穿透的原因是,业务层误操作把数据从缓存和数据库都删除了,如果误删除的数据很少,不会导致大量请求压到数据库的情况,那么快速恢复误删的数据就好了,不需要使用服务熔断、服务降级、请求限流。如果误操作删除的数据范围比较广,导致大量请求压到数据库层,此时使用服务熔断、服务降级、请求限流的方法来应对是有帮助的,使用这些方法先把缓存和数据库保护起来,然后使用备份库快速恢复数据,在数据恢复期间,这些保护方法可以为数据库恢复提供保障。 还有一种缓存穿透的场景,我们平时会遇到的,和大家分享一下。 对于一个刚上线的新业务模块,如果还没有用户在这个模块内产生业务数据,当用户需要查询这个业务模块自己的数据时,由于缓存和数据库都没有这个用户的数据,此时也会产生缓存穿透,但这种场景不像误删数据和恶意攻击那样,而是属于正常的用户行为。 这种场景采用服务熔断、服务降级、请求限流的方式就没有任何意义了,反而会影响正常用户的访问。这种场景只能使用缓存回种空值、布隆过滤器来解决。 可见,服务熔断、服务降级、请求限流的作用是,当系统内部发生故障或潜在问题时,为了防止系统内部的问题进一步恶化,所以会采用这些方式对系统增加保护,待系统内部故障恢复后,可以依旧继续对外提供服务,这些方法属于服务治理的范畴,在任何可能导致系统故障的场景下,都可以选择性配合使用。

十二、缓存污染

数据访问次数非常少,甚至只会被访问一次,当完成请求后,仍继续留在缓存中的情况称为缓存污染

如何解决

借助缓存淘汰策略

首先排除随机的两个策略,因为一旦淘汰错了就会发货所能缓存缺失

volatile-ttl也不行,因为过期时间到了,也会产生缓存缺失(除非当已知数据需要再次访问,根据已知情况设置时间)

正确的策略 LRU缓存策略

LRU缓存策略

LRU策略核心思想:一个数据刚刚被访问,那么这个数据肯定是热点数据,会被再次访问。于是会在每个数据对应的RedisObject结构体中设置lru字段,用来记录数据访问的时间戳,在进行淘汰时会在候选数据集中淘汰lru字段值的最小的数据(访问时间最久的)

优点:适合数据频繁访问的业务场景,能够有效留存访问时间最近的数据,提高业务访问速度

缺点:因为只看数据访问时间,使用LRU策略在处理扫描式单次查询操作(对大量数据进行一次全体读取,而且只会读取一次,lru时间戳都大,于是造成了缓存污染)时,无法解决缓存污染

LFU缓存策略优化

在LRU基础上,为每个数据增加计数器,来统计数据的访问次数。

使用时:首先根据数据的访问次数筛选(计数器的作用),淘汰低的,两个数据访问次数相同时,再按照时间长短进行筛选。——这样解决了扫描式单次查询的,避免缓存污染

为了避免操作链表的开销,Redis 在实现 LRU 策略时使用了两个近似方法: Redis 是用 RedisObject 结构来保存数据的,RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳; Redis 并没有为所有的数据维护一个全局的链表,而是通过随机采样方式,选取一定数量(例如 10 个)的数据放入候选集合,后续在候选集合中根据 lru 字段值的大小进行筛选。 在此基础上,Redis 在实现 LFU 策略的时候,只是把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分。 ldt 值:lru 字段的前 16bit,表示数据的访问时间戳; counter 值:lru 字段的后 8bit,表示数据的访问次数。 总结一下:当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。 到这里,还没结束,Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样可以吗? 在实际应用中,一个数据可能会被访问成千上万次。如果每被访问一次,counter 值就加 1 的话,那么,只要访问次数超过了 255,数据的 counter 值就一样了。在进行数据淘汰时,LFU 策略就无法很好地区分并筛选这些数据,反而还可能会把不怎么访问的数据留存在了缓存中。 我们一起来看个例子。 假设第一个数据 A 的累计访问次数是 256,访问时间戳是 202010010909,所以它的 counter 值为 255,而第二个数据 B 的累计访问次数是 1024,访问时间戳是 202010010810。如果 counter 值只能记录到 255,那么数据 B 的 counter 值也是 255。 此时,缓存写满了,Redis 使用 LFU 策略进行淘汰。数据 A 和 B 的 counter 值都是 255,LFU 策略再比较 A 和 B 的访问时间戳,发现数据 B 的上一次访问时间早于 A,就会把 B 淘汰掉。但其实数据 B 的访问次数远大于数据 A,很可能会被再次访问。这样一来,使用 LFU 策略来淘汰数据就不合适了。 的确,Redis 也注意到了这个问题。因此,在实现 LFU 策略时,Redis 并没有采用数据每被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规则。 简单来说,LFU 策略实现的计数规则是:每当数据被访问一次时,首先,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。

使用了 LFU 策略后,缓存还会被污染吗?

我觉得还是有被污染的可能性,被污染的概率取决于LFU的配置,也就是lfu-log-factor和lfu-decay-time参数。

1、根据LRU counter计数规则可以得出,counter递增的概率取决于2个因素:

a) counter值越大,递增概率越低 b) lfu-log-factor设置越大,递增概率越低

所以当访问次数counter越来越大时,或者lfu-log-factor参数配置过大时,counter递增的概率都会越来越低,这种情况下可能会导致一些key虽然访问次数较高,但是counter值却递增困难,进而导致这些访问频次较高的key却优先被淘汰掉了。

另外由于counter在递增时,有随机数比较的逻辑,这也会存在一定概率导致访问频次低的key的counter反而大于访问频次高的key的counter情况出现。

2、如果lfu-decay-time配置过大,则counter衰减会变慢,也会导致数据淘汰发生推迟的情况。

3、另外,由于LRU的ldt字段只采用了16位存储,其精度是分钟级别的,在counter衰减时可能会产生同一分钟内,后访问的key比先访问的key的counter值优先衰减,进而先被淘汰掉的情况。

可见,Redis实现的LFU策略,也是近似的LFU算法。Redis在实现时,权衡了内存使用、性能开销、LFU的正确性,通过复用并拆分lru字段的方式,配合算法策略来实现近似的结果,虽然会有一定概率的偏差,但在内存数据库这种场景下,已经做得足够好了 Kaito 2020-10-19 使用了 LFU 策略后,缓存还会被污染吗?

我觉得还是有被污染的可能性,被污染的概率取决于LFU的配置,也就是lfu-log-factor和lfu-decay-time参数。

1、根据LRU counter计数规则可以得出,counter递增的概率取决于2个因素:

a) counter值越大,递增概率越低 b) lfu-log-factor设置越大,递增概率越低

所以当访问次数counter越来越大时,或者lfu-log-factor参数配置过大时,counter递增的概率都会越来越低,这种情况下可能会导致一些key虽然访问次数较高,但是counter值却递增困难,进而导致这些访问频次较高的key却优先被淘汰掉了。

另外由于counter在递增时,有随机数比较的逻辑,这也会存在一定概率导致访问频次低的key的counter反而大于访问频次高的key的counter情况出现。

2、如果lfu-decay-time配置过大,则counter衰减会变慢,也会导致数据淘汰发生推迟的情况。

3、另外,由于LRU的ldt字段只采用了16位存储,其精度是分钟级别的,在counter衰减时可能会产生同一分钟内,后访问的key比先访问的key的counter值优先衰减,进而先被淘汰掉的情况。

可见,Redis实现的LFU策略,也是近似的LFU算法。Redis在实现时,权衡了内存使用、性能开销、LFU的正确性,通过复用并拆分lru字段的方式,配合算法策略来实现近似的结果,虽然会有一定概率的偏差,但在内存数据库这种场景下,已经做得足够好了

十三、原子操作

单线程的Redis为什么还会有并发问题?

  • 答:这里的并发指的是多个客户端同时对Redis进行不同的业务逻辑,会有不同的命令操作,在一个客户读写的时候,可能有其它客户并发操作执行,也会产生并发读写问题

原子操作是一种提供并发访问控制的方法,无锁操作,减少对性能的影响

并发访问需要对什么进行控制

对数据修改的操作

客户修改数据的流程

  • 先数据读取到本地,本地进行修改

  • 修改完后返回Redis

这个流程也叫RMW(读取-写回-操作),当多个客户端对同一份数据执行RMW操作,就需要让其涉及的代码以原子性方式执行。访问同一份数据的RMW操作代码,就叫临界区代码,如果不在临界区做控制,就会导致并发读写错误

两种原子操作

  • 单命令操作:把多个操作在Redis中实现成一个操作

    • INCR/DECR:把三个操作转为一个原子操作,可以对数据做增值/减值的操作,因为本身是单命令操作,所以在执行时存在互斥性
  • Lua脚本:把多个操作写到一个Lua脚本中,以原子性方式执行单个脚本

十四、分布式锁

十五、事务机制

A(Atomicity):原子性:一个事务中的多个操作必须都完成,或者都不完成

C(Consistency):一致性:数据库中的数据在事务执行前后是一致的

I(Isolation):隔离性:要求执行一个事务时,其他操作无法存取到正在执行事务访问的数据

D(Durability):持久性:执行事务后,数据的修改需要被持久化的保存下来

Redis如何实现事务

事务的执行过程

  • 客户端要使用MULTI显示地表示一个事务的开启

  • 客户端把事务中本身要执行的具体操作发给服务器,但是命令不会立刻执行,而是被暂存到命令队列中

  • 客户端向服务器发送提交事务的命令(EXEC),这时队列中的命令就被真正的执行了

原子性

Redis的事务机制并不能完全保证原子性

三种情况讨论

  • 入队之前,发现客户端发送的命令有问题(语法错误或不存在的命令)被识别并记录错误,这样当执行EXEC时,Redis就会拒绝所有提交的命令操作,返回事务失败,保证了原子性

  • 事务操作入队时命令和操作的数据类型不匹配,但Redis实例没有检查出错误。于是在EXEC执行完以后,执行操作时会报错,不过,正确的命令已经执行完了,这种情况下,原子性就无法保证。Redis提供了DISCARD命令用来主动放弃事务执行,把暂存的命令队列清空,但起不到回滚的效果

  • 在执行EXEC时,Redis发生故障,导致事务执行失败。可以通过AOF日志,用redis-check-aof工具检查AOF日志文件,将未执行的事务操作去除,这样AOF恢复后,也保证了原子性,但是如果没有AOF日志,那么数据将全部丢失

入队之前,发现错误,保证原子

入队之前,没发现,保证不了

执行EXEC,Redis宕机,有AOF可以保证,没有,数据都没了.

使用RDB可以保证原子性,因为RDB不会在事务进行时执行,所以RDB不会记录执行一半的事务数据,恢复的也是之前的数据,但是会丢失距离上一次RDB之间的所有更改操作

一致性

会受到错误命令、实例故障的影响,所以按照命令出错和实例故障分成了三种情况

  • 命令入队时就报错

    • 事务本身就会放弃执行,保证了一致性
  • 命令入队时没报错,实际执行报错

    • 错的不执行,正确的执行,依然保持了数据库的一致性
  • EXEC命令执行时实例发生故障

    • 如果不用RDB或AOF,数据全部丢失,保证了一致性

    • 如果用了RDB,RDB不会在快照期间执行,所以事务的结果不会保存到RDB中,保证了一致性

    • 如果用AOF

      • 事务操作还没记录到AOF日志时,实例就发生了故障,那么数据依然是一致的

      • 如果有部分操作记录到AOF日志,可以用redis-check-aof清除事务中已经完成的操作,恢复后也是一致的

理论上来说,Redis对一致性的是有保证的

隔离性

事务的执行分为命令入队和命令执行

针对这两个阶段,分成两种情况分析

  • 并发操作在EXEC前执行,隔离性的保证要使用WATCH机制,否则无法保证隔离性

  • 并发操作在EXEC后执行,可以保证隔离性

WATCH 机制的作用是,在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证。

持久性

完全取决于Redis的持久化配置

不管采用RDB还是AOF都不能保证事务的持久性的

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到