Redis总结

Redis简介

Redis是一个基于内存实现的Key-Value数据结构的Nosql数据库。

Redis基本数据结构类型

  • String(字符串)
    • 简介:String是Redis最基础的数据结构类型,是二进制安全的,可以存储图片或者序列化的对象,值最大存储为512M。
    • 应用场景:共享session,分布式锁,计数器,限流
    • 内部编码有三种,int(8字节长整型)、embstr(小宇等于39字节字符串)、raw大于39个字节字符串。
  • Hash(哈希)
    • 简介:在Redis中,哈希类型是指V本身又是一个键值对结构
    • 内部编码:ziplist,hashtable
    • 应用场景:缓存用户信息等
    • 注意点:hgetall在元素比较多的时候可能会导致redis阻塞(redis大key问题),可以使用hscan,如果只是获取部分field,建议使用hmget
  • List(列表)
    • 简介:列表类型是用来存储多个有序的字符串,一个列表最多可以存储int最大值个元素。
    • 内部编码:ziplist,linkedlist
    • 应用场景:消息队列,文章列表
  • Set(集合)
    • 简介:集合类型也是用来保存多个字符串元素,但是不允许重复元素。
    • 内部编码:intset,hashtable
    • 注意点:smembers和lrange,hgetall都属于比较重的命令,在元素过多的情况下可能会阻塞redis,可以使用sscan来完成
    • 应用场景:用户标签,生成随机数抽奖,社交需求。
  • Zset(有序集合)
    • 简介:已排序的字符串集合,不允许重复元素。
    • 内部编码:intset,hashtable
    • 应用场景:排行榜,社交需求(如用户点赞)

特殊数据结构类型

  • Geospatial:Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
  • Hyperloglog:用来做基数统计算法的数据结构,如统计网站的UV
  • Bitmap:用一个比特位来映射某个元素的状态,在Redis中,他的底层是基于字符串类型实现的,可以把bitmaps看做一个以比特位为单位的数组。

Redis为什么这么快在这里插入图片描述

  • 基于内存存储实现
    Redis是基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
  • 高效的数据结构
    MySQL索引为了提高效率,选择B+树的数据结构。合理的数据结构可以让你的应用、程序更快,Redis内部的数据结构&内部编码
    • SDS 简单动态字符串
      字符串长度处理:Redis获取字符串长度时间复杂度为O(1),在C语言中需要从头开始遍历,复杂度为O(N);
      空间预分配:字符串修改越频繁,内存分配越频繁,就会消耗性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗。
      惰性空间释放:SDS缩短时,不是回头多余的内存空间,二十free记录下多余的空间,后续有变更,直接使用freee记录的空间,减少分配。
    • 字典:Redis作为K-V型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,如HashMap,通过key就可以直接获取到对应的value。而哈希表的特性,在O(1)时间复杂度就可以获得对应的值。
    • 跳跃表:跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引,提升查找效率。跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点

合理的数据编码

Redis支持多种数据类型,每种基本类型可能对多种数据结构。什么时候,使用什么样的数据机构,使用什么样的编码,是Redis设计者总结优化的结果。

  • String:如果存储数字的话,是用int类型的编码,如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。
  • List:如果列表的元素个数小宇512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码。
  • Hash:哈希类型元素个数小于512个,所有值小于54字节的话,使用ziplist编码,否则使用hashtable编码。
  • Set: 如果集合中的元素都是整数,且元素个数小宇512个,使用insert编码,否则使用hashtable编码。
  • Zset:当有序集合的元素个数小宇128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist编码。

合理的线程模型

  • I/O多路复用
    多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用epoll作为I/O多路复用技术实现。并且Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
I/O多路复用机制,核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就是触发了读/写事件。就通知应用程序,去获取这个就绪的连接
进行读写操作。
也就是在应用程序里面可以使用单个线程同时处理多个客户端连接,在对系统资源消耗较少的情况下提升服务端的链接处理数量。
在IO多路复用机制的实现原理中,客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端
会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel
即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理就可以了。

常见的IO多路复用机制的实现方式有:select、poll、epoll.
这些都是Linux系统提供的IO复用机制的实现,其中select和poll是基于轮询的方式去获取就绪连接。而epoll是基于事件驱动的方式获取就绪连接。从性能的角度来看,基于事件驱动的方式要优于轮询的方式。

  • select模型
    • 原理:select模型它的基本原理是,采用轮询和遍历的方式。也就是说,在客户端操作服务器时,会创建三种文件描述符,简称FD。分别是writefds(写描述符)、readfds(读描述符)和exceptfds(异常描述符)。而select会阻塞监视这三种文件描述符,等有数据、可读、可写、出异常活超时都会返回。返回后通过遍历fdset,也就是文件描述符的集合,来找到就绪的FD,然后触发相应的IO操作。
    • 优点: 跨平台支持性好,几乎在所有的平台上支持。
    • 缺点:select是采用轮询的方式进行全盘扫描,因此随着FD数量增多而导致性能下降,操作系统对单个进程打开的FD数量是有限制的,一般模式是1024个。虽然可以通过操作系统的宏定义FD_SETSIZE修改,但是在IO吞吐量巨大的情况下,效率提升有限。
  • poll模型
    • 原理:poll模型的原理与selet模型基本一致,也是采用轮询加遍历,唯一的区别就是poll采用链表的方式来存储FD。
    • 优点:没有最大FD数量限制。
    • 缺点:随着FD数量增多而导致性能下降。
  • epoll模型
    • 原理:由于select和poll都会因为吞吐量增加而导致性能下降,因此才出现了epoll模型。它是采用事件通知机制来触发相关的IO操作。它没有FD个数限制,而且从用户态拷贝到内核态只需要一次。它主要通过系统底层的函数来注册、激活FD,从而触发相关的IO操作,这样大大提高了性能。主要是通过调用以下三个系统函数:
    • epoll_create()函数,在系统启动时,会在linux内核里面申请一个B+树结构的文件系统,然后,返回epoll对应,也是一个FD。
    • epoll_ctl()函数,每新建一个连接的时候,会同步更新epoll对象中的FD,并且绑定一个callback回调函数。
    • epoll_wait()函数,轮询所有的callback集合,并触发对应的IO操作。
    • 优点:将轮询改成了回调,大大提高了CPU执行效率,也不会随FD数量的增加而导致效率下降。他没有FD数量限制,也就是说,它能支持的FD上限是操作系统的最大文件句柄书。一般而言1G内存大概支持10万个句柄。
    • 缺点:只能在linux下工作。
      在这里插入图片描述

什么是缓存穿透、缓存击穿、缓存雪崩

缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

  1. 缓存穿透产生的几种情况:
  • 不合理的业务设计:比如大多数用户都没开守护,但是每个请求都去查询缓存,用户有没有守护。
  • 业务/运维/开发失误的操作:比如缓存和数据库的数据都被误删除了。
  • 黑客非法请求攻击:故意捏造大量非法请求,以读取不存在的业务数据。
  1. 避免缓存穿透的几种方法
  • 对于非法请求,在api入口,对参数进行校验,过滤非法值。
  • 如果查询数据库为空,可以给缓存设置个默认值,给缓存设置适当的过期时间,同时要保证缓存与数据库的一致性。
  • 使用布隆过滤器快速判断数据是否存在,即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
    布隆过滤器原理:它由初始值为 0的位图数组和 N 个哈希函数组成。一个对一个 key 进行N 个 hash 算法获取 N 个值,在比特数组中将这 N 个值散列后设定为 1,然后查的时候如果特定的这几个位置都为 1,那么布隆过滤器判断该key 存在。

缓存击穿问题

指热点key在某个时间点过期的时候,而恰好在这个时间点对这个key有大量的并发请求过来,从而大量的请求打到db。
解决方案:

  • 使用互斥锁防范:缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,去操作,成功的时候再去加载db数据库数据和设置缓存,否则就重试获取缓存。
  • 永不过期:是指没有设置过期时间,但是热点数据快要过期的时候,异步县城去更新和设置过期时间。

缓存雪崩

指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。

  • 缓存雪崩一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0-1800秒等。
  • Redis故障宕机也可能引起缓存雪崩。解决方案为构造Redis高可用集群 。

热Key

在Redis中我们把访问频率高的key,称为热点key。如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

  1. 产生的原因:
  • 用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
  • 请求分片几种。超过单Redis服务器的性能,比如固定名称key、hash落入同一台服务器,超过机器瓶颈。
  1. 解决办法
  • Redis集群扩容:增加分片副本,均衡读流量。
  • 将热key分散到不同的服务器中。
  • 使用二级缓存,即JVM本地缓存,减少Redis的读请求。

Redis过期策略和内存淘汰策略

过期策略

  1. 定时过期
    每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的cpu资源区处理过期数据。从而影响缓存的响应时间和吞吐量。
  2. 惰性过期
    只有当访问一个key时,才会判断该ket是否已经过期,过期则清楚。该策略可以最大化的节省cpu资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  3. 定期过期
    每隔一定时间,会扫描一定数据的数据库expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描时间间隔和每次扫描的限定耗时,可以在不同情况下使得cpu和内存资源达到最优的平衡效果。
    expires字典会保存所有设置了过期时间的key的过期时间数据。其中key时指向键空间中某个键的指针,value是该键的毫秒精度unix时间戳表示过期时间。键空间是指该Redis集群中保存的所有键。

Redis中同时使用了惰性过期和定期过期两种过期策略。

内存淘汰策略

  • volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key 中使用 LRU(最近最少使用)算法进行淘汰;
  • allkeys-lru:当内存不足以容纳新写入数据时,从所有 key 中使用 LRU(最近最少使用)算法进行淘汰。
  • volatile-lfu:4.0 版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用 LFU 算法进行删除 key。
  • allkeys-lfu:4.0 版本新增,当内存不足以容纳新写入数据时,从所有 key 中使用 LFU 算法进行淘汰;
  • volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key 中,随机淘汰数据;。
  • allkeys-random:当内存不足以容纳新写入数据时,从所有 key 中随机淘汰数据。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间✁ key 中,根据过期时间进行淘汰,越早过期✁优先被淘汰;
  • noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

Redis持久化方式

Redis持久化有RDB和AOF两种方式

  1. RDB
    RDB是通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写入到磁盘,以二进制的压缩文件进行存储。
    RDB快照的触发方式有很多,比如执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行命令。

  2. AOF
    自动触发bgsave主从复制的时候触发AOF持久化,它是一种近乎实时的方式,把Redis server执行的事务命令进行追加存储。简单来说,就是客户端执行一个数据变更的操作,Redis server就会把这个命令追加到aof缓冲区的末尾,然后再把缓冲区的数据写入到磁盘AOF文件里面,至于最终什么时候真正持久化到磁盘是根据刷盘策略来决定的。
    AOF这种指令追加的方式,或造成AOF文件过大,带来明显的IO性能
    RDB是每隔一段时间触发持久化,因此数据安全性低,AOF可以做到实时持久化,数据安全性较高,RDB文件默认采用压缩的方式持久化,AOF存储的事执行命令,所以RDB在数据恢复的时候性能比AOF要好。

Redis常用应用场景

  • 缓存
  • 排行榜
  • 计数器应用
  • 共享Session
  • 分布式锁
  • 社交网络
  • 消息队列
  • 位操作
    用户数据量上亿场景下,例如几亿用户系统签到,去重登陆次数统计,某个用户是否在线状态等等。腾讯10亿用户,要在几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建议一个key,然后挨个记(需要的内存会很恐怖),而且这种类似的需求很多。这里要用到位操作——使用setbit、getbit、bigcount命令。原理是:Redis内构一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id(必须是数字),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统。

Redis高可用

Redis高可用包括两个层面,一个是数据不能丢失或者说是尽量减少丢失的风险;另外一个就是保证Redis服务不中断。

  • 对于减少数据丢失,可以通过AOF和RDB保证。
  • 对于保证服务不中断的话,Redis就不能单点部署。

Redis主从

  • Redis主从模式,就是部署多台Redis服务器,有主库和从库,他们之间通过主从复制,以保证数据副本一致。
  • 主从库之间采用的事读写分离的方式,其中主库负责读操作和写操作,从库则负责读操作。
  • 如果Redis主库挂了,切换其中的从库成为主库。
  1. Redis主从同步过程
    在这里插入图片描述
    Redis主从同步包括三个阶段。
  • 第一阶段主从库之间建立连接、协商同步。
    • 从库向主库发送psync命令,告诉它要进行数据同步。
    • 主库收到psync命令后,响应FULLRESYNC命令(它表示第一次复制采用的事全量复制),并带上主库runID和主库目前的复制进度offset。
  • 第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载。
    • 主库执行bgsave命令,生成RDB文件,接着将文件发给从库。从库接收到RDB文件后,会先清空当前数据库,然后加载RDB文件。
    • 主库把数据同步到从库的过程中,新来的写操作会记录到replicationbuffer。
  • 第三阶段:主库把新写的命令,发送到从库
    • 主库完成rdb发送后,会把replication buffer中的修改操作发给从库,从库再重新执行这些操作。这样主从就实现同步了。
  1. Redis主从的一些注意点
  • 主从数据不一致
    因为主从复制是一部进行的,如果从库滞后执行,则会导致主从数据不一致,主从数据不一致一般有两个原因。
    • 主从库网络延迟
    • 从库收到了主从命令, 但是它正在执行阻塞性命令。
  • 读取过期数据
    Redis版本低于3.2,读从库时,并不会判断数据是否过期,而是会返回过期数据。而3.2版本后进行了改进,如果数据已过期,从库不会删除,却会返回空值。
  • 一主多从,全量复制时主库压力问题
    如果一主多从模式,从库很多的时候,如果每个从库都要和主库进行全量复制的话,主库压力是很大的。因为主库fork进程生成RDB,这个fork过程是会阻塞主线程处理正常请求的。同时,传输大RDB文件也会占用主库的网络带宽。
    可以使用主-从-从模式解决。部署从集群时,选择硬件网络配置比较好的一个从库,让它跟部分从库再建立主从关系。
  • 主从网络断了怎么办
    主从完成全量复制后,它们之间会维护一个网络长连接,用于主库后续收到写命令传输到从库,它可以避免频繁建立连接的开销。但是,如果网路断开成年后,是否还需要进行一次全量复制呢?
    如果是Redis2.8之前确实再进行一次全量复制,但是这样开销就很大。Redis2.8之后做了优化,重连后采用增量复制方式,即把主从库网络断连期间主库收到的写命令,同步给从库。
当主从库断开连接后,主库会把断连期间收到的写命令操作,写入replication buffer,同时也会把这些操作命令写入repl_backlog_buffer这个
缓冲区。 repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
  • 如何保证主从数据一致性
    Redis主从数据有三种同步方式
    • 全量同步
      全量同步一般发生在第一次建立主从关系、或者跟主断开时间比较久的场景。
      slave向master发起同步指令,master收到以后,会通过bgsave指令生成一个快照RDB文件,同步给slave,slave拿到后,会通过master同步的快照文件进行加载。这个时候,主生成RDB文件时候的所有数据都同步给了slave。
      但是bgsave指令是不会阻塞其他指令执行的,所以master在生成快照文件时,还是能接收新的指令执行。这些指令master会先保存到一个叫replication buffer的内存区间,等slave加载完快宅文件后会同步。
    • 增量同步
      在全量同步之后为了减少资源的消耗,就会使用增量同步。
    • 指令同步:master输入的指令会异步同步给slave。

Redis哨兵

主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis 从 2.8 开始正式提供了 Redis 哨兵机制来解决这个问题。

  1. 哨兵作用
    哨兵其实是一个运行在特殊模式下的Redis 进程。它有三个作用,分别是:监控、自动选主切换(简称选主)、通知。
    哨兵进程在运行期间,监视所有的 Redis 主节点和从节点。它通过周期性给主从库发送 PING 命令,检测主从库是否挂了。如果从库没有在规定时间内响应哨兵的 PING命令,哨兵就会把它标记为下线状态;如果主库没有在规定时间内响应哨兵的 PING命令,哨兵则会判定主库下线,然后开始切换到选主任务。所谓选主,其实就是从多个从库中,按照一定规则,选出一个当做主库。至于通知呢,就是选出主库后,哨兵把新主库✁连接信息发给其他从库,让它们和新主库建立主从关系。同时,哨兵也会把新主库✁连接信息通知给客户端,让它们把请求操作发到新主库上。
  2. 哨兵模式简介
    哨兵模式一般是由多个哨兵组成哨兵系统,防止出现单点问题,同时哨兵之间还会互相进行监控。
    其实哨兵之间是通过发布订阅机制组成集群,同时,哨兵又通过 INFO命令,获得了从库连接信息,也能和从库建立连接,从而进行监控。
  3. 哨兵如何判定主库下线
  • 主观下线
    哨兵进程向主库、从库发送PING命令,如果主库或者从库没有在规定时间内响应PING 命令,哨兵就把它标记位主观下线。
  • 客观下线
    如果是主库被标记位主管下线,则正在监视这个主库的所有哨兵要以每秒一次的频率发送PING命令,以确认主库是否真的进入了主观下线。当有多数的哨兵(一般少数服从多数,由Redis管理员自行设定的一个值)在指定的时间范围内确认主库的确进入了主观下线状态,则主库会被标记为客观下线。这样做的目的就是避免对主库的误判,以减少没有必要的主从切换,减少不必要的开销。
    假设我们有N个哨兵实例,如果有N/2+1个实例判断主库主观下线,此时就可以把节点标记为客观下线,就可以做主从切换了。
  1. 哨兵模式如何工作
  • 每个哨兵以每秒钟一次的频率向它所知的主库、从库以及其他哨兵实例发送一个PING命令。
  • 如果一个实例节点距离最后一次有效回复 PING命令的时间超过 down-after- milliseconds选项所指定的值, 则这个实例会被哨兵标记为主观下线。
  • 如果主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率确认主库的确进入了主观下线状态。
  • 当有足够数量的哨兵(大于等于配置文件指定的值)在指定的时间范围内确认主库的确进入了主观下线状态, 则主库会被标记为客观下线。
  • 当主库被哨兵标记为客观下线时,就会进入选主模式。
  • 若没有足够数量✁哨兵同意主库已经进入主观下线, 主库✁主观下线状态就会被移除;若主库重新向哨兵的 PING命令返回有效回复,主库的主观下线状态就会被移除。
  1. 哨兵模式如何选主
    哨兵选主包括量大过程,分别是过滤和打分。气死就是在多个从库中,先按照一定的筛选条件,把不符合条件的从库过滤掉。然后再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库。
  • 选主时,会判断从库的状态,如果已经下线,就直接过滤。
  • 如果从库网络不好,总是超时,也会被过滤掉。down-after-milliseconds,表示主从库断连的最大连接超时时间。
  • 过滤完成之后进行打分步骤,依照从库优先级、从库复制进度、以及从库ID号进行打分。
  • 从库优先级最高的话,打分就越高,优先级可以通过slave-priority配置。如果优先级一样就选与旧的主库复制进度最快的从库。如果优先级和从库进度都一样,从库ID号小的打高分。
  1. 由哪个哨兵执行主从切换
    当主库被标记位客观下线之后会进行以下操作
    哨兵向其他哨兵发送命令,再发起投票,希望它可以执行主从切换。这个投票过程为leader选举。因为最终执行主从切换的哨兵成为leader,投票过程就是确定leader。一个哨兵想成为leader需要满足两个条件:
  • 需要拿到num(sentinels)/2+1的赞成票。
  • 并且拿到的票数需要大于等于哨兵配置文件中的quorum值。
    举个例子,假设有 3 个哨兵。配置的 quorum 值为 2。即一个一个哨兵想成为Leader 至少需要拿到 2 张票。如下图
    在这里插入图片描述
    • 在 t1 时刻,哨兵 A1 判断主库为客观下线,它想成为主从切换的 Leader,于是先给自己投一张赞成票,然后分别向哨兵 A2 和 A3 发起投票命令,表示想成为Leader。
    • 在 t2 时刻,A3 判断主库为客观下线,它也想成为 Leader,所以也先给自己投一张赞成票,再分别向 A1 和 A2 发起投票命令,表示也要成为 Leader。
    • 在 t3 时刻,哨兵 A1 收到了 A3 的 Leader 投票请求。因为 A1 已经把票Y 投给自己了,所以它不能再给其他哨兵投赞成票了,所以 A1 投票 N给 A3。
    • 在 t4 时刻,哨兵 A2 收到A3 的 Leader 投票请求,因为哨兵 A2 之前没有投过票,它会给第一个向它发送投票请求的哨兵回复赞成票 Y。
    • 在 t5 时刻,哨兵 A2 收到A1 的Leader 投票请求,因为哨兵 A2 之前已经投过赞成票给A3 了,所以它只能给 A1 投反对票 N。
    • 最后t6 时刻,哨兵 A1 只收到自己的一票 Y赞成票,而哨兵 A3 得到两张赞成票(A2 和 A3 投的),因此哨兵 A3 成为了 Leader。
      假设网络故障等原因,哨兵 A3 也没有收到两张票,那么这轮投票就不会产生Leader。哨兵集群会等待一段时间(一般是哨兵故障转移超时时间的2 倍),再进行重新选举。
  1. 哨兵下的故障转移
    假设哨兵模式架构如下,有三个哨兵,一个主库 M,两个从库 S1 和 S2。
    在这里插入图片描述

当哨兵检测到 Redis 主库 M1 出现故障,那么哨兵需要对集群进行故障转移。假设选出了哨兵 3 作为 Leader。故障转移流程如下:
在这里插入图片描述

  • 从库 S1 解除从节点身份,升级为新主库。
  • 从库 S2 成为新主库的从库。
  • 原主节点恢复也变成新主库的从节点 。
  • 通知客户端应用程序新主节点的地址。

故障转移后:
故障转移后

Redis Cluster 集群

哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Reids Cluster 集群(切片集群的现方案)应运而生,它在 Redis3.0 加入,实现了 Redis的分布式存储。对数据进行分片,也就是说每台 Redis 节点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,
即分散数据到各个 Redis 实例,还提供复制和故障转移的功能。

比如一个Redis实例保存15G甚至更大的数据,响应就会很慢,这事因为Redis RDB持久化机制导致的,Redis会fork子进程完成RDB持久化操作,fork执行的耗时与Redis数据量成正相关。
这时候你很容易想到,把15G数据分散来存储就好了嘛。这就是Redis切片集群的初衷。举个栗子:如果你要用Redis保存15G的数据,可以用3台Redis实例组成切片集群。
切片集群和Redis cluster的区别:Redis cluster是从Redis3.0版本开始,官方提供的一种实现切片集群的方案。
既然数据是分片分不到不同Redis实例,那客户端到底是怎么确定想要访问的数据在哪个实例上呢?

  • 哈希槽
    Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。
    一个切片集群被分为 16384个 slot(槽),每个进入 Redis 的键值对,根据key 进行散列,分配到这 16384 插槽中的一个。使用的哈希映射也比较简单,用CRC16算法计算出一个 16bit的值,再对 16384取模。数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点都可以处理这 16384 个槽。集群中的每个节点负责一部分的哈希槽,假设当前集群有 A、B、C3 个节点,每个节点上负责的哈希槽数 =16384/3,那么可能存的一种分配:

    • 节点A 负责 0~5460 号哈希槽
    • 节点B 负责 5461~10922 号哈希槽
    • 节点C 负责 10923~16383 号哈希槽
      客户端给一个 Redis 实例发送数据读写操作时,如果这个实例上并没有相应的数据,会怎么样呢?MOVED 重定向和 ASK 重定向了解一下哈
  • MOVED 重定向和 ASK 重定向
    在 Redis cluster 模式下,节点对请求的处理过程如下:

    • 通过哈希槽映射,检查当前 Redis key 的否存在当前节点
    • 若哈希槽不由自身节点负责,就返回 MOVED 重定向
    • 若哈希槽确实由自身负责,且 key 在 slot 中,则返回该 key 对应结果
    • 若 Redis key 不存在此哈希槽中,检查该哈希槽是否正在迁出(MIGRATING)?
    • 若 Redis key 正在迁出,返回ASK 错误重定向客户端到迁移的目的服务器上
    • 若哈希槽未迁出,检查哈希槽是否导入中?
    • 若哈希槽导入中且有 ASKING 标记,则直接操作,否则返回 MOVED 重定向
  • Moved 重定向
    客户端给一个 Redis 实例发送数据读写操作时,如果计算出来的槽不在该节点上,这时候它会返回 MOVED 重定向错误,MOVED 重定向错误中,会将哈希槽所在的新实例的IP 和 port 端口带回去。这就是 Redis Cluster 的MOVED 重定向机制。流程图如下:
    MOVED重定向流程

  • ASK 重定向
    Ask 重定向一般发生于集群伸缩的时候。集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用 Ask 重定向可以解决此种情况。

  • Cluster集群节点✁通讯协议:Gossip
    一个Redis集群由多个节点组成,各个节点之间是怎么通信的呢?通过Gossip协议!Gossip是一种谣言传播协议,每个节点周期性地从节点列表中选择k个节点,将本节点存储的信息传播出去,直到所有节点信息一致,即算法收敛了。

Gossip协议基本思想:一个节点想要分享一些信息给网络中其他的一些节点。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。一般而言,信息会周期性的传递给N个目标节点,而不只是一个。这个N被称为fanoutRedisCluster集群通过Gossip协议进行通信,节点之前不断交换信息,交换的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot信息等
等。gossip协议包含多种消息类型,包括ping,pong,meet,fail,等等

  • meet消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
  • ping消息:节点每秒会向集群中其他节点发送ping消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
  • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。消息中同样带有自己已知的两个节点信息。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
  • 故障转移
    Redis集群实现了高可用,当集群内节点出现故障时,通过故障转移,以保证集群正常对外提供服务。
    Redis集群通过ping/pong消息,实现故障发现。这个环境包括主观下线和客观下线。
    • 主观下线:某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
      在这里插入图片描述
    • 客观下线: 指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移
      • 假如节点A 标记节点B 为主观下线,一段时间后,节点 A 通过消息把节点 B的状态发到其它节点,当节点 C 接受到消息并解析出消息体时,如果发现节点 B是pfail 状态时,会触发客观下线流程;
      • 当下线为主节点时,此时 Redis Cluster 集群为统计持有槽的主节点投票,看投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。
        在这里插入图片描述
    • 故障恢复:故障发现后,如果下线节点是主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。流程如下:
      在这里插入图片描述
      • 资格检查:检查从节点是否具备替换故障主节点的条件。
      • 准备选举时间:资格检查通过后,更新触发故障选举时间。
      • 发起选举:到了故障选举时间,进行选举。
      • 选举投票:只有持有槽的主节点才有票,从节点收集到足够✁选票(大于一半),触发替换主节点操作
  • 为什么solt选定是16384
    为了节省网络开销所以不用65536,而再小的话又会增加碰撞几率。

Redis分布式锁

普通分布式锁弊端

  • setnx+expire分开写
    执行完setnx,如果在设置过期时间的时候,进程crash或者重启,这个锁就一直不会释放。
  • setnx+value值和过期时间
    可能业务还没执行完,锁已经释放了,或者误删了别的客户端的锁
  • set ex px nx + 校验唯一随机值,再删除
    业务还没执行完,锁已经释放了

Redisson原理

在这里插入图片描述
Redisson只要线程一加锁成功,就会启动一个watch dog 看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断地延长锁key的生存时间。Redisson就是这样解决的锁过期释放,业务没执行完的问题。

Redlock算法

Redis一般都是集群部署,假设数据在主从同步过程,主节点挂了,Redis分布式锁可能会有哪些问题呢?
在这里插入图片描述
如果线程一在Redis的master节点上拿到了锁,但是加锁key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁了,但是线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis坐着antirez提出一种高级的分布式锁算法:
Redlock。

  • 按顺序向5个master节点请求加锁
  • 根据设置的超时时间来判断,是不是要跳过该master节点。
  • 如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功
  • 如果获取锁失败,解锁。

在Redisson框架中,实现了红锁的机制,Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。当红锁中超过半数的RLock加锁成功后,才会认为加锁是成功的,这就提高了分布式锁的高可用。

红锁其实也并不能解决根本问题,只是降低问题发生的概率。完全相互独立的redis,每一台至少也要保证高可用,还是会有主从节点。既然有主从节点,在持续的高并发下,master还是可能会宕机,从节点可能还没来得及同步锁的数据。很有可能多个主节点也发生这样的情况,那么问题还是回到一开始的问题,红锁只是降低了发生的概率。

其实,在实际场景中,红锁是很少使用的。这是因为使用了红锁后会影响高并发环境下的性能,使得程序的体验更差。所以,在实际场景中,我们一般都是要保证Redis集群的可靠性。同时,使用红锁后,当加锁成功的RLock个数不超过总数的一半时,会返回加锁失败,即使在业务层面任务加锁成功了,但是红锁也会返回加锁失败的结果。另外,使用红锁时,需要提供多套Redis的主从部署架构,同时,这多套Redis主从架构中的Master节点必须都是独立的,相互之间没有任何数据交互。

Redis跳跃表

在这里插入图片描述

  • 跳跃表是有序集合zset的底层实现之一
  • 跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
  • 跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(如表头节点,表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
  • 跳跃表就是在链表的基础上,增加多级索引提升查找效率。

Mysql与Redis如何保证双写一致性

  • 缓存延时双删
    • 写请求
    • 删除缓存
    • 更新数据库
    • 休眠一会再删除缓存,这个休眠时间=读业务逻辑数据的耗时+几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据

但是如果第二次删除缓存失败呢?数据如何保证一致性。给key设置一个自然的过期时间,让它自动过期,这样的话就要接受在过期时间内,数据不一致的情况。

  • 删除缓存重试机制
    因为延时双删可能会存在第二步✁删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了呀
    在这里插入图片描述
    • 写请求更新数据库
    • 缓存因为某些原因删除失败
    • 把删除失败的key放到消息队列
    • 消费消息队列的消息,获取想要删除的key
    • 重试删除缓存操作
  • 读取binlog异步删除缓存
    重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的 binlog 来异步淘汰 key。
    以 mysql 为例吧
  • 可以使用阿里的canal 将 binlog 日志采集发送到 MQ 队列里面
  • 然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

为什么Redis6.0之后改多线程呢?

Redis6.0 之前,Redis 在处理客户端的请求时,包括读 socket、解析、执行、写socket 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
Redis6.0 之前为什么一直不使用多线程?使用 Redis 时,几乎不存在 CPU 成为瓶颈的情况, Redis 主要受限于内存和网络。例如在一个普通的Linux 系统上,Redis 通过使用 pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要使用 O(N)或 O(log(N))的命令,它几乎不会占用过多 CPU。
Redis 使用多线程并非是完全摒弃单线程,Redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。这样做的目的是因为 Redis的性能瓶颈在于网络 IO 而非 CPU,使用多线程能提升 IO 读写的效率,从而整体提高 Redis的性能。

Redis存在线程安全问题吗

  • 第一个,从Redis服务端层面。
    Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。
    虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。
    为什么Redis没有采用多线程来执行指令,我认为有几个方面的原因。
    Redis Server本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis的瓶颈点,所以没必要使用多线程来执行指令。
    如果采用多线程,意味着对于redis的所有指令操作,都必须要考虑到线程安全问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。
  • 第二个,从Redis客户端层面。
    虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。
    假设两个redis client同时获取Redis Server上的key1,同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。
    当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。

Redis的事务机制

Redis 通过 MULTI、EXEC、WATCH 等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
简言之,Redis 事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。

  • 开始事务(MULTI)
  • 命令入队
  • 执行事务(EXEC)、撤销事务(DISCARD)
命令描述
EXEC执行所有事务块内的命令
DISCARD取消事务,放弃执行事务块内的所有命令
MULTI标记一个事务块的开始
UNWATCH取消WATCH命令对所有key的监视
WATCH监视key,如果在事务执行之前,该key被其他命令所改动,那么事务将被打断

面试题

1. Redis的Hash冲突怎么办

Redis 作为一个 K-V 的内存数据库,它使用用一张全局的哈希来保存所有的键值对。这张哈希表,有多个哈希桶组成,哈希桶中的entry 元素保存了 key和value 指针,其中 key 指向了实际的键,value 指向了实际的值。哈希表查找速率很快的,有点类似于Java中的HashMap,它让我们在 O(1)的时间复杂度快速找到键值对。首先通过 key 计算哈希值,找到对应的哈希桶位置,然后定位到entry,在 entry 找到对应的数据。
Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。
当哈希表插入数据很多,冲突也会越来越多,冲突链表就会越长,由于链表只能通过指针逐一查询,那查询效率就会越来越低。
为了保持高效,Redis会对哈希表做rehash操作,也就是增加哈希桶,减少哈希冲突,为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用与当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。

  • 哈希冲突的原因
    首先,我们得知道下HashMap的存储方式,HashMap是k-v的数据结构,底层是一个entry数组的数据结构,并且根据key计算出一个hash值,取模entry数组长度-1,得到一个下标位置来存储数据。所谓hash冲突,就是不同数据hash算法计算出来的结果可能是相同的,那么不同的数据就会放在同一个下标位置。
  • 解决哈希冲突的办法
  • 开放寻址法,包含线性探测、平方探测等等,就是从发生冲突的那个位置开始,按照一定的次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal就用到了线性探测法来解决hash冲突的。
  • 链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式来存储。
  • 再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash函数对这个key做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
  • 建立公共溢出区,就是把hash表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入
    到溢出表中。

HashMap就是通过链式寻址法来解决的,但是链式地址有个问题:当链表过长的时候,会影响查询
性能,所以在HashMap里,当链表长度大于8的时候,会转为红黑树,提升查询性能。但是树在添
加数据的时候也会有树的分裂合并,所以在树节点小于6的时候,又会转为链表

HashMap在并发的时候会有什么问题

因为HashMap添加数据的时候,不是线程安全的,所以当并发添加数据的时候,有可能导致数据丢失,比如:当2个数据同时链地址到一个节点的时候,只会有1个是成功的。
还有,因为entry节点会有数据结构的变化,比如当链表长度达到8并且数组长度达到64后,会转为链表。那么在并发的时候,可能会产生数据结构类型转换错误。因为有可能在操作的时候,其他线程把数据结构变更。所以,如果在有并发要求的场景,可以用currentHashMap或者hashTable。它们是线程安全的。

布隆过滤器

原理不赘述:布隆过滤器并没有存放完整的数据,它只是运用一系列哈希映射函数计算出位置,然后填充二进制向量。如果数量很大的话,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是挺划算的。目前布隆过滤器已经有相应实现的开源类库啦,如 Google 的 Guava 类库,Twitter 的 Algebird 类库,信手拈来即可,或者基于 Redis 自带的 Bitmaps自行实现设计也是可以的。

Redis主从复制原理

Redis主从复制,是指在Redis集群里面,Master节点和Slave节点数据同步的一种机制。简单来说就是把一台Redis服务器的数据,复制到其他Redis服务器中。其中负责复制数据的来源称为master,被动接收数据并同步的节点称为slave。
在Redis里面,提供了全量复制和增量复制两种模式。

  • 全量复制
    一般发生在Slave节点初始化阶段,这个时候需要把master上所有数据都复制一份。Slave向Master发送SYNC命令,Master收到命令以后生成数据快照,把快照数据发送给Slave节点,Salve节点收到数据后丢弃旧的数据,并重新载入新的数据。
    需要注意,在主从复制过程中,Redis并没有采用实现强数据一致性,因此会存在一定时间的数据不一致问题。
  • 增量复制
    就是指Master收到数据变更之后,把变更的数据同步给所有Slave节点。增量复制的原理是,Master和Slave都会维护一个复制偏移量(offset),用来表示Master向Slave传递的字节数。每次传输数据,Master和Slave维护的Offset都会增加对应的字节数量。Redis只需要根据Offset就可以实现增量数据同步了。

AOF重写的过程

AOF文件重写的具体过程分为几步:

  • 首先,根据当前Redis内存里面的数据,重新构建一个新的AOF文件
  • 然后,读取当前Redis里面的数据,写入到新的AOF文件里面
  • 最后,重写完成以后,用新的AOF文件覆盖现有的AOF文件

另外,因为AOF在重写的过程中需要读取当前内存里面所有的键值数据,再生成对应的一条指令进行保存。而这个过程是比较耗时的,对业务会产生影响。所以Redis把重写的过程放在一个后台子进程里面来完成,这样一来,子进程在做重写的时候,主进程依然可以继续处理客户端请求。
最后,为了避免子进程在重写过程中,主进程的数据发生变化,导致AOF文件和Redis内存中的数据不一致的问题,Redis还做了一层优化。就是子进程在重写的过程中,主进程的数据变更需要追加到AOF重写缓冲区里面。等到AOF文件重写完成以后,再把AOF重写缓冲区里面的内容追加到新的AOF文件里面。

Redis哨兵机制和集群有什么区别

  • 哨兵集群
    Redis哨兵集群是基于主从复制来实现的,所以它可以实现读写分离,分担Redis读操作的压力。
    Redis哨兵集群无法在线扩容,所以它的并发压力受限于单个服务器的资源配置。
    Redis 哨兵集群是一主多从。
  • Redis cluster集群
    Redis Cluster 集群的Slave节点只是实现冷备机制,它只有在Master宕机之后才会工作。
    Redis Cluster提供了基于Slot槽的数据分片机制,可以实现在线扩容提升写数据的性能。
    Redis Cluster是多主多从。

Redis的理解

Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List。针对不同的结构,可以解决不同场景的问题。因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
其次,由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件。
并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程
序的数据IO效率。
最后,作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用在Redis集群里
面,通过hash槽的方式实现了数据分片,进一步提升了性能。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

示圆阇梨偈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值