Redis为什么这么快&如何让Redis更快

  Redis是一款使用C语言编写、可基于内存亦可持久化的日志型、Key-Value型开源数据库。Redis因自身极其优越的性能和读取速度而被广泛使用。

一、Redis为什么那么快

1.1 完全基于内存

  Redis完全基于内存,大部分都是简单的存取操作,大量的时间花费在IO上。Redis绝大部分操作时间复杂度为O(1),所以速度十分快。

1.2 非阻塞IO、多路IO复用模型

  Redis采用多路IO复用模型,在内部采用epoll代理。多路是指多个网络连接,IO复用是指复用同一个线程。epoll会同时监察多个流的IO事件,在空闲时,当前线程进入阻塞,如果有IO事件时,线程会被唤醒,并且epoll会通知线程是哪个流发生了IO事件,然后按照顺序处理,减少了网络IO的时间消耗,避免了大量的无用操作。

1.3 单线程

  对于单线程来讲,不存在上下文切换问题,也不用考虑锁的问题,不存在加锁释放锁的操作,没有因为可能出现死锁而导致的性能消耗。虽然单线程无法发挥出多个CPU的性能,但是可以在单机开启多个Redis实例解决这个问题。reids的单线程是指处理网络请求只有一个线程。

1.3.1 上下文切换造成的影响

  上下文切换分为很多种,在这里我就不一一介绍了,简单说一下上下文切换的意义和影响。在多任务操作系统中,我们运行的任务远远大于CPU的个数,但是看起来确实可以同时执行,这是因为计算机通过CPU寄存器和程序计数器记录了进程、线程等任务的位置,从而实现保存任务的位置,以及再次运行任务,让任务看起来是连续运行的。
  看起来上下文切换是利于我们工作的,那么它存在什么影响呢。每次上下文切换都需要花费几十纳秒到数微秒的CPU时间,也就是说如果频繁的进行上下文切换会导致CPU大部分时间被浪费。

1.3.2 Redis为什么没有“锁”

  在关系型数据库中,会通过加锁来保证数据的一致性,这种锁被称为悲观锁。Redis为了近可能的减少客户端等待,使用WATCH命令对数据加锁,只会在数据被其他客户端修改时,才会通知执行WATCH的客户端,之后的事务不会执行。这种加锁方式被称为乐观锁,极大的提升了Redis的性能。

1.3.3 阿里云的Redis

  如果业务的QPS超过单个Redis server的能力上限,并且又使用了集群版本无法支持的命令时。业务拆分是一个选择,但这设计到业务的改造,成本会比较高。这个时候阿里云的reids多线程性能增强版本就是一个比较好的选择。
  阿里云Redis通过增加IO线程,将连接中数据的读写,命令的解析和数据包的回复放到单独的IO线程来处理,而对命令的处理,定时器事件的执行等仍让单一的线程来处理,以此达到提高单Redis server吞吐的目的。

1.4 数据结构简单

  数据结构设计简单,对数据的操作也简单,Redis中的数据结构是专门进行设计的。Redis的数据结构有简单动态字符串、链表、字典、跳跃表、整数集合、压缩字典。

1.4.1 简单动态字符串

  Redis并没有使用C语言的字符串,而是使用了简单动态字符串(SDS)。相对于C语言的字符串来讲,SDS记录了自身使用和未使用的长度,时间复杂度为O(1),而C语言则要遍历整个空间,时间复杂度为O(N)。

/* Redis简单动态字符串的数据结构 */
struct sdshdr {
    //字符长度,记录buf数组中已使用的字节数量
    unsigned int len;
    //当前可用空间,记录buf数组中未使用的字节数量
    unsigned int free;
    //具体存放字符的buf
    char buf[];
};

  SDS还会利用自身的长度取检查空间是否足够来满足将来的需求。另外,SDS还会利用自身未使用空间来实现空间的预分配和惰性空间释放的优化。

  • 空间预分配就是指当SDS进行空间扩展时,reids会为SDS所需空间和额外的未使用空间,来为下次扩容提供空间。
  • 惰性空间释放是指当SDS要收缩时,Redis会将收缩的部分回收以便将来使用。
    SDS可以通过自身长度来判断字符串是否结束,这样可以实现二进制数据的存储。

1.4.2 链表

  Redis的链表为双端链表,链表节点带有perv和next指针,链表还带有head和tail指针,使得获取链表某节点前后置节点的时间复杂度都是O(1)。并且Redis链表无环,prev和next指针指向null,对链表的访问以null作为截至的判断条件。
  链表中有记录自身长度的属性len,并且链表使用void*指针来保存节点值,可以通过list 结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用来保存各种不同类型的值。

typedef struct list{
    //表头节点
    listNode  * head;
    //表尾节点
    listNode  * tail;
    //链表长度
    unsigned long len;
    //节点值复制函数
    void *(*dup) (void *ptr);
    //节点值释放函数
    void (*free) (void *ptr);
    //节点值对比函数
    int (*match)(void *ptr, void *key);
}

1.4.3 字典

  字典由哈希表组成,而哈希表又由哈希结点组成。

typeof struct dictEntry{
   //键
   void *key;
   //值
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}
#每一个dictEntry都对应一个键值对
typedef struct dictht {
   //哈希表数组
   dictEntry **table;
   //哈希表大小
   unsigned long size;
   //哈希表大小掩码,用于计算索引值
   unsigned long sizemask;
   //该哈希表已有节点的数量
   unsigned long used;
}
#通过sizemark和哈希值一起绝对一个键应该被分配到数组的哪一个索引上。
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privedata;
    // 哈希表
    dictht  ht[2];
    // rehash 索引
    int rehashidx;
}
#type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。

1.4.4 跳跃表

  跳跃表是一种有序数据结构,通过在每个结点中维持多个指向其它结点的指针,从而达到快速访问结点的目的。Redis中在有序集合键和集群结点中的内部数据结构都用到了跳跃表。

1.4.5 整数集合

  Redis用于保存整数值的集合抽象数据结构,它可以保存类型为 int16_t、int32_t 或者int64_t的整数值,并且保证集合中不会出现重复元素。

1.4.6 压缩列表

  压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意个结点,每个结点可以保存一个字节数或者一个整数值。

1.5 底层模型不同

  Redis使用的底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
  Redis采用了RDB文件和VM机制来分别实现二进制存储、冷热淘汰的功能。

1.6 Redis优秀的过期策略和内存淘汰机制

1.6.1 定期删除

  定期删除是Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。

1.6.2 惰性删除

  在获取某个key的时候,Redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何结果。并不是key到时间就被删除掉,而是你查询这个key的时候,Redis再懒惰的检查一下。
  通过上述两种手段结合起来,保证过期的key一定会被干掉。

1.6.3 内存淘汰机制

  Redis的内存淘汰机制有六种:

volatile-lru:内存不足时,删除设置了过期时间的键空间中最近最少使用的key
allkeys-lru:内存不足时,在键空间中删除最少使用的key
volatile-random:内存不足时,随机删除在设置了过期时间的键空间中的key
allkeys-random:内存不足时,随即删除在键空间中的key
volatile-ttl:内存不足时,在设置了过期时间的键空间中,优先移除更早过期时间的key
noeviction:永不过期,返回错误

  在以上的淘汰策略中,使用allkeys-lru较好。

二、如何使Redis运行的更快

2.1 Redis问题

1、Redis作为缓存时的双写一致性问题
  Redis作为缓存使用时,涉及到一个问题,就是缓存中的数据和数据库不一致。在分布式系统中,一致性一般分为两种:强一致和最终一致性,Redis保证的就是最终一致性。那么Redis如何保证的最终一致性呢?
  1、给缓存设置过期时间,过期就读数据库的数据。
  2、先更新数据库,然后删除缓存。
2、缓存雪崩问题
  雪崩问题的意思就是大量缓存的过期时间设为一致,然后同一时间失效,这样就会同时读数据库,给数据造成巨大的压力。
  所以,我们在设置缓存过期时间的时候,要加一个随机值来保证过期时间的不一致。
3、缓存穿透问题
  缓存穿透就是恶意查询不存在的数据,然后去查询数据库,导致数据库连接异常。
  采用异步更新,如果缓存不存在,异步起一个线程去读数据库,然后更新缓存。或者利用互斥锁,当缓存失效的时候,不能直接访问数据库,而是要先获取到锁,才能去请求数据库。没得到锁,则休眠一段时间后重试。
4、缓存并发竞争问题
  缓存并发竞争问题就是多个客户端对同一个key进行操作导致的问题。
  可以通过设置一个消息队列,按照先后执行命令,或者通过分布式锁来绝对,谁持有锁谁先执行。

2.2 Redis优化

1、尽量使用短的key
  在表达其意义的基础上,尽量的短.
2、避免使用keys和模糊查询操作
  keys *和模糊查询会导致阻塞,如果有需求可以使用SCAN代替。
3、设置key有效期
  给key设置有效期可以避免很多问题,如:Redis作为缓存时的双写问题,保证缓存和数据库的一致性。另外设置缓存过去时间时不要设置为一样,否则会同时把压力压到数据库。
4、尽可能地使用哈希存储
5、持久化最好在备库做
6、多条命令使用管道
  使用管道可以降低网络的开销。
7、限制Redis的内存大小
  Redis在bgsvae的时候,会fork子进程,而这个操作需要拷贝父进程的空间内存页表,会耗费一定的时间。所以Redis的内存最好控制在10G以内
8、尽可能使用SSD
  reids在持久化的时候首先会写入aof buffer,再进行fsync。这时,主线程每次进行AOF会对比上次fsync成功的时间;如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成。因此,如果系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞;此外,使用everysec配置,AOF最多可能丢失2s的数据,而不是1s。所以应使用SSD,提高磁盘读写速度。
9、开启slowlog
  开启slow可以帮助我们定位数据库性能问题。

slowlog-log-slower-than 代表慢查询的阈值,单位为:微秒。当执行查询命令消耗时间大于配置的阈值时,会将该条命令记录到慢查询日志。当 slowlog-log-slower-than=0 时,记录所有命令。slowlog-log-slower-than<0 时,不记录任何命令。slowlog-log-slower-than 的默认值为 10000 (10毫秒,1秒 = 1,000毫秒 = 1,000,000微秒)。
slowlog-max-len 代表慢查询日志最大条数。它是一个队列形式的存储结构,先进先出的队列,即当慢查询日志达到最大条数后,会销毁最早记录的日志条目。slowlog-max-len 的默认值为 128,保存在内存内,所以重启 Redis 会清空慢查询日志。
上一篇:MinIO存储服务客户端使用指南(三)


下一篇:MongoDB 安装和可视化工具