Redis都做了哪些加快速度的设计

列表对象是 Redis5 种基础数据类型之一,在 Redis 3.2 版本之前,列表对象底层存储结构有两种:linkedlist(双端列表)和 ziplist(压缩列表),而在 Redis 3.2 版本之后,列表对象底层存储结构只有一种:quicklist(快速列表),难道通过精心设计的 ziplist 最终被 Redis 抛弃了吗?

列表对象

同字符串对象一样,列表对象到底使用哪一种数据结构来进行存储也是通过编码来进行区分:

编码属性 描述 object encoding命令返回值
OBJ_ENCODING_LINKEDLIST 使用 linkedlist 实现列表对象 linkedlist
OBJ_ENCODING_ZIPLIST 使用 ziplist 实现列表对象 ziplist
OBJ_ENCODING_QUICKLIST 使用 quicklist 实现列表对象 quicklist

linkedlist

linkedlist 是一个双向列表,每个节点都会存储指向上一个节点和指向下一个节点的指针。linkedlist 因为每个节点之间的空间是不连续的,所以可能会造成过多的内存空间碎片。

linkedlist存储结构

链表中每一个节点都是一个 listNode 对象(源码 adlist.h 内),不过需要注意的是,列表中的 value 其实也是一个字符串对象,其他几种数据类型其内部最终也是会嵌套字符串对象,字符串对象也是唯一一种会被其他对象引用的基本类型:

typedef struct listNode {
  struct listNode *prev;//前一个节点
  struct listNode *next;//后一个节点
  void *value;//值(字符串对象)
} listNode;

然后会将其再进行封装成为一个 list 对象(源码 adlist.h 内):

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

Redis 中对 linkedlist 的访问是以 NULL 值为终点的,因为 head 节点的 prev 节点为 NULLtail 节点的 next 节点也为 NULL,所以从头节点开始遍历,当发现 tailNULL 时,则可以认为已经到了列表末尾。

当我们设置一个列表对象时,在 Redis 3.2 版本之前我们可以得到如下存储示意图:

ziplist

压缩列表在前面已经介绍过,想要详细了解的可以点击这里。

linkedlist 和 ziplist 的选择

Redis3.2 之前,linkedlistziplist 两种编码可以进选择切换,如果需要列表使用 ziplist 编码进行存储,则必须满足以下两个条件:

列表对象保存的所有字符串元素的长度都小于 64 字节。列表对象保存的元素数量小于 512 个。

一旦不满足这两个条件的任意一个,则会使用 linkedlist 编码进行存储。

PS:这两个条件可以通过参数 list-max-ziplist-valuelist-max-ziplist-entries 进行修改。

这两种列表能在特定的场景下发挥各自的作用,应该来说已经能满足大部分需求了,然后 Redis 并不满足于此,于是一场改革引发了,quicklist 横空出世。

quicklist

Redis 3.2 版本之后,为了进一步提升 Redis 的性能,列表对象统一采用 quicklist 来存储列表对象。quicklist存储了一个双向列表,每个列表的节点是一个 ziplist,所以实际上 quicklist 并不是一个新的数据结构,它就是linkedlistziplist 的结合,然后被命名为快速列表。

quicklist 内部存储结构

quicklist 中每一个节点都是一个 quicklistNode 对象,其数据结构定义如下:

typedef struct quicklistNode {
  struct quicklistNode *prev;//前一个节点
  struct quicklistNode *next;//后一个节点
  unsigned char *zl;//当前指向的ziplist或者quicklistLZF
  unsigned int sz;//当前ziplist占用字节
  unsigned int count : 16;//ziplist中存储的元素个数,16字节(最大65535个)
  unsigned int encoding : 2; //是否采用了LZF压缩算法压缩节点 1:RAW 2:LZF
  unsigned int container : 2; //存储结构,NONE=1, ZIPLIST=2
  unsigned int recompress : 1; //当前ziplist是否需要再次压缩(如果前面被解压过则为true,表示需要再次被压缩)
  unsigned int attempted_compress : 1;//测试用
  unsigned int extra : 10; //后期留用
} quicklistNode;

然后各个 quicklistNode 就构成了一个快速列表 quicklist

typedef struct quicklist {
  quicklistNode *head;//列表头节点
  quicklistNode *tail;//列表尾节点
  unsigned long count;//ziplist中一共存储了多少元素,即:每一个quicklistNode内的count相加
  unsigned long len; //双向链表的长度,即quicklistNode的数量
  int fill : 16;//填充因子
  unsigned int compress : 16;//压缩深度 0-不压缩
} quicklist;

根据这两个结构,我们可以得到 Redis 3.2 版本之后的列表对象的一个存储结构示意图:

quicklist 的 compress 属性

compress 是用来表示压缩深度,ziplist 除了内存空间是连续之外,还可以采用特定的 LZF 压缩算法来将节点进行压缩存储,从而更进一步的节省空间,压缩深度可以通过参数 list-compress-depth 控制:

0:不压缩(默认值)
1:首尾第1个元素不压缩
2:首位前2个元素不压缩
3:首尾前3个元素不压缩以此类推

注意:之所以采取这种压缩两端节点的方式是因为很多场景都是两端的元素访问率最高的,而中间元素访问率相对较低,所以在实际使用时,我们可以根据自己的实际情况选择是否进行压缩,以及具体的压缩深度。

quicklistNode 的 zl 指针

zl 指针默认指向了 ziplist,上面提到 quicklistNode 中有一个 sz 属性记录了当前 ziplist 占用的字节,不过这仅仅限于当前节点没有被压缩(通过LZF 压缩算法)的情况,如果当前节点被压缩了,那么被压缩节点的 zl 指针会指向另一个对象 quicklistLZF,而不会直接指向 ziplistquicklistLZF 是一个 4+N 字节的结构:

typedef struct quicklistLZF {
  unsigned int sz;// LZF大小,占用4字节
  char compressed[];//被压缩的内容,占用N字节
} quicklistLZF;

quicklist 对比原始两种编码的改进

quicklist 同样采用了 linkedlist 的双端列表特性,然后 quicklist 中的每个节点又是一个 ziplist,所以quicklist 就是综合平衡考虑了 linkedlist 容易产生空间碎片的问题和 ziplist 的读写性能两个维度而设计出来的一种数据结构。使用 quicklist 需要注意以下 2 点:

如果 ziplist 中的 entry 个数过少,最极端情况就是只有 1entry 的压缩列表,那么此时 quicklist 就相当于退化成了一个普通的 linkedlist。如果 ziplist 中的 entry 过多,那么也会导致一次性需要申请的内存空间过大(ziplist 空间是连续的),而且因为 ziplist 本身的就是以时间换空间,所以会过多 entry 也会影响到列表对象的读写性能。

ziplist 中的 entry 个数可以通过参数 list-max-ziplist-size 来控制:

list-max-ziplist-size 1

注意:这个参数可以配置正数也可以配置负数。正数表示限制每个节点中的 entry 数量,如果是负数则只能为 -1~-5,其代表的含义如下:

-1:每个 ziplist 最多只能为 4KB

-2:每个 ziplist 最多只能为 8KB

-3:每个 ziplist 最多只能为 16KB

-4:每个 ziplist 最多只能为 32KB

-5:每个 ziplist 最多只能为 64KB

列表对象常用操作命令

lpush key value1 value2:将一个或者多个 value 插入到列表 key 的头部,key 不存在则创建 keyvalue2value1 之后)。

  • lpushx key value1 value2:将一个或者多个 value 插入到列表 key 的头部,key 不存在则不做任何处理(value2value1 之后)。
  • lpop key:移除并返回 key 值的列表头元素。
  • rpush key value1 value2:将一个或者多个 value 插入到列表 key 的尾部,key 不存在则创建 keyvalue2value1 之后)。
  • rpushx key value1 vaue2:将一个或者多个 value 插入到列表 key 的尾部,key 不存在则不做任何处理(value2value1 之后)。
  • rpop key:移除并返回列表 key 的尾元素。
  • llen key:返回列表 key 的长度。
  • lindex key index:返回列表 key 中下标为 index 的元素。index 为正数(从 0 开始)表示从队头开始算,index 为负数(从-1开始)则表示从队尾开始算。
  • lrange key start stop:返回列表 key 中下标 [start,end] 之间的元素。
  • lset key index value:将 value 设置到列表 key 中指定 index 位置,key 不存在或者 index 超出范围则会报错。 ltrim key start end:截取列表中 [start,end] 之间的元素,并替换原列表保存。

了解了操作列表对象的常用命令,我们就可以来验证下前面提到的列表对象的类型和编码了,在测试之前为了防止其他 key 值的干扰,我们先执行 flushall 命令清空 Redis 数据库。

接下来依次输入命令:

lpush name zhangsan type name object encoding name

可以看到,通过 type 命令输出的是 list,说明当前 name 存的是一个列表对象,并且编码是 quicklist(示例中用的是 5.0.5 版本)。

总结

本文主要介绍了 Redis5 种常用数据类型中的 列表对象,并介绍了底层的存储结构 quicklist,并分别对旧版本的两种底层数据 linkedlistziplist 进行了分析对比得出了为什么 Redis 最终要采用 quicklist 来存储列表对象。

到此这篇关于Redis都做了哪些加快速度的设计的文章就介绍到这了,更多相关Redis 加快速度的设计内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redis为什么快如何实现高可用及持久化

    前言 作为Java程序员,在面试过程中,缓存相关的问题是躲不掉的,肯定会问,例如缓存一致性问题,缓存雪崩.击穿.穿透等.说到缓存,那肯定少不了Redis,我在面试的时候也是被问了很多关于Redis相关的知识,但是Redis的功能太强大了,并不是一时半会儿能掌握好的,因为有些高级特性或是知识平时并不会用到. 所以回答的不好,人家就会觉得你对自己平时使用的工具都没有了解,自然就凉凉了.其实很早就有这个打算,打算好好总结一下Redis的知识,但也是由于自己都没有好好的了解Redis呢,所以一直没有开始

  • 硬核!15张图解Redis为什么这么快(推荐)

    作为一名服务端工程师,工作中你肯定和 Redis 打过交道.Redis为什么快,这点想必你也知道,至少为了面试也做过准备.很多人知道Redis快仅仅因为它是基于内存实现的,对于其它原因倒是模棱两可. 那么今天就和小莱一起看看: 图注:- 思维导图 - 基于内存实现 这点在一开始就提到过了,这里再简单说说. Redis 是基于内存的数据库,那不可避免的就要与磁盘数据库做对比.对于磁盘数据库来说,是需要将数据读取到内存里的,这个过程会受到磁盘 I/O 的限制. 而对于内存数据库来说,本身数据就存在于

  • 为啥Redis使用pipelining会更快

    为啥Redis使用pipelining会更快? 这是一个很考究细节的问题,大部分人都会说:因为减少了网络开销,那么,看如下例子: import time import redis client = redis.Redis(decode_responses=True) count = 10000 def no_pipelining(): for i in range(count): client.set("test:nopp:{}".format(i), i, ex=100) def w

  • redis单线程快的原因和原理

    Redis之所以执行速度很快,主要依赖于以下几个原因: (一)纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快: (二)单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗: (三)采用了非阻塞I/O多路复用机制 多路复用原理: 用户首先将需要进行IO操作的socket添

  • Redis凭啥可以这么快

    在日常开发中,为了保证数据的一致性,我们一般都选择关系型数据库来存储数据,如 MySQL,Oracle 等,因为关系型数据库有着事务的特性.然而在并发量比较大的业务场景,关系型数据库却又往往会成为系统瓶颈,无法完全满足我们的需求,所以就需要使用到缓存,而非关系型数据库,即 NoSQL 数据库往往又会成为最佳选择. NoSQL 数据库最常见的解释是 non-relational,也有人解释为 Not Only SQL.非关系型数据库不保证事务,也就是不具备事务 ACID 特性,这也是非关系型数据库

  • Redis都做了哪些加快速度的设计

    列表对象是 Redis 中 5 种基础数据类型之一,在 Redis 3.2 版本之前,列表对象底层存储结构有两种:linkedlist(双端列表)和 ziplist(压缩列表),而在 Redis 3.2 版本之后,列表对象底层存储结构只有一种:quicklist(快速列表),难道通过精心设计的 ziplist 最终被 Redis 抛弃了吗? 列表对象 同字符串对象一样,列表对象到底使用哪一种数据结构来进行存储也是通过编码来进行区分: 编码属性 描述 object encoding命令返回值 OB

  • Docker 部署 SpringBoot 项目整合 Redis 镜像做访问计数示例代码

    最终效果如下 大概就几个步骤 1.安装 Docker CE 2.运行 Redis 镜像 3.Java 环境准备 4.项目准备 5.编写 Dockerfile 6.发布项目 7.测试服务 环境准备 系统:Ubuntu 17.04 x64 Docker 17.12.0-ce IP:45.32.31.101 一.安装 Docker CE 国内不建议使用:"脚本进行安装",会下载安装很慢,使用步骤 1 安装,看下面的链接:常规安装方式 1.常规安装方式 Ubuntu 17.04 x64 安装

  • 使用AOP+redis+lua做方法限流的实现

    目录 需求 实现方式 源码 Limit 注解 LimitKey LimitType RedisLimiterHelper LimitInterceptor TestService 需求 公司里使用OneByOne的方式删除数据,为了防止一段时间内删除数据过多,让我这边做一个接口限流,超过一定阈值后报异常,终止删除操作. 实现方式 创建自定义注解 @limit 让使用者在需要的地方配置 count(一定时间内最多访问次数). period(给定的时间范围),也就是访问频率.然后通过LimitInt

  • 从请求到响应过程中django都做了哪些处理

    前言 最近面试的时候,被面试官问道一个问题,就是 request.user 里面的 user 是怎样得到的,这个问题当时没有回答上来,可以说是非常的尴尬,所以赶快查了一些资料,看了一些源码,特地来总结一下这个问题. 要想回答为什么可以直接通过 request.user 得到请求的用户,应该先来看看请求被处理以及如何返回响应的流程.今天先总结一下 django 从请求到响应都进行了哪些过程. WSGI 当客户端发送一次请求后,最先处理请求的实际上是 web 服务器就是我们经常说的 nginx.Ap

  • Linux下用Valgrind做检查(防止内存泄露)

    用C/C++开发其中最令人头疼的一个问题就是内存管理,有时候为了查找一个内存泄漏或者一个内存访问越界,需要要花上好几天时间,如果有一款工具能够帮助我们做这件事情就好了,valgrind正好就是这样的一款工具. Valgrind是一款基于模拟linux下的程序调试器和剖析器的软件套件,可以运行于x86, amd64和ppc32架构上.valgrind包含一个核心,它提供一个虚拟的CPU运行程序,还有一系列的工具,它们完成调试,剖析和一些类似的任务.valgrind是高度模块化的,所以开发人员或者用

  • 在项目中使用redis做缓存的一些思路

    目录 在项目中redis做缓存的一些思路 首先,缓存的对象有三种 本人走过的一些弯路 为什么没用Redis做缓存 使用Table作本地缓存 使用Redis作缓存 让我们来思考一下下面几个问题 那么使用本地缓存的问题是什么呢? 什么时候使用Redis? 在项目中redis做缓存的一些思路 首先,缓存的对象有三种 1.数据库中单条的的数据(以表名跟id作为key永久保存到redis),在有更新的地方都要更新缓存(不适用于需要经常更新的数据): 2.对于一些不分页,不需要实时(需要多表查询)的列表,我

  • redis 数据删除策略和逐出算法的问题小结

    数据存储和有效期 在 redis 工作流程中,过期的数据并不需要马上就要执行删除操作.因为这些删不删除只是一种状态表示,可以异步的去处理,在不忙的时候去把这些不紧急的删除操作做了,从而保证 redis 的高效 数据的存储 在redis中数据的存储不仅仅需要保存数据本身还要保存数据的生命周期,也就是过期时间.在redis 中 数据的存储结构如下图: 获取有效期 Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态 删除策略 在内存占用与CPU占用之间寻找一

  • redis与memcached的区别_动力节点Java学院整理

    传统MySQL+ Memcached架构遇到的问题 实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题: 1.MySQL需要不断进行拆库拆表,Memcached也需不断跟着扩容,扩容和维护工作占据大量开发时间. 2.Memcached与MySQL数据库数据一致性问题. 3.Memcached数据命中率低或down机,大量访问直接穿透到DB,MySQL

  • Redis数据库中实现分布式锁的方法

    分布式锁是一个在很多环境中非常有用的原语,它是不同进程互斥操作共享资源的唯一方法.有很多的开发库和博客描述如何使用Redis实现DLM(Distributed Lock Manager),但是每个开发库使用不同的方式,而且相比更复杂的设计与实现,很多库使用一些简单低可靠的方式来实现. 这篇文章尝试提供更标准的算法来使用Redis实现分布式锁.我们提出一种算法,叫做Relock,它实现了我们认为比vanilla单一实例方式更安全的DLM(分布式锁管理).我们希望社区分析它并提供反馈,以做为更加复杂

  • Redis正确使用的十个技巧

    Redis 在当前的技术社区里是非常热门的.从来自 Antirez 一个小小的个人项目到成为内存数据存储行业的标准,Redis已经走过了很长的一段路. 1.停止使用 KEYS * Okay,以挑战这个命令开始这篇文章,或许并不是一个好的方式,但其确实可能是最重要的一点.很多时候当我们关注一个redis实例的统计数据, 我们会快速地输入"KEYS *"命令,这样key的信息会很明显地展示出来.平心而论,从程序化的角度出发往往倾向于写出下面这样的伪代码: for key in 'keys

随机推荐