本片学习内容整理至b站 IT老哥 的视频 《会了这些面试题后,可以挑战一下字节了》

  • 单线程的Redis为什么快
  • 五种基本数据类型底层采用什么数据结构
  • 缓存雪崩,缓存穿透,缓存击穿,附加
  • Redis的过期淘汰机制
  • redis与memcached的区别
  • redis线程模型
  • 哨兵Sentinel
  • 如何实现redis事务

单线程的Redis为什么快?

Redis有多快?官方给出的读写速度是10w/s,在单线程的前提下跑出这个好成绩,原因有以下几点:

  • Redis是完全基于内存的,因此读写效率高,同时Redis的持久化操作是通过fork子进程和Linux系统的页面缓存技术完成,并不会影响Redis
  • 单线程操作:单线程避免了频繁上下文切换导致的性能开销
  • 合理高效的数据结构
  • 采用了非阻塞的IO多路复用机制:多路I/O复用模型是利用select,poll,epoll可以同时监察多个流的IO事件的能力,在空闲时阻塞当前线程,当有一个或多个流有IO事件时,就从阻塞中唤醒,程序再依次轮询所有的流,并且只依次顺序处理就绪的流,这种做法避免了大量无用操作

五种基本数据类型底层采用什么数据结构

String:

存储数字时:int 存储长度大于39字节字符:raw 存储长度小于39字节字符:embstr

raw与embstr都是由SDS动态字符串构成的。唯一区别是raw分配存储时,redisobject 和 sds 各分配一块,而 embstr 是 redisobject 在一块内存中

List:

列表所有对象长度均小于64字节,且元素数量小于512:ziplist 否则:双向链表

Hash:

列表所有对象长度均小于64字节,且元素数量小于512:ziplist 否则:哈希表

Set:

列表所有对象都是整数,且元素数量小于512:inset 否则:哈希表

Zset:

列表所有对象长度均小于64字节,且元素数量小于128:ziplist 否则:跳表

缓存雪崩:

在高并发下,大量缓存key在同一时间集体失效,大量请求直接落在数据库上,导致数据库宕机

解决方案:

  • 随机设置key失效时间,避免大量key集体失效
setRedis(Key, value, time+Math.random()*10000);
  • 如果是集群部署,可以将热点数据均匀分布在不同的Redis库中避免key全部失效
  • 跑定时任务,在缓存失效前刷新新缓存
  • 不设置过期时间(不推荐)

缓存穿透:

redis缓存没有数据库中没有相关数据(如用户查询携带id=-1的相关数据并不断发起请求),redis中没有数据,无法进行阻拦,请求直接穿透到数据库,导致数据库压力过大宕机

解决方案:

  • 对不存在的数据将其缓存到redis中,设置key,value值为null(不论是数据未null还是系统bug),设置一个短期过期时间,避免影响用户正常使用
  • 拉黑用户IP
  • 对参数进行校验,不合法参数进行拦截
  • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap (位图) 中,一个一定不存在的数据会被这个bitmap拦截掉,减少对底层存储系统的查询压力

缓存击穿:

某一个热点key,在不停的扛着高并发,当这个热点key在失效的一瞬间,持续的高并发访问就击破缓存直接访问数据库,导致数据库宕机

解决方案:

  • 设置热点数据“永不过期”
  • 加上互斥锁:对于多个线程同时去查询数据库的热点数据,我们可以在第一个查询数据上的请求使用互斥锁锁住它

其他线程需要使用就得等待,第一个线程查询到了数据,将数据放置redis中缓存起来,后面的进程就可以直接使用缓存数据

附加:

提前避免以上三问题:将redis,mysql等搭建成高可用集群,放置单点

出现错误如何修补 :服务中进行限流 + 降级,放置mysql被打崩溃

实在严重到宕机补救:Redis持久化 RDB + AOF,宕机重启,自动从磁盘加载数据,快速回复缓存数据

Redis的过期淘汰机制:

Redis中数据过期策略采用定期删除 + 惰性删除策略

1.定期删除,惰性删除策略是什么:

  • 定期删除:Redis启用一个定时器定时监听所有key,判定key是否过期,过期就删除。尽管可以保证所有的过期key都会被删除,但是十分浪费cpu资源,且对于以及过期但是定时器还没启动的key,它任然可以使用。
  • 惰性删除:在获得key时,先判断key是否以及过期过期就删除,缺点:如果这个key一直没被使用,那么它一直在内存,即便已经过期,这会浪费大量空间。

2.定期删除 + 惰性删除是如何工作的:

每次随机抽取一部分key进行检查,减少CPU资源的损耗,惰性删除策略互补了未检查到的key,基本上满足了所有要求

3.补充的内存淘汰机制:

  • volatile - lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

  • volatile - ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

  • volatile - random:从已设置过期时间的数据集(server.db[i].expires)中挑选任意数据淘汰

  • allkeys - lru:当内存不足以容纳新写入的数据时,在键空间内,移除最近最少使用的key(这个最常用)

  • allkeys - random:从数据集(server.db[i].dict)中任意选择数据淘汰

  • no-eviction:禁止驱逐数据,永不过期,也就是当内存不足以写入新数据时,写入操作会报错(默认)

  • volatile - lfu:4.0后加入,从已设过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰

  • allkeys - lfu:4.0后加入,当内存不足以写入新数据时,在键空间中,移除最不经常使用的key

Redis与memcached的区别:

存储方式上:memcached会将全部数据存入内存中,如果发生断电则会挂掉,数据不可以超过内存大小;redis有部分存在硬盘中,能保证数据的持久性

数据支持类型:memcached支持的数据类型相对简单;redis有复杂的数据类型

使用的底层模型不同:它们之间的底层实现,客户端之间通讯的应用协议不一样,redis自己构建了vm机制,因为一般的系统调用系统函数会浪费一定时间去移动和请求。value值大小不同,redis最大可到1gb;memcached只有1mb

Redis线程模型:

redis内部使用文件处理器 file event handler,这个文件事件处理器是单线程的,所以redis才叫单线程处理器模型。它采用IO多路复用机制监听多个socket,根据socket上事件处理器进行处理

文件事件处理器的结构包括4个部分:

  • 多个socket
  • IO多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)

多个socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,将socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应事件处理器进行处理。

哨兵Sentinel:

哨兵是Redis高可用的解决方案,可以运行多个Sentinel组成一个哨兵分布式系统

哨兵主要解决的问题:故障转移,如果主节点挂掉,就进行主从切换,让从节点升级为主节点,继续对外提供服务

使用流言协议(gossip protocols)来接收主机是否下线;并使用投票协议(agreeement protocols)来决定是否执行自动故障转移;以及选择哪个服务器作为新的主服务器

哨兵职责如下:

  • 监控:Sentinel会不断定期检查主服务器和从服务器是否运作正常

  • 提醒:当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或其他应用程序发送通知

  • 自动故障迁移:当一个主服务器不能正常工作时,Sentinel会开启一次自动故障迁移操作,将失效服务器的其中一个从服务器升级为新的主服务器,并让失效服务器的其他服务器改为复制新的主服务器,当客户端连接失败主服务器时,集群也会向客户端返回新服务器地址,使得集群可以使用新主服务器替代失效服务器

  • 统一配置管理:连接者询问Sentinel取得主从的地址

如何实现redis事务:

redis通过MULTI,EXEC,WATCH等命令来实现事务(transaction)功能,事务提供了一种将多个命令请求打包,然后一次性,按顺序的执行多个命令的机制,且在事务执行期间,服务器不会中断事务而改去执行其他客户端的请求,只有到这多个命令执行完毕后,才会接收其他命令

Redis事务也具有:原子性,一致性,隔离性;在特定情况下具有持久性