0%

查缺补漏-Redis

本章是整理知识内容,为强化知识长期更新。

Redis 简介

Redis 是完全开源免费的,遵守 BSD 协议,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

  • Redis 与 其他 key - value 缓存产品有以下三个特点:
    • Redis 支持数据持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
    • Redis 不仅仅支持简单的 key - value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储
    • Redis 支持数据的备份,即 master - slave 模式的数据备份。
  • Redis是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快,读的速度是 110000 次 /s, 写的速度是 81000 次 /s。另一方面是因为它的数据结构。键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。
  • Redis使用基于哈西槽(slot)的数据划分方式。

Redis数据类型与数据结构

数据结构时间复杂度

名称 时间复杂度
哈希表 O(1)
跳表 O(logN)
双向链表 O(N)
压缩列表 O(N)
整数数组 O(N)

Redis单线程

  • 我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。Redis 的单线程设计机制以及多路复用机制
  • Linux 中的 IO 多路复用机制
    • Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

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

  • 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
    • 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
    • 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
    • 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
    • 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
    • AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
    • 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
  • 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
  • Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
  • Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

Redis持久化机制

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。

  • Redis 4.0 之前数据持久化方式有两种:AOF 方式和 RDB 方式。

    • RDB(Redis DataBase,快照方式)是将某一个时刻的内存数据,以二进制的方式写入磁盘。RDB 默认的保存文件为 dump.rdb,优点是以二进制存储的,因此占用的空间更小、数据存储更紧凑,并且与 AOF 相比,RDB 具备更快的重启恢复能力。
    • AOF(Append Only File,文件追加方式)是指将所有的操作命令,以文本的形式追加到文件中。AOF 默认的保存文件为 appendonly.aof,它的优点是存储频率更高,因此丢失数据的风险就越低,并且 AOF 并不是以二进制存储的,所以它的存储信息更易懂。缺点是占用空间大,重启之后的数据恢复速度比较慢。
  • 将RDB做镜像全量持久化,AOF做增量持久化。RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

    • 实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程内存释放。
    • RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
  • AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

AOF日志

AOF持久化机制是以日志的形式记录Redis中的每一次的增删改操作,不会记录查询操作,以文本的形式记录,打开记录的日志文件就可以查看操作记录。

  • AOF更好保证数据不会被丢失,最多只丢失一秒内的数据,通过foek一个子进程处理持久化操作,保证了主进程不会进程io操作,能高效的处理客户端的请求。
  • AOF的日志文件的记录可读性非常的高,即使某一时刻有人执行flushall清空了所有数据,只需要拿到aof的日志文件,然后把最后一条的flushall给删除掉,就可以恢复数据。

AOF重写机制

在写入所有的操作到日志文件中时,就会出现日志文件很多重复的操作,甚至是无效的操作,导致日志文件越来越大。就浪费了资源空间,所以在Redis中出现了rewrite机制。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

  • 重写AOF的日志文件不是读取旧的日志文件瘦身,而是将内存中的数据用命令的方式重写一个AOF文件,重新保存替换原来旧的日志文件,因此内存中的数据才是最新的。

  • 重写操作也会fork一个子进程来处理重写操作,重写以内存中的数据作为重写的源,避免了操作的冗余性,保证了数据的最新。

  • 写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。

AOF 潜在的风险
  • 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
  • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
AOF三种写回策略
  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    • 同步写回可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    • 每秒写回采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
    • 操作系统控制的写回在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了。

RDB快照

RDB持久化就是将当前进程的数据以生成快照的形式持久化到磁盘中。对于快照的理解,我们可以理解为将当前线程的数据以拍照的形式保存下来。

  • 恢复数据速度快
  • 写入持久化文件快
  • RDB持久化后的文件是紧凑的二进制文件,适合于备份、全量复制、大规模数据恢复的场景,对数据完整性和一致性要求不高,RDB会丢失最后一次快照的数据。
RDB原理
  • fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

  • Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。

  • 对于 Redis 而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作,所以,针对任何操作,我们都会提一个灵魂之问:“它会阻塞主线程吗?”RDB 文件的生成是否会阻塞主线程,这就关系到是否会降低 Redis 的性能。

  • Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

    • save:在主线程中执行,会导致阻塞;
    • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

Redis启动持久化文件加载机制

Redis是基于内存的非关系型K-V数据库,它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。

缓存带来的问题

缓存雪崩

  • 我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
  • 解决办法:
    • 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是将缓存失效时间分散开。
    • 服务层增加熔断、降级、限流等操作,防止系统被流量攻溃。
    • 增加redis的监控措施,并采用高可用架构处理。

缓存穿透

  • 缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
  • 解决办法
  1. 缓存空对象:代码维护较简单,但是效果不好。
  2. 布隆过滤器:代码维护复杂,空间效率和查询时间都远远超过一般的算法,效果很好。有一定的误识别率,删除困难。
  3. 请求参数检验:防止恶意请求进入业务系统,将部分恶意请求拦截掉。
  • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  • 缓存空对象,一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
  • 布隆过滤器(推荐)
    • 就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
布隆过滤器

布隆过滤器是一种基于概率数据结构,主要用来判断某个元素是否在集合内,它具有运行速度快(时间效率),占用内存小的优点(空间效率),但是有一定的误识别率删除困难的问题。它只能告诉你某个元素一定不在集合内或可能在集合内。

  1. 一个非常大的二进制位数组 (数组里只有0和1)
  2. 若干个哈希函数
  3. 空间效率查询效率高
  4. 不存在漏报(False Negative):某个元素在某个集合中,肯定能报出来。
  5. 可能存在误报(False Positive):某个元素不在某个集合中,可能也被爆出来。
  6. 不提供删除方法,代码维护困难。
  7. 位数组初始化都为0,它不存元素的具体值,当元素经过哈希函数哈希后的值(也就是数组下标)对应的数组位置值改为1。
布隆过滤器案例

当查询一件商品的缓存信息,布隆过滤器只能精确判断数据不存在情况,对于存在我们只能说是可能,因为存在Hash冲突情况,当然这个概率非常低。

  1. 当查询一件商品的缓存信息
  2. 然后,在布隆数组中查找访问对应的位值,0或1
  3. 判断,三个值中,只要有一个不是1,那么我们认为数据是不存在的。
布隆过滤器删除问题

直接删除肯定不行,存在hash冲突,存在误删。

  • 定时任务,定期创建一个新的布隆过滤器。
如何减少布隆过滤器的误判
  • 增加二进制位数组的长度。这样经过hash后数据会更加的离散化,出现冲突的概率会大大降低。
  • 增加Hash的次数,变相的增加数据特征。特征数据越多,冲突的概率越小。

缓存击穿

缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。

  • 当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿。即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁单机的话用普通的锁(synchronizedLock)就够了。
    • 使用互斥锁方案缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
    • 持续不过期,通过定时任务的方式同步数据到缓存中。防止热key数据过期。

缓存预热

  • 缓存预热这个应该是一个比较常见的概念,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存更新

  • 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
    • 定时去清理过期的缓存;
    • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
  • 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

缓存降级

  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
    • 以参考日志级别设置预案:
      • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
      • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
      • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
      • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
  • 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

数据一致性

缓存一致性问题作为日常开发长期存在的问题。

缓存常见的操作
  • 写入:缓存和数据库是两个不同的组件,只要涉及双写,就存在只有一个写成功的可能性,造成数据不一致。

  • 更新:更新的情况类似,需要更新两个不同的组件。

  • 读取:读取要保证从缓存中读到的信息是最新的,是和数据库中的是一致的。

  • 删除:当删除数据库记录的时候,如何把缓存中的数据也删掉?

同步直写
  • 写数据库的时候也写入缓存,通过代码检查机制使数据库和缓存数据统一。通常会采用事务、重试机制来保证数据。
延时双删
  • 先更新再删除,但是执行删除动作,在不久之后再执行一次,比如 1-5 秒之后。
集中更新
  • 弱化数据库,将数据优先存放到redis总,在通过定时器或扫描逻辑代码将数据同步到mysql中。
触发式加载

使用懒加载的方式,可以让缓存的同步变得非常简单

  • 当读取缓存的时候,如果缓存里没有相关数据,则执行相关的业务逻辑,构造缓存数据存入到缓存系统;

  • 当与缓存项相关的资源有变动,则先删除相应的缓存项,然后再对资源进行更新,这个时候,即使是资源更新失败,也是没有问题的。

缓存热key

什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。

  • 如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
    • Redis集群扩容:增加分片副本,均衡读流量;
    • 使用二级缓存,即JVM本地缓存,减少Redis的读请求。

Redis 的线程模型

  • redis 内部使⽤⽂件事件处理器file event handler,这个⽂件事件处理器是单线程的,所以redis 才叫做单线程的模型。它采⽤ IO 多路复⽤机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进⾏处理。

单线程的Redis为什么这么快

  • 存内存操作。
  • 单线程操作,避免平凡的上下文切换。
  • 采用了非阻塞I/O多路复用操作。(这里的I/O是指网络I/O)

Redis的过期策略以及内存淘汰机制

  • redis采用的是定期删除+惰性删除策略。

    • 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
  • 定时删除

    • 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 内存淘汰策略

当Redis内存快耗尽时,Redis会启动内存淘汰机制,将部分key清掉以腾出内存。

  • 主要有5种处理机制
    • LRU 最近最少使用
    • LFU 最近使用频率低
    • Random 随机淘汰
    • TTL 过期时间
    • No-Enviction 驱逐(什么都不做直接返回错误)
  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  • volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
  • allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰。
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  • no-enviction(驱逐):禁止驱逐数据,新写入操作会报错。
  • 如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

Redis 的过期策略和内存淘汰机制有什么区别?

  • 新增 Redis 缓存时可以设置缓存的过期时间,该时间保证了数据在规定的时间内失效。通过这种方式可以完成某些场景的业务开发。对对于已经过期的数据,Redis 将使用两种策略来删除这些过期键,它们分别是惰性删除和定期删除。

  • 惰性删除是指 Redis 服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则正常返回值信息给客户端。

  • 定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。默认情况下 Redis 定期检查的频率是每秒扫描 10 次,用于定期清除过期键。当然此值还可以通过配置文件进行设置,在 redis.conf 中修改配置“hz”即可,默认的值为“hz 10”。Redis 服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描增加了最大执行时间为 25ms。

  • 当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,这和过期策略是完全不同的两个概念,经常有人把二者搞混,这两者一个是在正常情况下清除过期键,一个是在非正常情况下为了保证 Redis 顺利运行的保护策略。

Redis事务

严格上来说redis是伪事物,

  • Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。Redis会将一个事务中的所有命令序列化,然后按顺序执行。
    1. redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
    2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
    3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

为什么Redis的操作是原子性的,怎么保证原子性的?

  • 对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。Redis的操作之所以是原子性的,是因为Redis是单线程的。
  • Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
  • 使用Redis的事务,或者使用Redis+Lua的方式实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 客户端限流操作
//获取ip对应的访问次数
current = GET(ip)
//如果超过访问次数超过20次,则报错
IF current != NULL AND current > 20 THEN
ERROR "exceed 20 accesses per second"
ELSE
//如果访问次数不足20次,增加一次访问计数
value = INCR(ip)
//如果是第一次访问,将键值对的过期时间设置为60s后
IF value == 1 THEN
EXPIRE(ip,60)
END
//执行其他操作
DO THINGS
END

Redis 是如何实现高可用的?

  • 数据持久化,保证了系统在发生宕机或者重启之后数据不会丢失,增加了系统的可靠性和减少了系统不可用的时间(省去了手动恢复数据的过程)
  • 主从数据同步(主从复制),可以将数据存储至多台服务器,这样当遇到一台服务器宕机之后,可以很快地切换至另一台服务器以继续提供服务
  • Redis 哨兵模式(Sentinel),用于发生故障之后自动切换服务器
  • Redis 集群(Cluster),多主多从的 Redis 分布式集群环境,用于提供性能更好的 Redis 服务,并且它自身拥有故障自动切换的能力。

单机版

单机版的Redis也有很多优点,比如实现实现简单、维护简单、部署简单、维护成本非常低,不需要其它额外的开支。但是,因为是单机版的Redis所以也存在很多的问题,比如最明显的单点故障问题,一个Redis挂了,所有的请求就会直接打在了DB上。并且一个Redis抗并发数量也是有限的,同时要兼顾读写两种请求,只要访问量一上来,Redis就受不了了,另一方面单机版的Redis数据量存储也是有限的,数据量一大,再重启Redis的时候,就会非常的慢,所以局限性也是比较大的。

主从版本

主从的原理还算是比较简单的,一主多从,主数据库(master)可以读也可以写(read/write),从数据库仅读(only read)。主从模式一般实现读写分离主数据库仅写(only write),减轻主数据库的压力。但是数据的一致性问题,假如主数据库写操作完成,那么他的数据会被复制到从数据库,若是还没有即使复制到从数据库,读请求又来了,此时读取的数据就不是最新的数据。从主同步的过程网络出故障了,导致主从同步失败,也会出现问题数据一致性的问题。主从模式不具备自动容错和恢复的功能,一旦主数据库,从节点晋升未主数据库的过程需要人为操作,维护的成本就会升高,并且主节点的写能力、存储能力都会受到限制。

数据同步方式
  1. 当slave启动后会向master发送SYNC命令,master节后到从数据库的命令后通过bgsave保存快照(RDB持久化),并且期间的执行的些命令会被缓存起来。
  2. 然后master会将保存的快照发送给slave,并且继续缓存期间的写命令。
  3. slave收到主数据库发送过来的快照就会加载到自己的数据库中。
  4. 最后master讲缓存的命令同步给slave,slave收到命令后执行一遍,这样master与slave数据就保持一致了。

哨兵

哨兵模式是 Redis 官方推荐的高可用模式。哨兵模式是主从的升级版,因为主从的出现故障后,不会自动恢复,需要人为干预。在主从的基础上,实现哨兵模式就是为了监控主从的运行状况,对主从的健壮进行监控,就好像哨兵一样,只要有异常就发出警告,对异常状况进行处理。

监控

监控master和slave是否正常运行,以及哨兵之间也会相互监控

自动故障恢复

当master出现故障的时候,会自动选举一个slave作为master顶上去。

节点通信
  • 哨兵与master建立连接后,定期会向(10秒一次)master和slave发送INFO命令,若是master被标记为主观下线,频率就会变为1秒一次。
  • 发送的内容包含哨兵的ip和端口、运行id、配置版本、master名字、master的ip端口还有master的配置版本等信息。
  • 定期的向master、slave和其它哨兵发送PING命令(每秒一次),以便检测对象是否存活,若是对方接收到了PING命令,无故障情况下,会回复PONG命令。
  1. INFO:该命令可以获取主从数据库的最新信息,可以实现新结点的发现
  2. PING:该命令被使用最频繁,该命令封装了自身节点和其它节点的状态数据。
  3. PONG:当节点收到MEET和PING,会回复PONG命令,也把自己的状态发送给对方。
  4. MEET:该命令在新结点加入集群的时候,会向老节点发送该命令,表示自己是个新人
  5. FAIL:当节点下线,会向集群中广播该消息。
上线和下线

当哨兵与master相同之后就会定期一直保持联系,若是某一时刻哨兵发送的PING在指定时间内没有收到回复(sentinel down-after-milliseconds master-name milliseconds 配置),那么发送PING命令的哨兵就会认为该master主观下线Subjectively Down)。

因为有可能是哨兵与该master之间的网络问题造成的,而不是master本身的原因,所以哨兵同时会询问其它的哨兵是否也认为该master下线,若是认为该节点下线的哨兵达到一定的数量(前面的quorum字段配置),就会认为该节点客观下线Objectively Down)。

若是没有足够数量的sentinel同意该master下线,则该master客观下线的标识会被移除;若是master重新向哨兵的PING命令回复了客观下线的标识也会被移除。

选举算法

当master被认为客观下线后,又是怎么进行故障恢复的呢?原来哨兵中首先选举出一个老大哨兵来进行故障恢复,

选举master哨兵的算法叫做Raft算法
  1. 发现master下线的哨兵(sentinelA)会向其它的哨兵发送命令进行拉票,要求选择自己为哨兵大佬。
  2. 若是目标哨兵没有选择其它的哨兵,就会选择该哨兵(sentinelA)为大佬。
  3. 若是选择sentinelA的哨兵超过半数(半数原则),该大佬非sentinelA莫属。
  4. 如果有多个哨兵同时竞选,并且可能存在票数一致的情况,就会等待下次的一个随机时间再次发起竞选请求,进行新的一轮投票,直到大佬被选出来。
  • 选出master哨兵后,master哨兵就会对故障进行自动回复,从slave中选出一名slave作为主数据库
  1. 所有的slave中slave-priority优先级最高的会被选中。
  2. 若是优先级相同,会选择偏移量最大的,因为偏移量记录着数据的复制的增量,越大表示数据越完整。
  3. 若是以上两者都相同,选择ID最小的。

集群Cluster模式

集群模式实现了Redis数据的分布式存储,实现数据的分片,每个redis节点存储不同的内容,并且解决了在线的节点收缩(下线)和扩容(上线)问题。

![image-20211007011524701](/Users/zengqingfeng/Library/Application Support/typora-user-images/image-20211007011524701.png)

虚拟槽分区算法

在Redis集群中采用的使虚拟槽分区算法,会把redis集群分成16384 个槽(0 -16383)。

  • 当客户端请求过来,会首先通过对key进行CRC16 校验并对 16384 取模(CRC16(key)%16383)计算出key所在的槽,然后再到对应的槽上进行取数据或者存数据,这样就实现了数据的访问更新。
节点通信

节点之间实现了将数据进行分片存储,那么节点之间又是怎么通信的呢?这个和前面哨兵模式讲的命令基本一样。

  • 新上线的节点,会通过 Gossip 协议向老成员发送Meet消息,表示自己是新加入的成员。
  • 老成员收到Meet消息后,在没有故障的情况下会恢复PONG消息,表示欢迎新结点的加入,除了第一次发送Meet消息后,之后都会发送定期PING消息,实现节点之间的通信。
数据请求

在Redis的底层维护了unsigned char myslots[CLUSTER_SLOTS/8] 一个数组存放每个节点的槽信息。这样数组只表示自己是否存储对应的槽数据,若是1表示存在该数据,0表示不存在该数据,这样查询的效率就会非常的高,类似于布隆过滤器,二进制存储。并且,每个redis底层还维护了一个clusterNode数组,大小也是16384,用于储存负责对应槽的节点的ip、端口等信息,这样每一个节点就维护了其它节点的元数据信息,便于及时的找到对应的节点。

扩容和收缩

扩容和收缩也就是节点的上线和下线,可能节点发生故障了,故障自动回复的过程(节点收缩)。

  • 节点的收缩和扩容时,会重新计算每一个节点负责的槽范围,并发根据虚拟槽算法,将对应的数据更新到对应的节点。
  • 发生故障后,哨兵老大节点的选举,master节点的重新选举,slave怎样晋升为master节点,可以查看前面哨兵模式选举过程。
虚拟哈希槽分区

Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0~16383 整数槽内,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽(slot),每一个节点负责维护一部分槽以及槽所映射的键值数据。

一致性哈希算法原理

一致性 Hash 算法使用取模的方法,对 2 的 32 方取模。即,一致性 Hash 算法将整个 Hash 空间组织成一个虚拟的圆环,Hash 函数的值空间为 0~2^32-1(一个 32 位无符号整型)

slot 原理

Redis Cluster 中有一个 16384 长度的槽的概念,它们的编号为 0~16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster 中的每个 master 节点都会负责一部分的槽,当有某个 key 被映射到某个 master 负责的槽,那么这个 master 负责为这个 key 提供服务,至于哪个 master 节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成。每个槽映射一个数据子集,一般比节点数大。

  • Redis Cluster 是服务端管理节点、槽、数据,值得一提的是只有 master 节点才能分配槽。通过哈希算法再加上取模运算可以将一个值固定地映射到某个区间,在这里,这个区间叫做 slots,区间由连续的 slot 组成。在 Redis Cluster 中,我们拥有 16384 个 slot,这个数是固定的,我们存储在 Redis Cluster 中的所有的键都会被映射到这些 slot 中。
1
HASH_SLOT = CRC16(key) mod 16384
为何是 16384

Redis Cluster 为什么是 16384 个槽,16384 是 2^14 的结果

  • 作者怎么说
1
2
3
4
5
The reason is:

Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

大量key短时间内同时过期

  • 如果大量的key集中在某一个时间过期,会导致redis卡顿现象,所以在设置过期时间的时候最好加上一个随机数,让的key过期时间分散一点。

Keys命令

  • keys命令可以扫出指定的key列表,但是keys命令会导致线程柱塞,线上服务会停顿,直到指令执行完毕,服务才能恢复。建议使用scan命令来代替使用。scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。