Redis为什么这么快?

133 阅读5分钟

Redis性能高的原因, 总结:

  • 纯内存操作
  • 合理的线程模型
  • 高效的数据结构
  • 合理的数据编码
  • 虚拟内存机制

一、纯内存操作

  • 内存读写是比磁盘读写快很多的。
  • Redis是基于内存实现的数据库, 相对于数据存在磁盘的数据库, 省去磁盘磁盘I/O的消耗。
  • MySQL等磁盘数据库, 需要建立索引来加快查询效率, 而Redis数据存放在内存, 直接操作内存, 所以就很快。

二、合理的线程模型

1. 单线程模型: 避免了上下文切换。

  • Redis是单线程的, 其实是指Redis的网络IO和键值对读写是由一个线程来完成的。但Redis的其他功能, 比如持久化、异步删除、集群数据同步等等, 实际是由额外的线程执行的。

  • Redis的单线程模型, 避免了CPU不必要的上下文切换竞争锁的消耗

  • 也正因为是单线程, 如果某个命令执行过长(如hgetall命令), 会造成阻塞。Redis是面向快速执行场景的内存数据库, 所以要慎用如lrange和smembers、hgetall等命令。

Redis6.0引入多线程的原因?

2. I/O多路复用

  • 什么是I/O多路复用?
    • I/O: 网络I/O;
    • 多路: 多个网络连接;
    • 复用: 复用同一个线程;

I/O多路复用其实就是一种同步I/O模型, 它实现了一个线程可以监视多个文件句柄, 一旦某个文件句柄就绪, 就能够通知应用程序进行相应的读写操作; 而没有文件句柄就绪时, 就会阻塞应用程序,交出cpu。

image.png

多路I/O复用技术可以让单个线程高效的处理多个连接请求, 而Redis使用用epoll作为I/O多路复用技术的实现。并且Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件, 不在网络I/O上浪费过多的时间。

三、高效的数据结构

1. 简单动态字符串(SDS)

//SDS简单动态字符串
struct sdshdr{
    int len;  //buf已使用的长度;
    int free; //buf剩余长度;
    char buf[];//实际存储内容;
}

image.png

  • 字符串长度查询
    • c语言需要从头遍历, 时间复杂度O(N);
    • Redis直接返回len, 时间复杂度O(1);
  • 扩容 - 减少内存重新分配次数
    • 在c中, 对字符串的修改, 需要重新分配内存, 修改越频繁, 内存分配就越频繁, 复杂度为O(N);
    • SDS提供了两种优化策略:
      • 空间预分配;
      • 惰性空间释放;
    • 空间预分配:
      • 当SDS修改和空间扩充时, 除了分配必需的内存空间, 还会额外分配未使用的空间。分配规则如下:
        • SDS修改后, len的长度小于1M, 那么将额外分配与len相同长度的未使用空间。比如len=100。重新分配后,buf 的实际长度会变为100(已使用空间)+100(额外空间)+1(空字符)=201;
        • SDS修改后, len长度大于1M,那么程序将分配1M的未使用空间;
    • 惰性空间释放:
      • 当SDS缩短时候, 不是回收多余的内存空间, 而是使用free记录剩余的内存空间。后续再有修改操作, 直接使用free空间, 减少内存分配次数。

2. 哈希

  • Redis 作为一个 k-v 的内存数据库, 它使用一张全局的哈希表来保存所有的键值对。这张哈希表, 由多个哈希桶组成, 哈希桶中的entry元素保存了*key*value指针,其中*key指向了实际的键,*value指向了实际的值。
  • 用链表解决哈希冲突。

image.png

3. 跳跃列表(skiplist)

  • 跳跃列表是在链表的基础上, 增加多级索引, 以提高查询效率。
    • 每一层都有一条有序的链表, 最底层的链表包含了所有的元素;
    • 跳跃列表支持最好O(logN), 最坏O(N)时间复杂度的节点查找, 还可以通过顺序性操作批量处理节点。

image.png

4. 压缩列表(ziplist)

  • 由于内存空间是连续的, 所以遍历效率很快。
  • 压缩列表ziplist是列表键和字典键的的底层实现之一。

image.png

四、合理的数据编码

  • Redis支持多种数据基本类型, 每种基本类型对应不同的数据结构, 每种数据结构对应不一样的编码。为了提高性能, Redis设计者总结出了数据结构最适合的编码搭配。
  • Redis是使用对象(redisObject)来表示数据库中的键值对, 当我们在 Redis 中创建一个键值对时,至少创建两个对象, 一个对象是用做键值对的键对象, 另一个是键值对的值对象。
typedef struct redisObject{ 
    unsigned type:4;//类型;
    unsigned encoding:4; //编码;
    void *ptr; //指向底层数据结构的指针;
    //... 
    }robj;
typeencoding
String如果存储数字, 用int类型的编码;如果存储非数字, 小于等于39字节的字符串, 用embstr类型编码; 大于39个字节, 则是raw编码。
List如果列表的元素个数小于512个, 且列表每个元素的值都小于64字节(默认), 使用ziplist编码, 否则使用linkedlist编码。
Set如果集合中的元素都是整数且元素个数小于512个, 使用intset编码; 否则使用hashtable编码。
Zset当有序集合的元素个数小于128个, 每个元素的值小于64字节时, 使用ziplist编码, 否则使用skiplist(跳跃表)编码。

五、虚拟内存机制

  • Redis虚拟内存机制是什么?
    • 虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中, 从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。
    • 通过VM功能可以实现冷热数据分离, 使热数据仍在内存中、冷数据保存到磁盘。
    • 这样就可以避免因为内存不足而造成访问速度下降的问题。
    • 需要特别注意的是Redis并没有使用OS提供的Swap, 而是自己实现。
    • www.codenong.com/cs106843764…