redis夺命连环问1--为什么单线程Redis能那么快?

目录

  • 为什么单线程的 Redis 能那么快?(概括)
    • 1.先谈单线程是啥
    • 2.再谈单线程用来干啥
    • 3.再谈Redis的单线程怎么用
    • 4.再谈它是内存数据库并有高效数据结构
  • 刚刚你说到了高效的数据结构,能具体讲讲redis是怎么实现的吗?
    • 1.数据类型方面:可以谈一下Hash
    • 2.底层数据结构方面:可以谈压缩列表、跳表
    • 3.String数据类型和它具体实现也可扯一波

为什么单线程的 Redis 能那么快?(概括)

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;

  • 数据结构简单,对数据操作也简单;

  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

  • 使用多路 I/O 复用模型,非阻塞 IO。

1.先谈单线程是啥

首先咱得知道Redis是单线程。主要是指 Redis的网络 IO 和键值对读写是由一个线程来完成,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化等其实是由额外线程执行的。

2.再谈单线程用来干啥

其次咱得知道Redis 为什么用单线程?其实主要是为了避免多线程的开销问题。为了避免多线程编程面临的共享资源的并发访问控制问题,锁的控制以及死锁问题导致的性能开销,为了系统代码的易调试性和可维护性。

3.再谈Redis的单线程怎么用

最后 咱得知道Redis 是采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

  • 先说阻塞模式下三个网络IO可能的阻塞点,监听请求(bind/listen),建立连接(accept),读取请求(recv)会导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。所以redis采用非阻塞模式,当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。
  • 基于多路复用的高性能 I/O 模型,就是一个线程处理多个 IO 流。简单来说,在 Redis 单线程的情况下,允许内核中存在多个监听套接字和已连接套接字。内核监听套接字上的请求。一旦请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。


redis夺命连环问1--为什么单线程Redis能那么快?_第1张图片

  • 再说回调机制,select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。事件会被放进一个队列,Redis 单线程对该事件队列不断进行处理调用相应的处理函数。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费,提升 Redis 的响应性能。

4.再谈它是内存数据库并有高效数据结构

埋坑 Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。


刚刚你说到了高效的数据结构,能具体讲讲redis是怎么实现的吗?

首先 谈谈redis值有五种数据类型,
String(字符串)可以做简单的键值对缓存、
List(列表)可以存储一些列表型的数据结构、
Hash(哈希)可以存结构化的数据,比如一个对象、
Set(集合)可以做交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集、
Sorted Set(有序集合)去重但可以排序,如获取排名前几名的用户。

它们实现关系如下:
redis夺命连环问1--为什么单线程Redis能那么快?_第2张图片
就随便谈谈String 类型的底层实现只有一种数据结构,简单动态字符串。Hash是当数据量较小时用压缩列表实现,较大时用哈希表实现并且不可逆就行了。

1.数据类型方面:可以谈一下Hash

首先一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
redis夺命连环问1--为什么单线程Redis能那么快?_第3张图片
优点:
O(1) 的时间复杂度快速查找到键值对
缺点:
哈希表的冲突问题和 rehash 可能带来的操作阻塞。

  • Redis 中写入大量数据后,就可能发现操作有时候会突然变慢
    为什么哈希表操作变慢了?
    哈希冲突,也就是指,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。
    方案一
    拉链法。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
    方案二
    rehash
    过程分为三步:
    1.给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
    2.把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
    3.释放哈希表 1 的空间。
    方案三
    方案二第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。
    渐进式 rehash
    简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

2.底层数据结构方面:可以谈压缩列表、跳表

首先 redis集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈希表、压缩列表和跳表。

  • 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
    redis夺命连环问1--为什么单线程Redis能那么快?_第4张图片
  • 跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位
    redis夺命连环问1--为什么单线程Redis能那么快?_第5张图片

可以谈谈复杂度
redis夺命连环问1--为什么单线程Redis能那么快?_第6张图片

  • 单元素操作是基础(指每一种集合类型对单个数据实现的增删改查操作);
  • 范围操作非常耗时(这类操作的复杂度一般是 O(N),比较耗时,我们应该尽量避免。);
  • 统计操作通常高效(集合类型对集合中所有元素个数的记录);
  • 例外情况只有几个(压缩列表和双向链表都会记录表头和表尾的偏移量。)。

3.String数据类型和它具体实现也可扯一波

  • 谈谈String 类型内存开销大的问题
    RedisObject(embstr 编码方式或raw 编码模式:挨着或指着的SDS有元信息开销、int 编码方式(long):long无元信息开销):
    有元信息和指针开销

  • 用什么数据结构可以节省内存
    压缩列表:表头有列表长度、列表尾的偏移量、列表中的 entry 个数。列表尾还有 zlend表示列表结束。
    redis夺命连环问1--为什么单线程Redis能那么快?_第7张图片
    为什么呢?
    一、它是用一系列连续的 entry 保存数据,entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。
    二、Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的集合类型,节省了 dictEntry 的开销。当你用 String 类型时,一个键值对就有一个 dictEntry。但采用集合类型时,一个 key 就对应一个集合的数据,能保存的数据多了很多,但也只用了一个 dictEntry,这样就节省了内存。

  • 那怎么用压缩列表保存单值的键值对呢?
    用基于 Hash 类型的二级编码方法
    分配某字段前几位作键,然后把后几位作为 Hash 集合的 key,对应的另一个字段作 Hash 集合的 value
    二级怎么用?
    二级编码方法中采用的 位的长度是有讲究的。
    涉及到一个问题–Hash 类型底层结构小于设定值时使用压缩列表,大于设定值时使用哈希表。
    一旦从压缩列表转为了哈希表,Hash 类型会一直用哈希表进行保存,而不会再转回压缩列表。
    在节省内存空间方面,哈希表就没有压缩列表那么高效。为能充分使用压缩列表的精简内存布局,一般要控制保存在 Hash 中的元素个数。

你可能感兴趣的:(redis夺命连环问系列,redis)