器 | Redis知识体系整理

919 阅读9分钟

Redis知识整理

学习研究《Redis深度历险:核心原理和应用实践》这本书,系统性整理感兴趣的Redis相关的知识,姑且算作读书笔记吧。

一、数据结构

1.string

动态字符串,内部结构类似于ArrayList,实则为bitmap「位图」,其采取预分配冗余空间策略减少内存的频繁分配。最大长度为512M。

指令:

setgetexpire,setex,setnx

扩容策略:

字符串长度小于1M的时候,扩容是加倍现有空间。超过1M,每次扩容增加1M。

用途:

2.list

相当于LinkedList,实则为快速链表 quicklist, 插入和删除操作非常快。时间复杂度为 O(1),但是索引定位很慢,时间复杂度为O(n)。当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

指令:

rpushrpop

数据结构:

列表元素较少的时,使用连续的内存存储,这个结构是zipList,即压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。

比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next

所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

用途:

常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理,类似于栈。

3.hash

字典

相当于Hashmap,为无序字典。数据结构为数组+链表。

数据结构:

数据+链表,Redis字典的值只能为字符串,且当字典的值过大的时候,它的rehash过程与hashmap不一致。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

用途:

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

缺点:

hash 结构的存储消耗要高于单个字符串。

4.set

类似于HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。

用途:

set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

5.zset

有序列表

类似于sortedSet + Hashmap,一方面因为是set,所以保证了内部value的唯一性。另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

数据结构:

跳跃列表。

用途

  • zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
  • zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。

二、用途

1.分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。Redis 分布式锁不要用于较长时间的任务

** 指令 **

setnx,del,expire

2.延时队列

Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

解决pop空读,首先可以采取加休眠时间。最好使用blpop/brpop,也就是阻塞读。阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。当然如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常来。

延时队列可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取到期的任务进行处理。

3.位图

按位存取value,节省空间。

4.HyperLogLog

HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加 计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用 户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。

5.布隆过滤器

一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,它可能会误判。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

三、原理

Redis作为一个单线程模型,为什么如此之优秀?答案是细节,基于全部内存级别的运算,采用多路复用的线程模型,解析性能极好的文本通信协议,极大减少通信交互次数的管道,适应不同场景的快照和AOF双持久化机制,以及五大优秀的基本数据结构等等。总的来说,该它如此过分的优秀。

1.线程I/O模型

Redis是单线程程序。所有的数据都在内存中,所有的运算都是内存级别的运算。

None-Blocking + 事件轮询(多路复用)。非阻塞读解决传统IO读写阻塞的问题,事件轮询解决何时读的问题。现代操作系统的多路复用 API 已经不再使用 select 系统调用,而改用 epoll(linux)和 kqueue(freebsd & macosx),因为 select 系统调用的性能在描述符特别多时性能会非常差。

2.通信协议

RESP(Redis Serialization Protocol)是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。

3.管道

读->写 ->读->写 转换成 读 ->读->写->写,节省网络开销。实则由Redis客户端是去实现的。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省 IO 时间。管道中指令越多,效果越好。实质为客户端改写读写的先后顺序。

4.事务

为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis 也不例外。Redis 的事务使用非常简单,不同于关系数据库,我们无须理解那么多复杂的事务模型,就可以直接使用。不过也正是因为这种简单性,它的事务模型很不严格,这要求我们不能像使用关系数据库的事务一样来使用 Redis。

5.小道消息pubsub

pubsub主要是用来支持消息多播,多播就是生产者生产一次消息,中间件负责将消息复制到多个消息队列,每个消息 队列由相应的消费组进行消费。redis的五大基本类型并不支持多播,要单独使用了一个模块来支持消息多播,这个模块的名字叫着 PubSub,也就是 PublisherSubscriber,发布者订阅者模型。

6.同步备份

增量同步和快照同步

增量同步(AOF)

增量同步的是对状态产生修改性影响的指令流。先同步到本地的Buffer,然后再异步的传输给从节点,从节点一般同步主节点状态,一般想主节点反馈同步的偏移量。

Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。所以在网络通信不佳的情况下, 可能会导致还未被同步的数据被覆盖掉。所以需要快照同步。

快照同步(RDB)

快照同步是在主库上进行一次 bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点接收到数据完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。

快照同步极易产生死循环,所以务必配置一个合适的复制 buffer 大小参数,避免快照复制的死循环。