Interview preparation -- redis

Redis 全局命令
  • Keys命令:慎用,这个命令时间复杂度O(n),他将当前所有key-value拉出来,生产几乎不考虑。
  • dbsize:返回当前数据库中键的综合,Redis内置了一个键总数遍历,时间复杂度O(1)
  • del:可以对任何数据类型执行del命令
  • expire:添加过期时间
布隆过滤器
  • bloomFilter是基于BitMap实现的,当我们面对巨量数据时候,用普通的数据结构存储时候,会消耗很大的内存,例如,5千万用户的访问,我们对比集合类型和Bitmaps类型,加入用户id是Long,64位

    • 集合类型 64bit * 50000000 = 381MB
    • Bitmaps 类型 1bit *100000000 = 11.8MB
  • 如上可见对于存储单一类型的数据,比如只需要表示是/否,这种情况,Bitmaps的优势更加明显

  • 而Bloom Filter的作用就是利用Bitmaps来解决巨量数据情况下的驱虫问题。在起到去重作用的同时,能最大可能的节省空间

  • 每一个Bloom Filter都会对应一个大型的位数组 和 多个不一样的hash函数,

    • 当添加key时候,会使用多个hash对key计算,得到多个索引,然后对bitMap取模得到位置,接着将这个几个位置设置为1即可。
    • 当询问key释放存在时候,和add一样,多个hash位置计算出,只有都为1 才表示极大大概率存在。

在这里插入图片描述

HyperLogLog
  • Redis 提供HyperLogLog数据结构用来解决统计问题。提供不精确的去重计数方案,虽然不精确,但是标准误差也只有0.81%,并且以极小的内存代价(12KB)完成统计亿级数据量
  • 主要指令pfadd,pfcount
  • Redis 对HyperLogLog的存储进行了优化,在计数比较小时候,存储空间采用稀疏矩阵存储,空间占用少,只有当计数变大,稀疏矩阵占用空间渐渐超过阈值,才会一次性转变为稠密矩阵,才会占用12KB空间
  • HyperLogLog是通过样本估计算法来推算出总体的大小,在该数据结构中,分成16384个桶,每个桶6bit,通过统计每个桶中低位开始第一个1 开始的位置的平均值K,推算出样本数据的数据量N。
  • 12KB计算 16384 个桶是 2 14 2^{14} 214, 内存计算 2 14 2^{14} 214 * 6/8 = 12KB

在这里插入图片描述

Redis 分布式锁
  • Redis 分布式锁实一般使用setnx指令,只有key不存在时候才会设置成功,并且只会被一个客户端设置成功,之后用del指令释放。

  • 在Redis2.8 版本后setnx指令中添加了过期时间参数,这样可以直接设置过期时间

  • 超时问题:

    • 如果在分布式锁锁住的逻辑中有太长时间消耗的操作,甚至超过锁超时时间,会出现问题,此时第二个线程可能重新获取到这吧锁导致并发问题。而新的锁可能被上一个线程给删掉
  • 超时问题解决方案:

    • 不要用于较长时间的任务
    • 乐观锁方式删除,set指令的value给一个随机数,删除锁之前,先对比随机数是否一致再删除,次数匹配value与删除key不是原子操作,需要借助Lua脚本完成。
Redis 内存结构以及扩容
  • Redis中所有key都存储在一个很大的字典桶,这个字典结构和Java的HashMap一样。一维数组,二维链表结构,数组的大小总是 2n ,扩容每次加倍 也就是 2n+1

在这里插入图片描述

  • Java的HashMap扩容会一次性将旧数据全部rehash到新数组,如果数据量大,会导致卡顿
  • Redis的扩容采用渐进式rehash,同时保留旧的数组和新的数组,接着用定时任务以及 后续对hash的指令操作上一步一步的将旧的数组中数据移到新数组中。这种情况会导致,如果操作处在rehash中的字典,需要同时访问新旧两个数据结构
Redis 线程模型
  • Redis基于Reactor模式开发了自己的网络事件处理器-- 文件事件处理器,
  • 采用I/O多路复用同时监听多个socket,根据Socket当前执行的事件来为Socket选择对应的事件处理器。
  • 当被监听的socket准备好执行 accept,read,write,close,等操作时候先将指令放入队列中,文件事件处理器会通过轮训的方式处理到来的事件
  • 虽然Redis是单线程,但是通过I/O多路复用,可以做到一个线程监听处理多个Socket

在这里插入图片描述

Redis 文件事件处理器几个组成部分
  • Socket:文件事件就是对Socket操作的抽象,每个Socket都对应 accetp,read,write,close等事件操作。每个操作都对应不同事件

  • I/O多路复用程序: 负责监听多个种类的Socket
    请添加图片描述

  • 文件事件分派器:负责接收I/O多路复用传递来的Socket,并根据不同Socket产生的事件类型,调用对应的处理器

  • 文件事件处理器:服务器会为不同任务执行不同事件处理器,这些处理器是一个个不同的函数。定义了某类型事件发生时候服务器执行逻辑。

Redis淘汰策略
  • 当Redis 内存超出物理内存限制时,内存数据会开始和磁盘产生频繁交换。Redis性能极具下降。

  • Redis提供maxmemory参数配置,超过配置最大内存使用时候可以安一定的策略来删除数据:

  • Noeviction: 不在继续写请求(除了del),默认策略,保证数据不丢,但是线上业务无法用

  • volatile-lru:淘汰设置了过期时间的最少使用的key

  • volatile-ttl:淘汰设置了过期时间的最近快要过期的数据

  • volatile-random:随机淘汰已经过期的key

  • allkeys-lru:淘汰所有key中最少使用的key

  • allkeys-random:随机淘汰所有key

LRU算法
  • LRU算法中维护了一个链表,链表中元素安顺排列,空间满时候,剔除尾部数据,单A被访问时候,则将A放到链表头,到最后链表中元素就是按照访问时间顺序排列的

请添加图片描述

Redis中LRU
  • Redis中并不是如上LRU,知识思想一致而已,因为维护一个链表代价太大,要大量额外空间,对现有数据结构破坏较大。Redis采用近似LRU
  • 近似LRU在现有数据结构情况下给每个Key增加一个额外小字段,每个字的长24bit,存储的是最后访问的时间。
  • Redis执行写时候,发现内存超过maxmamory,执行LRY,随机采样5个(可配置)key,然后淘汰最旧的key,如果淘汰后还超出,继续执行采样淘汰,采样数越大越接近严格LRU
Redis中LFU算法
  • Redis 4.0添加的一种淘汰策略,核心思想安最近被访问频率淘汰,最少被访问的优先淘汰,留下访问对的。
  • LFU更好的表示一个key的访问热度
  • LFU将原来key中24位时间戳改了,16位高位表示时间,后8位表示计数器,最多能表示255,因为只有255 所以存储的是访问频次的对数值
过期策略和惰性删除
  • Redis所有数据结构都可以设置过期时间。时间一到自动删除。但是Redis单线程,删除的时间会占用处理时间,太多删除会造成卡顿
  • Redis会将每个设置了过期时间的key放入一个独立的字典,之后定时遍历这个字典来删除到期的key
  • Redis还使用惰性删除过期key,也就是字客户端范围这个key时候,对时间进行对比,过期了就立刻删除
定时扫描
  • Redis默认每秒进行10次过期扫描,但是不是遍历过期key的字典,而是用贪心策略
    • 从过期字典随机20个key
    • 删除这个20个key中已经过期的key
    • 如果过期的key比率超过1/4 ,就重复第一步
  • 因此大量过期会一直扫描知道key变得稀疏为止,表现出来就是请求卡顿
从库过期策略
  • 枞哭不会扫描过期,从库对过期的处理是被动过期,主库key到期,在AOF增加一条del指令,同步到从库,从库通过这条del删除key
  • 因此大量过期时候,主从数据存在不一致
Redis lazyfree
  • redis 用del命令删除大key时候造成redis阻塞,或者redis清理过期数据和淘汰内存超限的数据,碰到大key也会造成服务器阻塞
  • 为了解决大key删除问题,Redis 4.0 引入了lazyfree机制,可以将删除键或者删除数据库操作放后台线程执行,从而尽可能避免服务器阻塞
Redis持久化
  • Redis支持RDB 和AOF 两种模式持久化机制
RDB模式
  • RDB模式将当前进程全部数据生成快照保存到硬盘,也就是当前内存所有数据放入瓷盘。因此RDB文件越大,写磁盘时间就越长。

  • Redis 提供两个手动命令生成RDB文件:save,bgsave

    • save:在主线程中执行,会阻塞;不建议用
    • bgsave:创建一个子线程,用于写RDB文件,可以避免对主线程的阻塞,Redis RDB的默认用法
      请添加图片描述
  • Redis在执行bgsave的时候主线程的写操作还在进行中,此时,利用写时复制(Copy-On-Write)技术做到并行进行,当写发生,被写入的这块数据会被复制一份,fork线程操作复制数据写入RDB,主线程不影响

  • 优点:

    • RDB文件是二进制文件,占用空间小
    • Redis加载RDB文件恢复数据远远快于AOF方式
  • 缺点:

    • 不能做到实时持久化/秒级持久化,以为一次bgsave时间过长,期间的操作很多,而且bgsave不宜频繁执行
    • Redis 新老版本RDB文件之间格式不同,互不兼容
AOF模式
  • AOF(Append only file):将每次执行的redis写命令以日志的方式记录,重启时候重新执行AOF文件中的写操作来完成数据恢复目的。主要解决数据持久化的实时性质要求。

请添加图片描述

  • AOF指令写入会先写入到缓存中:
    • 因为Redis使用单线程,如果每次都写磁盘,性能完全不受控取决于磁盘负载,先写入缓冲区aof_buf中,可以在之后用多种策略同步磁盘,在性能与安全性上做权衡。
  • AOF同步有多中情况:
    • always:每个写命令执行完立刻同步
    • everysec:每个写命令执行完先写到aof_buf中,没隔1 秒钟将缓冲区内容写入磁盘(建议&默认策略)
    • no:每次写命令都写缓存,又操作系统决定何时将缓冲区内容写入磁盘,同步周期最长30s
  • 如上:
    • 想要高性能就选no,要最高安全性就aways,允许数据有一点丢失(1s中指令),又希望性能不太大影响就选everysec
AOF文件重新
  • Redis引入AOF文件重写机制压缩文件体积。AOF文件重写是直接将当前Redis内存中数据转为写命令同步到新的AOF文件中。
  • 更小的AOF文件可以被Redis更快加载

请添加图片描述

  • 体积变小原因:
    • 旧文件中对同一个key的多次写入,直接写结果即可
    • 对list,set,hash,zset等类型的操作 能合并多个单条指令,达到批量操作的目的,例如用lpush
缓存穿透
  • 指查询一个不存在的数据,每次缓存+数据库都不能命中,造成请求每次都直达DB,这样就失去了缓存保护后段存储的意义。缓存穿透可能导致DB负载多大

  • 可能原因:

    • 业务问题,例如id从1 开始,但是有请求的是-1
    • 恶意攻击,爬虫等大量空明中,
  • 解决方式:

  1. 缓存空对象:
    • 空缓存的情况多的话导致内存占用,应该给一个较短过期时间,让自动剔除
    • 可能存在短期数据不一致,例如缓存空后,写入了该值到DB,此时我们只能依靠缓存更新策略,写DB后删除缓存。
缓存击穿
  • 缓存击穿单一个热点key在高并发访问的时候到了过期时间失效,导致大量请求直接落在DB上造成数据库的负载飙升。

  • 解决方式:

  1. 热点key设置永远不过期
  2. 利用分布式锁加载缓存: 在加载缓存时候,利用Redis分布式锁对key进行加锁,此时只有一个线程能获取锁并且进行DB查询加载缓存的操作,这样能避免所有请求直接落到DB上导致的问题。
缓存雪崩
  • 由于缓存层某些原因不能提供服务,或者统一时间缓存数据大面积失效造成所有请求都直接落到DB上,造成DB负载过高。

  • 解决方式:

  1. 保证缓存服务高可用,集群
  2. 给系统增加限流策略,例如sentinel,这样在流量突增的时候有个缓存,达到削峰填谷目的
  3. 将缓存实现时间分散开,比如在失效时间设置时候增加一个随机值。
热点KEY问题
  • Redis 中访问频率高的key称为热点key
  • 产生原因:
    • 某些业务场景,例如商品抢购,热点评论,明星直播等
    • 请求分片集中,超过当个Server极限。服务端数据读取时候,往往对数据进行分片切分,此过程会在某主机server上对应的key访问,单所有请求都访问同一个key,可能超过server的极限。导致热点key问题。
  • 危害:
    • 流量集中,达到物理网卡极限
    • 请求过多,缓存分片服务被拖垮
    • DB击穿,引起业务雪崩
  • 热点key发现:
  1. 客户端发现:
    • 每次在查询Redis的时候对访问的key做一次AtomicLongMap记录,这样能最直观看出请求量
    • 问题
      • 无法预知key个数,存在内存泄漏危险
      • 对业务代码有强侵入性
  2. monitor命令:
    • monitor命令可以兼容Redis执行的所有命令,利用monitor结果可以统计处一段时间内的热key
    • 问题:
      • monitor命令在高并发下会影响Redis性能,只适合在短时间内使用
      • 只能统计单个redis的热点key,对于集群需要进行汇总统计
  3. 抓取TCP包发现
    • Redis客户端使用TCP协议与服务端进行交互,如果能在机器上监听Redis端口进行抓包就能统计热点key
    • 问题:
      • 需要开发成本
      • 抓包可能存在丢包可能性
      • 维护成本高
    • 抓包现有解决方案:ELK(elasticSearch Logstash Kibana) 体系下的packetbeat[2] 插件,可以实现对Redis,MySql等服务的数据抓包,分析,报表展示
  • 解决方式:
    • 使用二级缓存,用guava-cache或者hcache,发现热点key后,将这些热点key加载到Jvm中作为本地缓存,访问这些热点key从本地缓存中获取即可,不直接从redis,保护了缓存服务器。
    • key分散:将热点key分散成多个子key,然后存储在缓存集群的多个不同机器上,子key对对应的value都一样。查询时候,通过hash算法随机一个key取读即可。
    • 利用Redis 3.0 集群中Redis Cluster将数据分片存储在多个节点上,每个节点只存储部分数据,并且Redis3.0集群支持动态扩容以及节点中slot数据的动态迁移,这样可以将热点数据所在的server上的slot在线的迁移到多台其他的server中的slot中,并且查询时候会自动做转发。
bigKey问题
  • Bigkey指key对应的value占用太大内存
  • 危害:
    • 内存空间不均匀,Redis cluster集群中,会造成内存使用不均匀
    • 超时阻塞:Redis操作大key比较耗时,例如del操作,而且Redis单线程,这样阻塞可能性变大
    • 网络拥堵:大key产生的网络流量太大。
  • 发现Bigkey
    • 可以利用scan命令来查询,设置合适的参数可以避免scan造成的阻塞问题。
  • 解决方式:
    • 对BigKey存储进行拆分,编程value1,value2,value3等,业务做适配
Redis6.0 中多线程
  • Redis 处理客户端请求的时候,包括Socket 连接,解析,执行,返回等都是通过主线程来完成,这是所谓的单线程。
  • 但是Redis4.0 之后,有一些其他的缓慢操作会通过后台线程来完成,例如,fork出的子线程做RDB备份,大key的删除,无用连接释放等
Redis6.0 之前为啥不用多线程
  • 官方回复:Redis使用,几乎不存在CPU瓶劲的情况
  • Redis主要受限于内存,网络,例如Linux上,官方给出通过pipelining可以100WTPS,也就是每一秒处理100w个请求
  • 使用单线程,可维护性高,多线程更复杂,不确定性更高,甚至可能有锁的消耗,死锁等问题。单线程使用下很多线程不安全的操作也可以在无锁情况下执行。
Redis 6.0引入多线程原因
  • 对于小的数据包,Redis服务器可以处理8w ~10W QPS,对于90%的公司单线程Redis肯定够了
  • 但是有些公司动不动上亿级别交易,因此更大QPS的需求。因此对于更高要求的Redis多线程有如下原因:
    • 可以充分利用服务器CPU资源,目前主线程只利用一个核
    • 多线程任务分摊Redis可以同步IO读写负荷
  • Redis 6.0 默认配置是禁用多线程的,需自己配置开启。
Redis缓存系统时间戳
  • 相对于缓存的读取,直接获取linux系统时间戳的成本太高了单线程Redis消耗不起,因此Redis会对时间进行缓存,并且由一个定时任务来更新时间戳,或者时间戳直接从缓存中直接拿。
Cache Aside 机制
  • Cache Aside 总的来说就是,DB & Cache 的双写一致性机制,在读数据是存在三种情况:
    • 失效:应用程序先从cache取数据,如果没有,则从数据库中取成功后放入缓存
    • 命中:直接从缓存中取数据,成功后返回
    • 更新:先将数据写入数据库中,成功后,让缓存失效
  • 而Cache Aside 的核心思想就是先更新数据库,在删除缓存的策略
  • 可能存在的问题:

请添加图片描述

  • 如上流程中:第一次读操作A中在他读完与 写入缓存的间隙,有一个重新写入数据库的线程B更新数据,导致A中读取到的数据是脏数据而造成数据不一致
  • 满足如上流程前提条件是,A的读取操作比B 的写入要慢才会出现,但是实际项目中很少有读慢于写的情况。要不然读写分离就没有意义了
  • 解决方案: 延迟双删, B线程在删除缓存后,延迟一定的时间例如2s后在做一次删除操作。能缓解这种问题。降低出现的概率
Read/Write Through机制
  • 调用方只和缓存交互,而不需要关心缓存后方的数据提供方。而由缓存来保证自身数据和数据提供方数据的一致性。

    • 读操作直接读取缓存中的结果,缓存中没有则返回没有
    • 写操作写入缓存,由缓存同步写入到调用方,写入操作才完成。
  • Read/Write Through 与Cache Aside区别

    • CacheAside数据写入缓存操作是由调用的查询结果触发的。
    • Read/Write Through 则需要在缓存启动的时候,自己完成数据的加载工作
  • 因此对于CacheAside 缓存只是一个辅助,没有缓存也可以依赖数据源完成数据读写,但是Read/Write Through只能强依赖于缓存中间件,要求缓存系统十分可靠

Redis 这么快的原因
  • 纯内存的KV操作
    • Redis是纯内存的,CPU不是性能瓶颈,Redis的瓶颈时机器内存与网络带宽,内存速度时远远大于磁盘操作。
  • 单线程:
    • 使用单线程让Redis设计更简单,没有上下文切换,不用考虑锁造成的性能损耗
    • Redis单线程时网络请求模块使用一个线程,即一个线程处理所有网络请求,其他模式改用多线程还是用比如lazyfree 机制,将要删除的大Key放到后台线程执行,依次规避主线程阻塞问题
  • IO多路复用:
    • 因为Redis时单线程的,所有操作都是按顺序执行,因此Redis基于Reactor开发了自己的网络事件处理器----文件事件处理器,服务端用单线程轮询的方式执行client提交到queue中的socket事件,高效率处理事件
Redis 中String 底层数据结构
  • Redis中字符串存储结构类似C中的一个结构体
struct SDS<T>{
	T capacity;			//数组容量
	T len;				//已有数据长度
	byte flags; 		//特殊标志位
	byte[] content;      //数组主体内容
}
  • capacity 和 len是一个T类型,因为Redis在数据量比较小时候,用byte 或者short表数量,可以做到对象压缩的目的。等数据上来了,可以用int,之后可能用long
  • Redis中字符串最长512M,创建字符串时候 len 与 capacity一样长,不分配冗余空间,因为绝大多数时候不会修改。
  • 每个Redis对象都有一个对象头信息:

在这里插入图片描述

Redis 使用规范
  1. 控制Key的长度(统一用项目名:key名)
  2. 避免存储大key:String存储在10KB以下,List/Hash/Set/Zset:元素个数载1万以内
  3. 选择合适数据结构:
    - 例如String,Set载存储int数据时候,用整数编码。Hash,ZSet元素少的时候用的压缩列表ziplist,数据多的时候采用哈希表和跳表
    - String 与Set 尽可能存储int类型
    - Hash,ZSet存储元素控制载转换阈值之下
    - Hash ,ZSet所有键值对长度小于64字节,并且键值对总数小于512
  4. 将Redis当缓存用,而不是数据库
  5. 设置maxxmemory + 淘汰策略,开启lazy-free策略
  6. 添加key 尽量都设置过期时间
  7. 尽量不用时间复杂度O(N) 的操作,例如keys,scan等,先看数据量级,在分批处理
  8. 大key删除用lazy-free,大集合对象,先看量级在分批删除
Redis中key散列算法步骤
  • 获取Key二进制数据
  • 计算hash值,使用murmurHash算法,得到64位的Hash值
  • 对Hash值进行取模,Redis中用的是一致性Hash算法,将Hash值映射到0~223 -1 的幻形空间中,将改空间划分为多个虚节点,每一个虚节点都对应一个Redis实例,可以N个,散列算法后将对应key的hash映射到对应虚节点上,让后找到虚节点对应的Redis实例,
  • 将key存储到Redis实例中,在Redis实例中,key存储在Hash表中,类似java中HashMap,找到Key的hash与 length 取模,得到位置,查看key是否存在,如果存在直接返回对应的value。
Redis client连接的是一个Service还是集群
  • Redis Client 连接的是集群,而不是某个Redis 服务,请求发送到某个Redis实例,改Redis实例会帮你定位当前key是在哪个slot,并且将请求转发到slot对应负责的实例上,因此Redis是不需要连接到某个实例,有集群内部进行转发
Redis 哨兵机制

- Redis

  • Redis 官方给出的功能如下:

    • 监控:持续监控master,slave是否处于预期工作状态
    • 自动切换主库:master运行故障,哨兵启动自动故障恢复,从slave选一台作为新master
    • 通知:让slave执行replicaof,与新的master同步,并且通知客户端与新的master建立连接
  • 监控原理

  1. 每个Sentinel每秒给master,slave,其他sentinel发ping命令
  2. ping超时,会被标记下线
  3. 如果多个Sentinel在指定时间内都检测到master的下线状态,就标记为下线
  4. Sentinel 和其他Sentinel 协商主节点的状态,如果主节点处于SDown状态,则投票自动选出新主节点,将剩余从节点指向新的主节点进行数据复制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值