本文仅用作本人的笔记和复习。
为什么要上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。