为什么要用缓存?

  • 使用缓存的目的就是提升读写性能。在实际的业务场景下,更多的是为了提升读性能,带来更好的性能和并发量。Redis的读写性能比MySQL好的多,我们就可以把MySQL中的热点数据缓存到Redis,提升读取性能,同时减轻了MySQL的读取压力。

什么是Redis?

  • Redis是一个高性能的内存数据存储系统,也可以称为键值存储系统。它支持多种数据结构,包括字符串、哈希、列表、集合、有序集合等,还提供了一些高级功能,如发布订阅、事务、Lua脚本等。Redis的特点是数据存储在内存中,可以快速读写,同时支持数据持久化到磁盘中。Redis还具有分布式特性,可以通过分片和赋值来实现高可用和高扩展性。
  • Redis主要应用于缓存、会话存储、消息队列、排行榜等场景,具有快速、稳定、可靠等优点。由于其出色的性能和易用性,Redis已经成为最受欢迎的内存数据库之一。

使用Redis有哪些好处?

  • 使用Redis有以下几个好处
    1. 高性能:Redis将数据存储在内存中,读写速度非常快,可以达到几十万甚至上百万QPS,特别适合高并发场景。
    2. 数据结构丰富:Redis支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,可以满足不同场景下的需求。
    3. 持久化:Redis支持将数据持久化到磁盘中,以保证数据的安全性和可恢复性。
    4. 分布式特性:Redis支持分片和复制,可以实现高可用和高扩展性,支持数据在多台服务器之间的共享。
    5. 丰富的功能:Redis提供了许多高级功能,如事务、Lua脚本、发布订阅、过期策略等,可以满足更加复杂的业务需求。

为什么要是用Redis而不是用Memcached呢?

  • Redis和Memcached都是流行的内存缓存系统,它们都可以在内存中快速读写数据,但是在一些方面有所不同,下面是Redis相较于Memcached的一些优点
    1. 数据结构更丰富:Redis支持多种数据结构,例如字符串、哈希、列表、集合、有序集合等,这些数据结构可以直接映射到实际的数据模型中,方便业务开发和数据处理。
    2. 多种持久化方式:Redis支持多种持久化方式,包括哦RDB(快照)和AOF(日志),这些持久化方式可以保证数据的安全性和可恢复性。
    3. 多种复制方式:Redis支持主从复制和哨兵模式,可以实现高可用和自动故障转移,而Memcached则需要通过第三方工具来实现高可用。
    4. 更好的性能:Redis在读写性能和并发能力上相较于Memcached更好,尤其是在多核CPU环境下,Redis可以充分利用多核的优势,提高系统的吞吐量。
    5. 更丰富的功能:Redis提供了更丰富的功能,如事务、Lua脚本、发布订阅、过期策略等,可以满足更加负载的业务需求。

说说Redis线程模型

  • Redis采用单线程模型,也就是说所有的请求都由同一个线程来处理。这个线程主要负责网络IO、请求解析、命令执行和数据返回等业务。Redis内部通过事件驱动机制来实现异步IO操作,包括文件事件和时间事件。具体来说,Redis在启动时会创建一个事件处理器,来监听客户端套接字的读写事件,并在事件发生时触发响应的回调函数来处理事件。
  • Redis单线程模型的优点是代码简洁、易于维护和调试,同时可以避免多线程并发带来的同步和锁的问题。此外,Redis还采用了多路复用机制,可以将多个客户端的请求合并到一起,减少IO操作的次数,提高系统的吞吐量和响应速度。
  • 当然,Redis的单线程模型也存在一些缺点,如无法充分利用多核CPU的优势,容易受到单点故障的影响等。为了解决这些问题,Redis引入了多个进程和多个实例的方案,如主从复制、哨兵模式和集群模式等。这些方案可以提高系统的可用性和扩展性,同时保持了Redis简洁、高效的特点。

为什么Redis单线程模型效率也能那么高?

  1. C语言实现,效率高
  2. 纯内存操作
  3. 基于非阻塞的IO复用模型机制
  4. 单线程的话可以避免多线程的频繁上下文切换问题
  5. 丰富的数据结构,全程采用哈希结构,读取速度非常快,对数据存储进行了一些优化,例如压缩表、跳表等。

为什么Redis需要把所有数据放到内存中?

  • Redis之所以将所有数据都放在内存中,是因为它设计的目标是高性能、高吞吐量和低延迟,而内存访问的速度比磁盘访问的速度快很多。如果数据存储在硬盘中,磁盘I/O会严重影响Redis的性能。而且Redis还提供了数据持久化功能,不用担心服务器重启对内存中数据的影响。

Redis的同步机制了解吗?

  • Redis支持主从同步和从从同步,而在进行第一次主从同步时,需要现在主节点上执行BGSAVE命令,将当前内存中的数据持久化道磁盘上生成RDB文件,并且将主节点需要将后续修改操作记录到内存缓冲区中。在这个过程中,主节点会将生成的RDB文件发送给从节点,从节点接收并加载RDB文件到自己的内存中。加载完成后,从节点会通知主节点,将主节点在复制期间产生的命令同步到从节点,以此完成主从同步过程。

pipeline有什么好处,为什么要是用Pipeline?

  • 使用Pipeline的好处在于可以将多次I/O往返的时间缩短为一次,从而提高Redis的吞吐量和性能。Pipeline允许客户端将多个Redis命令打包成一次请求发送给Redis服务器,Redis服务器收到后,将多个命令按顺序执行,并将执行结果按照请求的顺序返回给客户端,这样就避免了每次请求都要进行网络通信的开销。

说一下Redis有什么优点和缺点?

  • 优点:
    1. 高性能:Redis使用C语言编写,采用单线程模型,将数据全部存储在内存中,加上异步I/O和时间驱动机制等优化,使得Redis在读写数据时的性能非常高。
    2. 数据结构丰富:Redis支持多种数据结构,如字符串、列表、哈希表、集合、有序集合等,这些数据结构可以满足不同的业务需求。
    3. 持久化机制:Redis提供了两张持久化机制,即RDB和AOF,可以将内存中的数据持久化到磁盘上,保证了数据的可靠性和安全性。
    4. 高可用性:Redis提供了主从复制和Sentinel机制,可以实现数据的高可用性和容错能力。
  • 缺点:
    1. 内存受限:Redis将所有数据存储在内存中,如果数据量很大,会受到内存大小的限制,不适合存储大规模数据。
    2. 持久化机制可能带来性能损失:由于Redis提供了持久化机制,数据需要同步到磁盘上,真会导致写入性能的下降。
    3. 单线程模型可能存在瓶颈:尽管Redis采用了单线程模型,但是在极端情况下,可能会出现性能瓶颈,影响系统性能。
    4. 不支持多机数据共享:Redis不支持多机数据共享,需要使用其他技术如主从复制和Sentinel机制来实现高可用性和容错能力。

Redis缓存刷新策略有哪些?

  • Redis提供了以下几种缓存刷新策略
    1. 基于过期时间:可以设置key的过期时间,当过期时间到达后,Redis会自动删除该key。
    2. 基于LRU算法:Redis使用LRU算法来淘汰最近最少使用的key,以保留热点数据。
    3. 基于LFU算法:Redis使用LFU算法来淘汰最不经常使用的key,以保留热点数据。
    4. 基于手动刷新:可以手动删除缓存中的key,或者通过发送通知来通知客户端删除key。
    5. 基于定时刷新:可以定时清空缓存,或者定时刷新缓存中的数据,以保持数据的及时性。

Redis持久化方式有哪些?有什么区别?

  • Redis提供两种持久化机制:RDB和AOF。
  • RDB(Redis DataBase)持久化:会将Redis在内存中的数据快照保存到磁盘上,形成一个RDB文件,该文件包含了Redis在某个时间点上的数据快照
    • 优点:
      1. 只有一个dump.rdb文件,方便持久化
      2. 容灾性好,一个文件可以保存到安全的磁盘。
      3. 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,I/O最大化
      4. 数据集较大时,比AOF的启动效率更高。
    • 缺点:
      1. 数据安全性较低,RDB是间隔一段时间进行持久化,如果持久化之间Redis发生故障,会发生数据丢失,因此这种方式更适合数据要求不严谨的时候。
  • AOF(Append Only File)持久化:是将Redis写操作记录到一个文件中,每次Redis执行一条写命令,就将该命令写入AOF文件中,这样可以保证每条命令都能被保存下来。AOF文件可以进行追加和重写操作,当文件太大时,Redis会自动进行重写,将多次修改合并成一条,以减少磁盘占用空间。
    • 优点:
      1. 数据安全:AOF持久化可以配置appendfsync属性,它可以指定AOF文件的刷盘策略。默认情况下appendfsync的值为everysec。即每秒中将AOF缓存中的数据写入磁盘一次。但是,用户也可以将appendfsync的值设置为always,这样每次执行写操作都会立即将AOF缓存中的数据写入磁盘。这样即使Redis发生异常情况。只要AOF文件中已经记录了相应的写操作,就可以通过AOF文件来恢复数据。
      2. 数据一致性:AOF持久化是通过append模式写入文件的,即每次写操作都是追加到AOF文件末尾。因此,即使Redis在写入AOF文件的过程中宕机,AOF文件也不会损坏,而是只会丢失一部分的数据。当Redis重新启动是,会通过redis-check-aof工具将AOF文件中不一致的数据进行修复,保证数据的一致性。需要注意的是,使用AOF持久化时,如果Redis频繁执行写操作,那么AOF文件可能会非常大,可能影响性能。因此,用户可以通过配置AOF重写规则,定期对AOF文件进行压缩,以减小文件大小。
    • 缺点:
      1. AOF文件比RDB文件大,且恢复速度慢。
      2. 数据集较大时,比RDB启动效率低。
  • Redis支持同时使用RDB和AOF持久化机制。在使用时,Redis会先尝试使用AOF文件来恢复数据,如果AOF文件不存在或者恢复失败,Redis会尝试使用RDB文件来恢复数据。同时使用两种持久化机制可以在保证数据完整性的同时提高恢复速度。

持久化有两种,那么该怎么选择呢?

  1. 不要仅仅使用RDB,因为那样会导致丢失很多数据。虽然RDB持久化机制的忒但是可以生成数据的快照,这样在恢复数据的时候非常快速。但是RDB持久化只在发生故障时执行,如果Redis崩溃或意外关闭,可能会丢失最近执行的一些命令。因此,建议使用AOF持久化来记录Redis执行的所有写操作,并将RDB持久化用于冷备。
  2. 也不要仅仅使用AOF,虽然AOF持久化机制可以记录Redis执行的所有写操作,因此在数据恢复方面会比RDB更加健壮,但是它也存在一些问题。如果仅使用AOF进行冷备,那么在恢复数据时,它可能会比RDB持久化慢。如果只使用AOF持久化,那么可能会因为AOF文件过大导致性能下降。
  3. Redis支持同时使用AOF和RDB持久化机制。使用AOF持久化可以保证数据不丢失,并作为数据恢复的首选,使用RDB持久化作为冷备,以提供快速数据恢复选项。这种方式可以利用AOF和RDB持久化机制的优点来提高数据安全性和恢复速度。
  4. 如果同时使用RDB和AOF持久化机制,在Redis重启时,会使用AOF来重构数据,因为AOF中的数据更加完整。

怎么使用Redis实现消息队列?

  • Redis可以使用list结构作为队列来实现消息队列,使用rpush生产消息,使用lpop消费消息。当lpop没有消息的时候,需要适当的sleep一会儿再重试。但是也可以使用blpop命令来阻塞住,直到消息到来,避免了sleep操作。
  • 如果需要实现生产一次消费多次的场景,可以使用pub/sub主题订阅者模式,实现1:N的消息队列。
  • 但是pub/sub的缺点是在消费者下线的情况下,生产的消息会丢失。因此,如果需要更可靠的消息队列,需要使用专业的消息队列,例如RabbitMQ。
  • 此外,Redis还可以使用sortedset结构来实现延时队列。使用时间戳作为score,消息内容作为key,调用zadd来生产消息。消费者可以使用zrangebyscore指令获取N秒之前的数据,然后轮询进行处理。

说说你对Redis事务的理解

  • 什么是Redis事务?
    • Redis中的事务是一组命令的集合,是Redis的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会被序列化、按顺序地执行,服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。Redis事务通过MULTI、EXEC、DISCARD、WATCH等命令来实现的。
命令作用
MULTI开启一个事务
EXEC提交事务,从命令队列中取出提交的操作命令,进行实际执行
DISCARD放弃一个事务,清空命令队列
WATCH检测一个或多个键的值在事务执行期间是否发生变化,如果发生变化,那么当前事务放弃执行
  • Redis事务的注意点有哪些?
    1. Redis事务是不支持回滚的。
    2. Redis服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断,直到事务命令全部执行完毕才会执行其他客户端的命令。
  • Redis事务为什么不支持回滚?
    • Redis的事务不支持回滚,但是执行的命令如果有语法错误,Redis会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行剩下的命令。这样做的原因是因为回滚需要增加很多工作,而不支持回滚可以保持简单、快速的特性。

Redis为什么设计成单线程的?

  • Redis的单线程设计是其高性能的重要原因之一。Redis单线程的设计思想主要是为了避免多线程带来的上下文切换、锁竞争等开销。从而提高Redis的效率和性能。
  • 具体来说,Redis单线程的设计主要有以下几个方面的考虑:
    1. 避免上下文切换:在多线程环境下,线程的切换会涉及到上下文的切换,这个切换本身就就会消耗CPU资源和时间。而Redis单线程的设计可以避免这种上下文切换的开销,从而提高Redis的性能。
    2. 避免锁竞争:在多线程环境下,线程之间共享数据时需要使用锁来保证数据的一致性和可靠性。而所本身也会带来开销和竞争,降低Redis的效率和性能。而Redis单线程的设计可以避免这种锁竞争的开销,从而提高Redis的性能。
    3. 减少内存分配:在多线程环境下,线程之间需要共享内存,而内存共享会涉及到内存分配和管理的开销。而Redis单线程的设计可以避免这种内存分配的开销,从而提高Redis的效率和性能。

什么是Bigkey?会存在什么影响?

  • Bigkey指的是Redis中的大键,即占用内存较多的键值对。造成的影响如下:
    1. 内存占用:Bigkey会占用大量的内存资源,导致Redis内存不足,从而影响Redis的性能和可用性。
    2. 网络传输:Bigkey会增加网络传输的负担,因为在进行数据备份和复制的时候,需要将Bigkey的数据全部传输,从而增加了网络带宽的使用。
    3. 超时阻塞:由于Bigkey占用的空间较大,所以Redis在对其操作时,可能会消耗过长的时间,导致客户端超时阻塞。因为Redis采用单线程模型,当处理一个大key时,其他请求必须等待该操作完成后才能执行,而这个操作可能会需要较长的时间,从而导致阻塞。为了避免这种情况的发生,可以对bigkey进行拆分或优化。
    4. 内存碎片:Bigkey会导致Redis中出现内存碎片,从而影响Redis的内存使用效率,导致Redis内存占用率上升。

说说Redis哈希槽的概念

  • Redis哈希槽是Redis集群中用来实现数据分片的一种机制,可以将所有的键均匀地分布到多个节点上,以实现高可用和高性能分布式数据存储。
  • 具体来说,Redis集群将整个数据集分为16384个哈希槽,每个节点负责其中的一部分哈希槽,节点之间通过Gossip协议进行通信,维护整个集群的状态。当一个客户端想要访问一个键时,Redis会根据键名计算出该键对应的哈希值,然后找到哈希槽的编号,再根据哈希槽的映射关系,将请求路由到对应节点上。

Redis常见性能问题和解决方案有哪些?

  1. 网络延迟:Redis的性能很大程度上受限于网络延迟,因此需要尽可能减少网络传输次数和数据量,避免过多的网络IO操作
    • 解决方案:可以使用Redis的Pipline特性,将多个请求打包发送,减少网络传输的次数;也可以使用Redis的批量操作命令,将多个数据一次性提交,减少网络传输的数据量。
  2. 大量的数据读写:Redis的单线程模型会在高并发读写的情况下出现性能瓶颈,导致响应时间变长。
    • 解决方案:可以使用Redis的主从复制和集群特性,将数据分布在多个节点上,增加系统的读写并发能力。
  3. 慢查询:当Redis中存在大量慢查询操作时,会影响Redis的整体性能。
    • 解决方案:可以使用Redis的slowlog功能,记录Redis的慢查询操作,并使用Redis的监控工具进行监控,及时发现慢查询问题。
  4. 内存使用过多:Redis需要将所有的数据存储在内存中,当数据量过大时,会占用大量的内存资源,导致Redis的性能下降。
    • 解决方案:可以使用Redis的持久化功能,将数据写入磁盘中,以释放内存空间;也可以使用Redis的内存优化技巧,如删除不必要的数据、合理使用Redis的数据结构等。
  5. 阻塞操作:当Redis执行某些操作时,会阻塞其他操作的执行,从而影响Redis的整体性能。
    • 解决方案:可以使用Redis的异步操作特性,将阻塞操作转化为异步操作,以提高Redis的性能和吞吐量。

如果Redis中有1亿个key,其中有10w个key是以某个固定的已知前缀开头的,如何将它们全部找出来?

  • 我们可以使用keys命令或scan命令,然而在数据量庞大的环境下,不推荐使用keys命令。
    1. keys命令是遍历查询的,时间复杂度为O(n),数据量越大查询时间越长,且Redis是单线程的,使用keys命令会导致线程阻塞一段时间,从而导致Redis会出现假死问题,直到keys命令执行完毕才能恢复,这在生产环境下是不可接受的。此外,keys命令没有分页功能,会一次性查询出所有符合条件的key值,输出的信息非常多。
    2. 相对来说,scan命令比keys命令更适合生产环境。sacn命令可以实现和keys命令相同的匹配功能,但是在执行过程中不会阻塞线程,并且查询的数据可能存在重复,需要客户端去重。因为scan命令是通过游标方式查询的,所以不会导致Redis出现假死问题。Redis在查询过程中会把游标返回给客户端,单词返回控制且游标不为0,则说明遍历还没有结束,客户端继续遍历查询。但是,scan命令在检索的过程中,被删除的元素是不会被查询出来的,如果在迭代过程中有元素被修改,scan命令也不能保证查询出对应的元素。相对来说,scan命令查找花费的时间会比keys命令长。
  • 补充:假死问题
    • Redis假死问题是指当Redis实例在进行某些耗时操作时(例如遍历所有key),由于Redis是单线程的,所以这个操作会导致Redis线程被阻塞,从而导致Redis无法处理其他请求,造成Redis服务不可用的状态。在这种情况下,Redis似乎已经死了,但其实Redis线程仍在执行操作,只是无法处理其他请求而已。因此,这种状态被称为Redis假死问题。避免Redis假死问题的常见方法是使用Redis提供的异步命令和管道技术,以避免在生产环境中会使用遍历所有key的操作。

如果有大量的key需要设置同一时间过期,一般需要注意什么?

  • 如果大量缓存同时失效,会导致大量的请求直接访问数据库,容易造成数据库崩溃或者降低数据库的性能,进而影响整个系统的稳定性。
  • 为了预防这种情况的发生,我们最好在设计数据过期时间的时候,都加上一个随机值,让过期时间更加分散,从而尽量避免大量的key在同一时刻失效。

什么情况下可能会导致Redis阻塞?

  • Redis可能出现阻塞的情况包括:
    1. Redis主线程在执行阻塞命令(如BRPOPBLPOPBRPOPLPUSHSUBSCRIBE等)时,会阻塞其他客户端的请求,直到命令执行完毕才能继续处理其他请求。
    2. Redis主线程在执行某些耗时的命令(如SORTKEYS等)时,也会阻塞其他客户端的请求,同样需要等待命令执行完毕后才能继续处理其他请求。
    3. Redis内存使用达到最大限制时,执行写操作(如SETINCR等)可能会导致Redis阻塞。这是因为Redis需要执行内存回收操作以释放内存空间,如果回收操作耗时过长,就会导致Redis阻塞。
    4. Redis主从同步过程中,如果主库无法及时响应从库的同步请求,就会导致从库阻塞,无法继续进行数据同步。
  • 对于这些阻塞情况,可以采取一些措施来避免或减少阻塞的影响,例如
    1. 尽可能使用非阻塞命令,例如LPUSHRPOP代替BLPOP,使用Lua脚本实现多个操作的原子性等。
    2. 尽量避免使用耗时的命令或对大数据集进行操作,如果必须使用,可以考虑将这些操作放在后台进行。
    3. 设置合理的内存使用上限,同时使用内存淘汰策略来控制内存使用情况。
    4. 配置合理的主从架构,避免主库过于繁忙,导致从库同步阻塞。

怎么提高缓存命中率?

  • 提高缓存命中率可以采取以下措施:
    1. 预热缓存:在系统启动的时候,将一些热点数据提前加载到缓存中,可以避免在系统运行时出现缓存穿透和缓存雪崩的情况。
    2. 增加缓存容量:增加缓存容量可以缓存更多的数据,从而提高缓存命中率。
    3. 优化缓存设计:合理的缓存设计是提高缓存命中率的前提,包括选择合适的数据结构、缓存过期时间、缓存的key命名等。
    4. 使用多级缓存:多级缓存可以将热点数据缓存在更快速、容量更小的缓存中,减少从慢速缓存或者数据库中读取数据的次数。
    5. 缓存穿透处理:针对一些缓存中不存在,但是经常被查询的数据,可以采取布隆过滤器或设置空值等方式来进行预判,避免缓存穿透的情况。
    6. 建立读写分离的架构:将读请求和写请求分别处理,读请求可以直接从缓存中读取数据,写请求更新数据库后再更新缓存,从而避免缓存和数据库的一致性问题。

Redis如何解决key冲突?

  • 如果两个key的名字相同,后一个key会覆盖前一个key。因此,为了避免key冲突,最好为每一个key取一个独特的、易于辨识的名称。
  • 通常可以使用业务名和参数来区key,这样可以避免key冲突,同时也方便业务逻辑的管理和维护。

Redis报内存不足怎么处理?

  • 可以考虑以下几种处理方式:
    1. 增加物理内存:增加Redis所在服务器的物理内存,可以让Redis有更多的空间来存储数据。
    2. 减少数据量:可以删除一些已经不再使用的数据,或者将一些数据进行持久化,以释放内存。设置缓存淘汰策略,提高内存的使用效率。
    3. 修改Redis配置:可以调整Redis配置文件中的一些参数,如maxmemory等,增加Redis可用内存。
    4. 使用Redis集群:可以将数据分散在多个Redis节点中,每个节点存储一部分数据,从而减少单个Redis实例的内存使用量。

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级都了解吗?

  • 简述其概念如下
    1. 缓存雪崩:指在某个时间段内缓存集体过期失效或缓存服务重启,导致大量请求都落到数据库上,从而导致数据库崩溃的情况。
    2. 缓存穿透:指查询一个不存在的数据,由于缓存没有命中,导致所有的请求都会到数据库上,造成数据库压力过大,严重的可能会导致数据库宕机。
    3. 缓存预热:指系统上线后,将相关的缓存数据直接加载到缓存系统中,避免在用户请求过程中因没有预先加载而导致缓存穿透的现象。
    4. 缓存更新:指对数据库中的数据更新时,同时更新缓存中的数据,保证缓存数据和数据库的一致性。
    5. 缓存降级:指在缓存失效或缓存访问异常时,为了保证系统的可用性,通过一些机制,将请求转发到其他服务或者直接返回默认值,从而避免系统崩溃或者因为缓存故障导致业务受损。
  • 常见的Redis缓存降级策略包括:
    1. 熔断降级:当Redis缓存故障或者超时时,系统会进入熔断状态,所有请求奖杯转发到备用服务或者直接返回默认值。
    2. 限流降级:当Redis缓存无法处理所有请求时,系统会采用限流策略,限制访问流量,保护系统资源,避免系统崩溃。
    3. 数据降级:当Redis缓存故障时,系统可以返回默认值,避免因缓存故障导致业务受损。

热点数据和冷数据是什么?

  • 热点数据和冷数据是根据数据被访问的频率来进行划分的。
    • 热点数据值的是被频繁访问的数据,通常是系统的核心数据,例如热门商品、热门文章、热门活动等,这些数据的访问量非常高,如果没有得到有效的缓存优化,系统将会面临严重的性能问题。
    • 冷数据则相反,指的是不经常被访问的数据,它们的数据访问频率较低,例如旧的文章、过期的活动等。
  • 了解热点数据和冷数据对于缓存设计和优化非常重要,因为不同的数据需要采用不同的缓存策略。
    • 例如对于热点数据需要采用缓存预热、缓存更新等策略来保证缓存的命中率,而对于冷数据则可以采用懒加载等策略来避免不必要的缓存开销。

Memcached和Redis的区别都有哪些?

  • Memcached和Redis是两种常用的缓存系统,它们的区别如下:
    1. 数据类型:Redis支持更丰富的数据类型,包括字符串、哈希、列表、集合、有序集合等,而Memcached仅支持简单的键值对存储。
    2. 持久化:Redis支持数据的持久化,可以将数据写入磁盘,而Memcached不支持数据的持久化。
    3. 分布式支持:Memcached天生支持分布式,多个节点可以组成一个集群,而Redis的分布式支持需要通过集群、分片等方式实现。
    4. 性能:在单机环境下,Redis的性能通常比Memcached更好,但在分布式环境下,由于网络通信开销的增加,两者的性能差距可能会减小。
    5. 缓存策略:Redis支持更多的缓存策略,比如LRU(最近最少使用)、LFU(最少使用)、随机等,而Memcached仅支持LRU。
    6. 应用场景:Redis更适合需要丰富数据类型、支持持久化、缓存策略较多、单机性能较好的场景;而Memcached更适合需要高速读写、分布式支持、缓存策略相对简单的场景。

Redis的数据类型,以及每种数据类型的使用场景?

  • 常见的几种数据类型和使用场景如下:
    1. 字符串(String):字符串类型是Redis最基本的数据结构,一个键最大能存储512MB。
      • 使用场景:适用于计数器、分布式锁、缓存等常见。
    2. 列表(List):列表是链表结构,可以在头部和尾部添加元素。
      • 使用场景:可以做简单的消息队列功能。利用Irange命令,做基于Redis的分页功能。
    3. 集合(Set):集合是通过哈希表实现的无序集合,每个元素都是独一无二的。
      • 使用场景:适用于好友关系、共同好友等去重和计算交集、并集、差集的场景。
    4. 哈希(Hash):哈希结构类似于关联数组,由字段和值组成。
      • 使用场景:适用于对象缓存。
    5. 有序集合(Sorted Set):有序集合类似于集合,不同的是每个元素都会关联一个权重(score),按照权重进行排序。
      • 使用场景:排行榜、带权重的任务队列等场景。
    6. 位图(BitMap):用于存储二进制位的数据结构,可以进行位运算,支持高效的位图计算。
      • 使用场景:用户签到记录。
    7. 地理位置(Geo):用于存储地理位置信息的数据结构。
      • 使用场景:附近的酒店、餐厅。
    8. HyperLogLog:用于进行基数计数的数据结构,支持高效的对大量元素进行去重统计。
      • 使用场景:网站的UV统计。

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

  • Redis的过期策略和内存淘汰机制如下:
    1. 过期策略:Redis中可以设置key的过期时间,过期时间到期后,key将会自动被删除。Redis提供了两种不同的过期策略:
      • 定时删除:在设置key过期的同时,创建一个定时器,当过期时间到达时,就会立即删除该key。
      • 惰性删除:再获取某个key的值时,先检查该key是否过期,如果过期就删除,否则返回该key的值。
      • Redis默认使用惰性删除策略。
    2. 内存淘汰机制:当Redis内存达到了最大限制时,需要从内存中删除一些数据。Redis提供了多种内存淘汰机制:
      • noeviction:当内存空间不足以容纳新写入数据时,新写入操作会报错,这种方式不会删除任何数据,应该只用于特殊场景。
      • allkeys-lru:当内存空间不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(LRU算法)。这是Redis默认的淘汰策略。
      • allkeys-random:从所有key中随机选择一些进行删除。
      • volatile-lru:当内存空间不足以容纳新写入数据时,再设置了过期时间的键空间中,移除最近最少使用的key(LRU算法)。
      • volatile-ramdom:从设置了过期时间的key中随机选择一些进行删除。
      • volatile-ttl:从设置了过期时间的key中,根据过期时间的先后顺序进行删除,越早过期的越优先删除。

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

  • Redis的操作是原子性的,是因为Redis是单线程的,Redis中的所有操作都是在一个单线程中执行,这样就可以避免并发的环境下多个线程同时修改同一个键值对的问题。在Redis中,任何一个操作都是原子性的,要么执行成功,要么执行不成功。如果一个操作包含多个步骤,那么这些步骤会被当成一个整体,要么全部执行成功,要么全部不执行。
  • Redis保证原子性的方式主要有两种:事务和Lua脚本。在事务中,Redis会将多个命令打包成一个事务进行执行,事务中的所有命令都会在一次操作中被执行,要么全部执行成功,要么全部不执行。而Lua脚本则可以将多个操作打包成一个原子性的操作进行执行,这个操作要么全部执行成功,要么全部不执行。另外,Redis还提供了一些原子性操作,例如INCR、DECR等,这些操作都是原子性的。
  • 在并发环境下,如果多个线程同时执行get和set命令,可能会出现竞争条件,从而导致数据不一致的问题。但是如果使用Redis提供的原子性操作INCR,则不会存在这种问题,因为INCR命令是原子性的。
  • 因此可以使用Redis事务或者Redis+Lua的方式保证多个命令在并发中的原子性,或者使用Redis提供的原子性操作。

面试模拟

  • 面试官:什么是缓存穿透?怎么解决?
  • 候选人:
  • emm,我想一下
    • 缓存穿透是指查询一个不存在的数据,如果从存储层查询不到数据,则不会写入缓存,此时就会导致每次请求这个不存在的数据,都会到DB里去查询,可能会导致DB挂掉,这种情况大概率是遭到了攻击。
    • 解决方案的话,一般可以缓存空数据,即缓存这个不存在的数据。另外一种解决方案就是使用布隆过滤器
  • 面试官:好的,那你能介绍一下布隆过滤器吗?
  • 候选人:
  • 嗯,是这样
    • 布隆过滤器主要是用于检索一个元素绝对不在集合中或可能在集合中。它的底层主要是先去初始化一个比较大的数组,里面存放二进制的0或1,在一开始都是0,当一个key来了之后经过3个哈希函数的计算,模于数组长度找到数据的下标,然后把下标位置的0改为1,这样的话,三个数组下标的位置就能标明一个key的存在,查找的过程也是一样的。
    • 当然这个也是有缺点的,布隆过滤器可能存在一些误判,我们一般是可以设置这个误判率的,大概不会超过5%,因为哈希冲突不可能避免,所以这个误判是必然存在的,要不然就增加数组长度。但其实5%的误判率一般项目也是能接受的,不至于高并发下压倒数据库。
    • 用上图来举个例子
      1. 初始化布隆过滤器为 16 位,每一位初始值都为 0。
      2. Fredy 录入布隆过滤器,经过三个哈希函数的计算,将第 1、3、7 位设为 1。
      3. Eli 录入布隆过滤器,经过三个哈希函数的计算,将第 10、12、15 位设为 1。
      4. 查询 Tom,经过三个哈希函数的计算,得到第 0、2、5 位,这三个位置上的数字都是 0。根据布隆过滤器的规则,可以判断 Tom 绝对不在数据库中。
      5. 查询 Lily,经过三个哈希函数的计算,得到第 7、12、15 位,这三个位置上的数字都是 1。这里需要澄清一点:虽然这三个位置上的数字都是 1,但布隆过滤器只能判断 Lily 可能存在于数据库中,而不能确定 Lily 真的在数据库中。因为这三个位上的 1 可能是由 FredyEli 共同组成的,所以存在一定的误判率。
      6. 当布隆过滤器判断数据一定不存在,就不用查询数据库,直接返回不存在的结果。当布隆过滤器判断可能存在,那么查询数据库,以确认元素是否真的存在。
  • 面试官:什么是缓存击穿?怎么解决?
  • 候选人:
  • emm,我想想
    • 缓存击穿是指在缓存中设置了过期时间的某个key,在某个时间点这个key正好过期,导致缓存失效,恰好此时有大量病发情趣同时访问该key,这些请求会直接访问后端数据库,造成数据库压力骤增,严重影响系统性能,甚至直接压垮数据库。
    • 解决方案的话,我了解的有两种
      • 方案一:使用互斥锁
        1. 当缓存失效时,不立即去加载数据库,而是先使用Redis的SETNX(Set if Not Exists)命令去设置一个互斥锁,只有一个请求能够成功设置互斥锁,其他请求会在这一步被阻塞
        2. 成功设置互斥锁的请求,再去加载数据库,并将加载的数据回设到缓存中
      • 方案二:逻辑过期
        1. 在设置缓存key的同时,额外存储一个过期时间字段到缓存中,但是不给当前key设置过期时间
        2. 当查询请求到达时,首先从缓存中取出数据,并且额外判断一下存储key的过期时间字段,若过期则认为缓存失效
        3. 当缓存失效时,开启另外一个线程进行数据的异步加载和缓存更新,当前请求直接返回缓存中的旧数据,但这部分数据可能不是最新
        4. 这种方案一定程度上保证了高可用性,避免了大量请求直接打到数据库。
    • 当然两种方案各有利弊
      • 方案一使用了互斥锁,保证了数据的强一致性,到哪性能可能会受到锁的竞争影响,而且需要考虑死锁的问题
      • 方案二优先考虑的是高可用和性能,但不能保证强一致性,有可能会出现缓存和数据库数据不一致的情况。
      • 实际使用中根据我们的需求来选择要保证一致性还是可用性。
  • 面试官:什么是缓存雪崩?怎么解决?
  • 候选人:
  • 缓存雪崩是指换成那种设置了相同的过期时间,导致大量缓存在同一时刻同时失效,进而导致所有的请求直接转发到了后端数据库,导致数据库瞬时压力过大,可能直接压垮数据库。它与缓存击穿的区别在于,缓存雪崩是很多key同时失效,而缓存击穿是某一个key缓存失效
  • 解决方案主要是在设置缓存的时候,可以给每个缓存的过期时间加上一个随机值,例如在原有的失效时间基础上,再加上一个1~5分钟的随机值。
  • 面试官:Redis作为缓存,MySQL的数据如何与Redis进行同步呢?(双写一致性问题)
  • 候选人:
  • 嗯,这个需要分为两种情况。
    • 保证强一致性:使用读写锁。
      • 如果是需要保证强一致性的话,可以采用Redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写、读读都互斥,这样能保证在写数据的同时不会让其他线程来读数据,避免其他线程读到脏数据。这里需要注意读方法和写方法上需要使用同一把锁。
      • 排他锁的底层使用的也是setnx,保证同时只有一个线程操作被锁住的方法
    • 然后不追求强一致性,数据同步可以有一定的延时。
      • 可以采用阿里的canal组件来实现数据同步:不需要更改业务代码,部署一个canal服务,canal服务会把自己伪装成MySQL的一个从节点,当MySQL数据更新以后,canal会读取binlog数据,然后通过canal的客户端获取到数据,更新缓存即可。
  • 面试官:Redis作为缓存,数据的持久化是怎么做的呢?
  • 候选人:
  • 在Redis中提供了两种数据持久化的方法
    1. RDB
      • RDB是一个快照文件,它是把Redis内存存储的数据写到磁盘上,当Redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
    2. AOF
      • AOF的含义是追加文件,当Redis操作写命令的时候,都会存储到这个文件中,当Redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
  • 面试官:那这两种方式,哪种恢复的比较快呢?
  • 候选人:
  • RDB因为是二进制文件,在保存的时候体积也是比较小的,所以恢复的比较快,但是它有可能会丢失数据,因为Redis内部设置了触发RDB的机制,所以可能会丢失一段时间的缓存
1
2
3
save 900 1      ## 900秒内,如果至少有1个key发生了变化,则触发一次持久化操作
save 300 10 ## 300秒内,10个key变化
save 60 10000 ## 60秒内,10000个key变化
  • AOF恢复的速度慢一些,但是它丢失数据的风险要小很多,在Redis的配置文件中也可以配置AOF的刷盘策略,采用everysec的话,最多丢失一秒的数据
配置项刷盘时机优点缺点
Always同步刷盘可靠性高,几乎不丢数据性能影响大
everysec每秒刷盘性能适中最多丢失1秒数据
no操作系统控制性能最好可靠性较差,可能丢失大量数据
  • 实际环境中,都是结合使用RDB和AOF来使用的。
  • 面试官:Redis的数据过期策略有哪些?
  • 候选人:
  • 嗯,Redis提供了两种数据过期删除策略
    • 第一种是惰性删除,在设置key过期时间后,我们不去管它,当我们需要使用该key时,我们再检查其是否过期,如果过期,我们就删除它,没过期,我们就返回该key。这种方法的缺点是,如果有大量冷数据,长时间不会被使用到,会占用内存空间,不会被及时清理掉。
    • 第二种是定期删除,每隔一段时间,我们就对一些key进行检查,删除里面已经过期的key。定期清理的两种模式
      1. SLOW模式是定时任务,执行频率默认为10HZ,每次不超过25ms,不过也可以通过Redis的配置文件来手动配置
      2. FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
    • Redis的过期删除策略,一般都是结合这二者一起来使用的。
  • 面试官:Redis的数据淘汰策略有哪些?
  • 候选人:
  • 嗯,Redis支持八种不同的策略来选择要删除的key
    1. noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
    2. volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
    3. allkeys-random:对全体key ,随机进行淘汰。
    4. volatile-random:对设置了TTL的key ,随机进行淘汰。
    5. allkeys-lru: 对全体key,基于LRU算法进行淘汰,LRU的意思是最近最少使用
    6. volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
    7. allkeys-lfu: 对全体key,基于LFU算法进行淘汰,LFU的意思是最少频率使用
    8. volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
  • 面试官:假如数据库中有1000w数据,Redis中只能缓存20w条数据,如何保证Redis中的数据都是热点数据
  • 候选人:
  • 嗯,这个问题可以从Redis的数据淘汰策略来考虑,如果需要保留热点数据,那么可以根据最近最少使用来淘汰数据,即LRU算法,这样剩下的就是热点数据了。
  • 面试官:Redis内存用完了会发生什么?
  • 候选人:
  • 嗯,这个问题得看配置的Redis数据淘汰策略是什么,如果是默认的配置,Redis内存用完以后直接报错,可以根据自己的需求来设置数据淘汰策略,我一般都是使用的LRU来淘汰数据。
  • 面试官:Redis的分布式锁如何实现
  • 候选人:
  • 嗯,Redis中提供了一个SETNX命令,即Set if Not Exists
    • 由于Redis是单线程的,所以使用该命令后,只能有一个客户端对某一个key设置值,在没有过期或删除该key之前,其他客户端是不能设置这个key的
  • 面试官:那你是如何控制Redis实现分布式锁的有效时长呢?
  • 候选人:
  • 嗯,采用Redis的SETNX命令确实不太好控制这个时长,但是可以采用Redis的一个框架,Redisson来实现。
    • 在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成时,Redisson的看门狗机制会每隔一段时间来检查当前业务是否还持有锁,如果持有锁,就增加锁的持久时间,当业务执行完成之后手动释放锁即可。
  • 面试官:Redis集群有哪些方案?
  • 候选人:
  • Redis提供的集群方案有三种:主从复制、哨兵模式、Redis分片集群
  • 面试官:那你来介绍一下主从同步
  • 候选人:
  • 嗯,是这样的,单节点的Redis并发能力有限,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写入数据,从节点负责读数据,主节点写入数据之后,需要将数据同步到从节点中。
  • 面试官:那你继续说一下主从同步的流程吧
  • 候选人:
  • 主从同步分为两个阶段,一个是全量同步,一个是增量同步
    • 全量同步是指从节点第一次与主节点建立连接的时候
      1. 从节点请求主节点同步数据,从节点会携带自己的replication id和offset偏移量
      2. 主节点判断是否是第一次请求,判断的依据是,主节点与从节点是否是同一个replication id,然后不是,那主节点就会把自己的replication id和offset发送给从节点,让主节点和从节点的信息保持一致
      3. 与此同时,主节点会执行bgsave,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发来的RDB文件,这样就保持了一致。
      4. 如果在从节点执行RDB文件的期间,仍有请求到了主节点,那么主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就保证了主节点和从节点数据完全一致,后期再进行同步数据的时候,都是依赖于这个日志文件。这个就是全量同步
    • 增量同步是指,当从节点服务重启之后,数据不一致了,这个时候,从节点会请求主节点同步数据,主节点还是先判断是不是第一次请求,判断的语句还是replication id,不是第一次请求的话,就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。
  • 面试官:怎么保证Redis的高并发高可用
  • 候选人:
  • 首先可以搭建主从集群,再加上使用Redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障回复、通知;如果主节点故障,哨兵会将一个从节点提升为主节点,当故障实例恢复正常之后,也是以最新的主节点为主。同时哨兵也充当Redis客户端的服务发现来源,当集群发生故障转移的时候,会将最新消息推送给Redis客户端,所以一般项目都会采用哨兵模式来保证Redis的高并发高可用
  • 面试官:Redis是单线程的,为什么还那么快?
  • 候选人:
  1. 首先Redis完全是基于内存的,内存的读写速度是非常快的,并且Redis是由C语言编写的
  2. 采用单线程,可以避免不必要的上下文切换的竞争条件
  3. 使用I/O多路复用模型,非阻塞IO
    • I/O多路复用指的是利用单个线程来监听多个socket,并在某个socket就绪的时候,得到通知,从而避免无效的等待,充分利用CPU资源。目前I/O多路复用都是采用的epoll模式实现,它会通知用户进程socket就绪的同时,把已就绪的socket写入用户空间,不需要挨个遍历socket来判断是否就绪。提升了性能。
  4. Redis的网络模型使用I/O多路复用结合事件处理器来应对多个socket请求,例如,提供了连接应答处理器、命令回复处理器、命令请求处理器。
    • Redis因为是基于内存的,内存读写速度非常快,所以性能瓶颈不在内存,而是在网络I/O,具体是在命令的解析这一块,假如很多的客户端都来读数据,那么他们都会携带自己的命令,Redis需要接收网络请求的数据,转化为Redis命令,那么Redis此时可能就忙不过来了。在Redis 6.0之后引入了多线程,用于解析网络请求。但是真正去执行命令的时候,还是使用的单线程。