long 转为string_面试必问 Redis数据结构底层原理String、List篇

点击关注上方“Java大厂面试官”,第一时间送达技术干货。

阅读文本大概需要 8 分钟。

前言

今天来整理学习下Redis有哪些常用数据结构都是怎么使用的呢?首先看下全局存储结构。

全局存储结构

基础你们肯定都知道,redis支持的基础数据结构如下: String(字符串)、List(链表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合),那我来给你整个的画一画redis全局存储结构模型。( redis版本不同,代码也不尽相同,但是看原理够用了),从redis源码开始分析:

  • 首先是redis启动会初始化redisServer,默认创建16个数据库redisDb,默认我们用的都是第一个编号0的数据库。

    struct redisServer {
     // …
     // redis数据库数组
     redisDb *db;
        // 数据库的数量 默认16
        int dbnum;
     //...
    }
  • 每个redisDb数据库用dict(字典)保存着数据库中的所有键值对

     struct redisDb {
        // 数据库键空间,保存着数据库中的所有键值对
     dict *dict; 
     //...
    }
  • dict(字典)使用一对hashtable哈希表实现,跟Java中的HashMap很像

    typedef struct dict {
        // 包含2个hashtable
        dictht ht[2];
        // ...


    typedef struct dictht {
        // 哈希表数组
        dictEntry **table;
        // ...


    typedef struct dictEntry {
        // 键
        void *key;
        // 值
        void *val;
        // 指向下个哈希表节点,形成链表
        dictEntry *next;

    }
  • 字典中实际存储的Redis对象

    typedef struct redisObject {   
        // 类型    
        unsigned type:4;// string,list,set,zset,hash等
        // 编码    
        unsigned encoding:4;  // int,raw,embstr,ziplist,intset,quicklist,skiplist等  
        // 对象最后一次被访问的时间    
        unsigned lru:REDIS_LRU_BITS;  
        // 引用计数    
        int refcount;   
        // 指向实际值的指针   
        void *ptr;

    从上面分析可得Redis全局存储结构如下:

d21ade142784227b0365d85263e570b7.png
在这里插入图片描述

(这个图直接把我画裂开了,如有错误欢迎指正)

下面我们用"3w"方法来一一介绍下,每个数据类型,底层所用到了哪些数据结构(编码 )。

String 字符串

是什么

内部其实就是一个带长度信息的字节数组,原理类似Java中的ArrayList,可以动态扩容,所以很多特性都类似了,原理是相通的。内容是以二进制的形式存储的,所以 SDS(Simple Dynamic  String) 可以存储任何类型的二进制数据,同时也不需要担心数据格式转换的问题。

struct SDS {
    // ...
  T capacity; // 数组容量
    T len; // 数组长度
    byte[] content; // 数组内容
}   
a39f5d2ddb886bcc5470e9095b334c27.png
在这里插入图片描述

为什么

1.为什么申请空间比实际占用空间大,冗余了很多空位?

字符串支持append修改操作,如果没有冗余空间,那么追加操作必会引起频繁的数组扩容,而扩容是个耗时操作,所以通过空间预分配的方式来解决,即用冗余空间换时间。

2.实际使用长度len字段存在的意义是什么?

我们来用反证法证明,如果没有len来记录字符串长度,那么每次获取字符串长度时,就要调用默认的strlen函数来获取,而这个函数的时间复杂度是O(n),如果有了len,每次获取长度可以直接访问它,时间复杂度立马降至为O(1)。查询效率迎来质的飞跃,这块跟Arraylist的size原理一样。

如何实现

我们来直接用redis自带的debug命令看下实际存储对象的底层编码encoding,来看下底层使用了什么数据结构。

本文实例用的是redis版本:6.0.6

int编码

> set key1 2000222222
OK
> debug object key1
Value at:0x7f21f2eadd20 refcount:1 encoding:int serializedlength:5 lru:13142802 lru_seconds_idle:25

embstr编码

> set key2 01234567890123456789012345678901234567890123  // 44个字符
OK
> debug object key2
Value at:0x7f21f2e15140 refcount:1 encoding:embstr serializedlength:21 lru:13145749 lru_seconds_idle:5

raw编码

> set key2 012345678901234567890123456789012345678901234 // 45个字符
OK
> debug object key2
Value at:0x7f21f2eadd40 refcount:1 encoding:raw serializedlength:21 lru:13145765 lru_seconds_idle:2

总结:

为了节省内存空间,会按照实际存储字符串长度类型来选用不同编码

  • 存储的字符串可以转为long型,则用long类型存储,编码为int

  • 存储的字符串长度不大于44个字节时,用embstr编码

  • 存储的字符串长度大于44个字节时,用raw编码

编码类型分这么细的原因?为了优先使用更紧凑的数据结构来解决问题,终极目标就是为了压缩内存、压缩内存、压缩内存。

raw和embstr的区别?embstr编码: RedisObject的元数据,指针和SDS是连续的,可以避免内存碎片

raw编码: Redis会给SDS分配独立的空间,并用指针指向SDS结构

扩容策略

  • 字符串长度小于1MB时,采用加倍策略,ArrayList1.5
  • 字符串长度大于1MB时,采用每次扩容只加固定1MB

这个扩容策略,就比ArrayList高明了,当字符串比较大时,比如200M,每次还是double的话,400M,那就太浪费空间了,为了避免这种过大的空间浪费,使用了这种阈值判断方式,针对原始数据的不同大小采用相应的有效策略。

Reids规定了字符串最大长度不能超过512MB

使用场景

常用于缓存用户信息、原子加减。

注意: 原子计数是有范围的(long的范围),超过了会报错异常

List 链表

是什么

  • 版本3.2之前在Redis中使用的是压缩列表ziplist+双向链表linkedlist.
  • 版本3.2之后快速链表quickList

3.2之前初始化的 List 使用的压缩列表ziplist,随着数据增多,转化为双向链表linkedlist。压缩列表转化成双向链表的条件:

  • 如果添加的字符串元素长度超过默认值64
  • zip包含的节点数超过默认值512

这两个条件是可以修改的,在redis.conf中

list-max-ziplist-value 64 
list-max-ziplist-entries 512  
linkedlist

原理类似Java中的LinkedList,增删时间复杂度O(1),查询O(n).

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
  // ...
}
typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}
81a41167a430c1ba40d683bc24270bb5.png
在这里插入图片描述
ziplist

ziplist是什么

ziplist压缩列表是内存地址连续,元素之间紧凑存储,功能类似链表的一种数据结构。

struct ziplist {
  int32 zlbytes;   // 整个列表占用字节数
  int32 zltail_offset; // 达到尾部的偏移量
  int16 zllength; // 存储元素实体个数
  T[] entries; // 存储内容实体
  int8 zlend; // 尾部标识
}
struct entry {
  int prevlen;   // 前一个entry的字节长度int encoding;; // 元素类型编码
  optional byte[] content; // 元素内容
}
cfb0d18f4480531bf50122896b69d763.png
在这里插入图片描述

为什么用ziplist?

因为普通的链表要附加prev、next前后指针浪费空间(64位操作系统每个指针占用8个字节),另外每个节点的内存是单独分配,会加剧内存的碎片化,影响内存管理效率。

如何实现

简单的来说就是用非指针连接的方式实现了双向链表的能力,能从头部和尾部(zltail)双向遍历没有维护双向指针prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度 更费内存。这是典型的“时间换空间”。只有字段、值比较小,才会用ziplist

优点:

  • 内存地址连续,省去了每个元素的头尾节点指针占用的内存,节省空间

缺点:

  • 插入数据、删除数据会导致连锁更新问题,有点儿类似Arraylist为保证内存连续性的数据移动的原理
quicklist

quicklist是什么

quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。

为什么

为什么用quicklist

结合了 zipListlinkedList 的优点设计出来的,ziplist会引入频繁的内存申请和释放,而linkedlist由于指针也会造成内存的浪费,而且每个节点是单独存在的,会造成很多内存碎片,所以结合两个结构的特点,设计了quickList。

如何实现

debug看下encoding: quicklist

> rpush key3 a b c
3
> debug object key3
Value at:0x7f21f2eaddb0 refcount:1 encoding:quicklist serializedlength:22 lru:13150287 lru_seconds_idle:17 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:20
struct quicklist {
  quicklistNode *head;   
  quicklistNode *tail; 
  long count; // 元素总数
  // ... 
}
struct quicklistNode  {
  quicklistNode *prev;  
  quicklistNode *next; 
  ziplist *zl;// 压缩列表

quickList 的每个节点使用 ziplist 来保存数据,有headtail,每一个节点是一个quicklistNode,包含prev和next指针。每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值。所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。

结构如下图:

d45b98655648d0b561f244516d384f0c.png
在这里插入图片描述

每个quicklist节点上的ziplist大小可以配置

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。

-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。

-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。

-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)

-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

list-max0ziplist-size -2

中间节点压缩策略可配置

0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
以此类推
list-compress-depth 0

总结

  • 整个redis全局存储模型,是用字典完成的,类似Java中的HashMap原理
  • String类型,底层是动态字符串,会根据字符串类型和大小决定使用int编码raw编码或者embstr编码
  • List类型,3.2版本之前会根据数据大小判断用ziplist还是linkedlist,3.2版本之后优化为quicklist方式编码。

参考:

  • 《Redis深度历险 核心原理与应用实践》
  • https://juejin.cn/post/6863256540439117831
db0c39a7b30f19d4fe48ff5ff198049d.png

往期推荐

看故事学知识-三年工龄了还讲不清redis持久化!

生产环境下,如何排查CPU异常,定位鬼畜代码

为什么JDK源码中,无限循环大多使用for(;;)而不是while(true)?

一起进大厂

成为架构师

长按加关注

79f094cd274e958970a94bdea1f7ebb9.png9dc99427f3602c87b1c1f14341f30402.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/461279.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wpf 3D学习

最近在看一些关于wpf 3d的效果,研究了一些代码特效,现在和广大博友共享一下. 首先用到的是MeshGeometry3D,msdn上介绍:用于生成三维形状的三角形基元。主要有4个依赖属性:NormalsProperty,PositionsPropert…

字符设备驱动高级篇4——自动创建设备文件的函数代码分析

以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。 一、概述 设备文件的创建,主要涉及class_create()函数、device_create()函数。 class_create()函数用于自动创建 /sys/class/目录下的xxx目录。 device_create()函数用于自动创建 /dev/…

unicode字符、python乱码问题

http://www.cnblogs.com/BeginMan/archive/2013/08/08/3246619.html#a1 Python常见常用知识点http://blog.csdn.net/tingsking18/article/details/4033645 Unicode和Python的中文处理如何让Python的Unicode字符串支持中文?要想利用Python的Unicode机制处理字符串&…

win10下如何安装vb6.0sp6_Mac如何安装win10系统?Parallels Desktop 15 Mac安装win10系统教程...

Parallels Desktop 15 mac版是mac上非常强大也非常好用的虚拟机软件,最新版本的parallels desktop mac 15针对最新的Windows 10更新和macOS Catalina(10.15)进行了优化。今天分享的内容就是Parallels Desktop 15 mac版如何安装win10系统。PD虚…

android面试题精选

1.android dvm 的进程和Linux的进程,应用程序的进程是否为同一个概念: 答:dvm是dalivk虚拟机。每一个android应用程序都在自己的进程中运行,都拥有一个dalivk虚拟机实例。而每一个dvm都是在linux的一个进程。所以说可以认为是同一…

字符设备驱动高级篇5——静态映射表的建立过程,动态映射结构体方式操作寄存器

以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。 补充内容:字符设备驱动基础5——驱动如何操控硬件_天糊土的博客-CSDN博客 一、静态映射表的建立过程 关于“静态映射表的建立”这部分内容,有以下三个关键: &…

python 分布图_python数据分布型图表柱形分布图系列带误差线的柱形图

柱形分布图系列柱形分布图系列使用柱形图的方式展示数据的分布规律;可以借助误差线或散点图;带误差线的柱形图就是使用每个类别的均值作为柱形的高度;再根据每个类别的标准差绘制误差线;缺点:无法显示数据的分布情况&a…

[汇编] 002基础知识-CPU和寄存器

CPU是什么 当然这里的内存不仅仅指电脑上的内存,例如:我的金士顿8G内存,七彩虹1G独显,在这里来说,显卡也是有内存的(寄存器) CPU如何控制其它部件的? 问题:CPU是如何和电脑主机中其它芯片有条不…

Asp.net中页面传值几种方式

页面传值是学习asp.net初期都会面临的一个问题,总的来说有页面传值、存储对象传值、ajax、类、model、表单等。但是一般来说,常用的较简单有QueryString,Session,Cookies,Application,Server.Transfer。  …

字符设备驱动高级篇6——内核提供的读写寄存器接口

以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。 1、访问寄存器的方式 之前对寄存器的操作,都是先定义指向寄存器的指针,然后再解引用来对寄存器进行操作。这是因为ARM体系中,内存和IO是统一编址的。但是其他体系…

java台球游戏设计原理_Java实现简单台球游戏

Java实现简单台球桌问题,供大家参考,具体内容如下需求:使小球可以在桌面上移动,移动到桌面边缘将被弹回,显示小区的移动素材:小球照片桌球照片程序源代码:package 桌球游戏;import java.awt.*;i…

wordpress教程:默认http头信息X-Pingback的隐藏与修改

利用站长工具的http状态查询工具查询可以看到类似如下的一段http HEAD信息 X-Pingback: http://www.kristain.com/wordpress真实路径/xmlrpc.php 其实这样就已经暴露了wordpress网站的真实路径了,那么如何来隐藏wordpress默认http HEAD信息中的X-Pingback信息呢&…

关于java assertion

大部分转载自参考资料:http://www.ibm.com/developerworks/cn/java/l-javaassertion/index.html assertion(断言)在软件开发中是一种常用的调试方式,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序…

mupdf不支持x64_Delphi xe2使用x64编译器编译ASM代码时出错 . 不支持的语言功能:'ASM'...

代码无法直接正确移植到x64,因为它将执行64位指针截断 - 有关详细信息,请参见下文 .64位应用程序不支持将汇编语句与Pascal代码混合使用 . 使用Pascal代码或完全用汇编编写的函数替换汇编语句 .这里使用装配是不必要的 . 我不确定为什么原作者会选择去解…

IOC是什么?

2019独角兽企业重金招聘Python工程师标准>>> Inversion of Control,即反转控制,或许说为依赖注入更为合适。IoC就是一种设计模式。 Interface Driven Design接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现&a…

poj2516Minimum Cost

http://poj.org/problem?id2516 建图的时候 有个地方写错了 卡了半年。。 题意看了N久啊 有N个店主需要K种物品 有M个供应点 每个供应点有K种物品 其实是算K次最小费用 然后叠加 分解开来这题就是求把某种物品从供应点送到店主那里 多个源点-》多个汇点 所以加一个超级源点 和…

设备驱动框架1——LED驱动框架的分析(核心层)

以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。 一、驱动框架的含义 1、理解层面1:驱动的分层设计 设备驱动程序,是由内核驱动部分的维护者,以及驱动开发工程师协作完成的。 内核驱动部分的维护者,往…

myeclipse连接mysql怎么调用_myeclipse连接mysql数据库详细步骤

第一步 打开Database windows-prefenrence-showview-DBbrowser ,此时会在工具底部有个DBbrowser ,选中它,再它所控制的页面的任意位置 右击new---跳转到一个配置driver的页面 (选择连接方式)图一打开myeclipse然后点击window窗口 点击Open Perspective…

struts2中文件上传

注意点 private File image;//对应的就是表单中文件上传的那个输入域的名称,Struts2框架会封装成File类型的private String imageFileName;// 上传输入域FileName 文件名private String imageContentType;// 上传文件的MIME类型 单个文件 1 package cn.itcast.ac…

虚拟内存管理

MMU 现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要处理器中的MMU(Memory Management Unit,内存管理单元)提供支持,本节简要介绍MMU的作用。 首先引入两个概念&…