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…

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虚…

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

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

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

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

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

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

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

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

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

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

关于java assertion

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

IOC是什么?

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

poj2516Minimum Cost

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

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

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

虚拟内存管理

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

mysql重新用户设置密码_mysql用户密码如何重新设置?

mysql用户密码重新设置停掉MySQL服务:sudo service mysql stop以上命令适用于Ubuntu和Debian。CentOS、Fedora和RHEL下使用mysqld替换mysql。以安全模式启动mysql:sudo mysqld_safe --skip-grant-tables --skip-networking &这样我们就可以直接用roo…

第三章 门电路

1 半导体二极管开关特性 1 二极管的特性可以近似的用3.2.1的PN结方程和图3.2.2伏安特性曲线描述 如下图 二极管近似伏安特性和对应的等效电路 1 a电路表示vcc和r都很小时候二极管正向导通压降和正向电阻都不能忽视 2 b电路表示二极管正向导通电压不可以忽视,但是二…

mysql查询数据库日期_mysql如何查询日期与时间

前言:在项目开发中,一些业务表字段经常使用日期和时间类型,而且后续还会牵涉到这类字段的查询。关于日期及时间的查询等各类需求也很多,本篇文章简单讲讲日期及时间字段的规范化查询方法。1.日期和时间类型概览MySQL支持的日期和时…

设备驱动框架3——使用gpiolib完成LED驱动

以下内容源于朱有鹏嵌入式课程的学习整理,如有侵权请告知删除。 一、前言 在实际情况中,很多硬件都要用到GPIO,因此GPIO会复用;如果同一个GPIO被2个驱动同时控制就会出现bug;因此内核提供了gpiolib来统一管理系统中所有…

from PyQt4 import QtGui,QtCore出错-解

from PyQt4 import QtGui,QtCore出错-解今天尝试着安装PyQt写界面,官网下载后发现import出错了,情况如下图:import PyQt4就可以,from PyQt4 import QtCore却不行提示DLL load faied找了下网上有些人说是某些dll文件丢失了&#xf…

设备驱动框架4——将驱动集成到内核中

以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。 驱动集成到内核的概念 驱动开发的步骤一般是: (1)以模块的形式在内核外部编写与调试 (2)将调试好的驱动代码集成到kernel中 之前我们编写的…

例子简单说说C# ref和out

首写从这字段看 ref 就是引用的意思 out当然就是输出了public void getRefStr(ref string str) {str"hello 你好,你变成了Ref了" }public void getOutStr(out string outStr){outStr "hello 你好,你是out输出的值";} protected…