摸鱼聊面试——Redis(一)

208 阅读10分钟

本文仅用作本人的笔记和复习。

为什么要上Redis

主流应用架构 一般来说,客户端会先想缓存层请求数据。如果缓存层存在数据,则直接返回。如果缓存层没有该数据,那么就会继续向存储层访问数据,即穿透查询。在存储层得到数据后,再把数据“回种”回缓存层,再由缓存层返回给客户端。如果存储层承受不住访问压力,就会直接由缓存层直接返回数据。

由此可见,有个缓存层Redis,是很必要的。

Redis优点

  • 数据类型丰富
  • 支持数据磁盘持久化存储
  • 支持主从
  • 支持分片

为什么Redis这么快?

Redis可以支持100000+QPS(QPS query per second,每秒内查询次数) 原因:

  • 完全基于内存,绝大部分请求是内存操作,不受硬盘IO的限制
  • 数据结构简单,不使用表,不会要求用户对数据进行关联。
  • 采用单线程,客户端对数据的访问只有一个单线程,避免了多线程的上下文切换。
  • 采用多路I/O复用模型,非阻塞IO

关于多路I/O复用模型

要了解多路I/O复用模型,我们首先要先了解FD(File Descriptor,文件描述符)。

File Descriptor

在操作系统中,一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射。在Linux中,FD用一个整数表示。

Select系统调用

在Linux中,由Select系统调用可以同时监控多个文件描述符是否可读、可写。当其中某些文件符可读或可写时,Selector就会返回可读可写的文件符的个数。

监听的任务交给SELECT之后,程序就可以去做其他事情而不被阻塞了。

Redis采用的I/O多路复用函数,有epoll/kqueue/evport/select等。但是Redis自身会因地制宜地去选择最合适的复用函数,优先选择时间复杂度为O(1)的I/O多路复用函数作为底层实现。但是由于几乎所有的Linux系统都实现了select函数,所以Redis会使用复杂度为O(n)的select作为保底。

Redis是基于react设计模式监听I/O事件

聊聊你用过的Redis的数据类型

  • String:最基本的数据类型,其二进制安全。可以包括jpg图片或者序列化的对象。

  • Hash:String元素组成的字典,适合用于存储对象

  • List:列表,按照String元素插入顺序排序

  • Set:String元素组成的无序集合,通过哈希实现,不能重复插入

  • Sorted Set:通过分数来为集合中的成员进行从小到大的排序

底层数据类型基础

1. 简单动态字符串
2. 链表
3. 字典
4. 跳跃表
5. 整数集合
6. 压缩列表
7. 对象

有关这部分的实现可以参照《Redis的实现与设计》。

Redis如何从海量Key里查询出某一固定前缀的Key

首先要清楚数据规模。

然后可以使用KEYS pattern来查找。 KEYS patter:查找所有适合给定模式patter的key。 如使用keys k1* 就可以返回以k1开头的数据。但是,KEYS指令是一次性返回所有匹配的key,如果key的数量过大会使服务卡顿。

而SCAN可以无阻塞地取出指定的key。 如: SCAN cursor [MATCH pattern] [COUNT count]

SCAN是一个基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。 当curosor游标为0时,服务器将开始新的迭代。当服务器向用户返回游标0时,则表示遍历结束。 而SCAN不保证每次执行都返回某个给定数量的元素,支持模糊查询。 一次返回的数量是不可控的,只能大概率返回符合count参数的数量。

如何通过Redis实现简单的分布式锁

分布式锁是控制在分布式系统中不同系统访问共享资源的一种实现。如果在分布式系统中系统访问共享资源,往往要互斥,以防止互相干扰一致性。 窃以为,该文写得不错:juejin.cn/post/684490…

分布式锁需解决的问题

  • 互斥性 同一时刻只能有一个系统获取锁。

  • 安全性 锁只能被持有该锁的客户端删除。

  • 死锁 获取锁的客户端由于某些原因宕机,其他系统获取不到该锁。需要避免此问题。

  • 容错 当部分redis节点宕机时,系统仍能获取锁和删除锁。

具体实现

SETNX + EXPIRE

在Redis里面可以使用SETNX key value。 SETNX key value:如果key不存在,则创建并赋值。 时间复杂度:O(1) 返回值:设置成功,返回1;失败则返回0。 然后,我们就要给setnx设置的值给一个过期时间。 这里可以使用EXPIRE指令。 EXPIRE key seconds:设置key的生存时间,当key过期时(生存时间为0),会被自动删除。 但是,使用SETNX + EXPPIRE的缺点,是无法实现原子性的。

SET

现在的Redis版本把SETNX和EXPIRE糅合成了SET操作,从而保证了原子性。 使用格式: SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX seconds:设置键的过期时间为seconds秒。
  • PX millisecond:设置键的过期事件为milliseconds毫秒。
  • NX :只在键不存在时才对键进行设置操作
  • XX :只在键已经存在时,才对键进行设置操作

SET操作成功完成时,会返回OK,否则会返回nil。

示例: set locktarget 12345 ex 10 nx

大量的Key同时过期需要注意的问题

如果大量的key集中过期,由于清楚大量的key很耗时,会出现短暂的卡顿现象。 这种解决方案也很简单,可以尝试在设置Key的过期时间的时候,给每个Key的过期时间加上随机值,从而让过期时间分散一些,从而可以在很大程度上避免卡顿现象的发生。

明天继续更新。

Redis如何实现异步队列

我们可以使用List作为队列,RPUSH生产消息,LPOP消费消息。

当我们使用lpop没能得到数据,则说明消息已经被消费完毕,而生产者仍没有生产新的数据。 缺点:没有等待队列里有值就直接消费 弥补:在应用层引入Sleep机制去调用LPOP重试

如果不希望引入Sleep机制,我们还可以使用BLPOP指令。

BLPOP指令

BLPOP key [key ...] timeout :阻塞知道队列有消息或者超时 测试: 打开两个redis-cli客户端。 在第一个redis-cli输入:


blpop testlist 30

那么它会在三十秒内等待testlist出现数据。 然后我们再在第二个redis-cli输入:

rpush testlist aaa

此时,在第一个redis-cli便会出现:

127.0.0.1:6379> blpop testlist 30
1) "testlist"
2) "aaa"
(26.85s)

这就说明第一个redis-cli是可以获取到第二个redis-cli的数据的。 缺点:只能供一个消费者消费。

pub/sub:主题订阅者模式

我们使用pub/sub:主题订阅者模式便可以实现一对多的消费队列。

  • 发送者(pub)发送消息,订阅者(sub)接收消息
  • 订阅者可以订阅任意数量的频道 Publisher发送消息给Topic,那么订阅了该Topic的三个客户端就可以得到该消息。

示例: 打开第一个redis-cli,输入:

127.0.0.1:6379> subscribe myTopic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myTopic"
3) (integer) 1

这样便是订阅了一个频道。redis-cli直接订阅频道即可。频道无需预先设立。 打开第二个redis-cli,也同样输入:

127.0.0.1:6379> subscribe myTopic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myTopic"
3) (integer) 1

这样,第一个和第二个redis-cli都是订阅了同一个频道。

然后我们再打开第三个redis-cli,输入:

127.0.0.1:6379> subscribe anotherTopic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "anotherTopic"
3) (integer) 1

订阅了一个叫anotherTopic的新频道。

最后我们再打开第四个redis-cli,向myTopic发送消息,输入:

127.0.0.1:6379> publish myTopic "Hello"
(integer) 2

这样,我们可以发现,第一第二个redis-cli确实可以接收到消息。

127.0.0.1:6379> subscribe myTopic
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myTopic"
3) (integer) 1
1) "message"
2) "myTopic"
3) "Hello"

然而pub/sub的缺点,也很明显。其消息的发布是无状态的,无法保证让消费者得到信息。 这就只能通过专门的消息队列Kafka来解决了。

Redis持久化

一旦服务器退出,Redis的数据就会丢失。因此Redis会尝试把数据保存到磁盘中,避免数据丢失。

一般有三种:

RDB

RDB(快照)持久化:在一个特定的时间端保存某个时间点的全量数据快照。 我们可以看到Redis文件夹内的redis.conf里的:

save 900 1
save 300 10
save 60 10000

这个意思表示,在900秒内如果有1条数据写入,就保存快照。在300秒内如果有10条数据写入就保存快照。在60秒内如果有10000条数据写入就保存快照。

这是为了平衡性能与数据安全来进行合理配置。

另一个比较重要的是

stop-writes-on-bgsave-error yes

这表示,当快照保存失败,redis进程就拒绝写入新的操作了。这是为了保护持久化一致性的问题。

rdbcompression yes

这表示备份的时候,要将rdb文件压缩之后再进行保存。这个一般建议设置为no。

要禁用RDB,可以在SAVE 300 100000下面加入save ""。如下:

save 900 1
save 300 10
save 60 10000
save ""

在Redis文件夹内,有一个dump.rdb。这个文件就是我们保存的快照。

要操作RDB,我们有两个命令:

  • SAVE :阻塞Redis的服务器进程,知道RDB文件被创建完毕。

  • BGSAVE :Fork出一个子进程来创建RDB文件,来记录BGSAVE当时的数据库状态。父进程继续处理接收到的命令。子进程完成 文件创建之后,会发送信号给主进程。与此同时,主进程也会通过轮训的方式来接收子进程的信号。BGSAVE使用后台的方式来处理rdb文件。不阻塞服务器进程。 执行BGSAVE之后,会立即返回"ok"。我们可以使用LASTSAVE来获取上次执行SAVE的时间。

自动触发RDB持久化

  • 根据redis.conf配置里的SAVE m n 定时触发(实际上是BGSAVE)

  • 主从复制时,主节点主动复制

  • 执行Debug Reload

  • 执行Shutdown且没有开启AOF持久化

BGSAVE原理

执行BGSAVE,会先检查是否存在正在进行AOF/RDB的子进程。如果存在,则返回错误。不存在,触发持久化。触发持久化,就是调用函数rdbSaveBackground(),再执行fork系统调用。

系统调用fork():创建进程,实现了Copy-on-Write。

AOF

持久化

Pipeline及主从同步

Redis集群