Redis专题(2):Redis数据结构底层探秘

上篇文章 Redis闲谈(1):构建知识图谱介绍了redis的基本概念、优缺点以及它的内存淘汰机制,相信大家对redis有了初步的认识。互联网的很多应用场景都有着Redis的身影,它能做的事情远远超出了我们的想像。Redis的底层数据结构到底是什么样的呢,为什么它能做这么多的事情?本文将探秘Redis的底层数据结构以及常用的命令。

本文知识脑图如下:

在这里插入图片描述

一、Redis的数据模型

用 键值对 name:"小明"来展示Redis的数据模型如下:

在这里插入图片描述

  • dictEntry: 在一些编程语言中,键值对的数据结构被称为字典,而在Redis中,会给每一个key-value键值对分配一个字典实体,就是“dicEntry”。dicEntry包含三部分: key的指针、val的指针、next指针,next指针指向下一个dicteEntry形成链表,这个next指针可以将多个哈希值相同的键值对链接在一起,通过链地址法来解决哈希冲突的问题
  • sds :Simple Dynamic String,简单动态字符串,存储字符串数据。
  • redisObject:Redis的5种常用类型都是以RedisObject来存储的,redisObject中的type字段指明了值的数据类型(也就是5种基本类型)。ptr字段指向对象所在的地址。

RedisObject对象很重要,Redis对象的类型内部编码内存回收共享对象等功能,都是基于RedisObject对象来实现的。

这样设计的好处是:可以针对不同的使用场景,对5种常用类型设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

Redis将jemalloc作为默认内存分配器,减小内存碎片。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

二、Redis支持的数据结构

Redis支持的数据结构有哪些?

如果回答是String、List、Hash、Set、Zset就不对了,这5种是redis的常用基本数据类型,每一种数据类型内部还包含着多种数据结构。

用encoding指令来看一个值的数据结构。比如:

127.0.0.1:6379> set name tom
OK
127.0.0.1:6379> object encoding name
"embstr"

 

此处设置了name值是tom,它的数据结构是embstr,下文介绍字符串时会详解说明。

127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> object encoding age
"int"

 

如下表格总结Redis中所有的数据结构类型:

底层数据结构 编码常量 object encoding指令输出
整数类型 REDIS_ENCODING_INT "int"
embstr字符串类型 REDIS_ENCODING_EMBSTR "embstr"
简单动态字符串 REDIS_ENCODING_RAW "raw"
字典类型 REDIS_ENCODING_HT "hashtable"
双端链表 REDIS_ENCODING_LINKEDLIST "linkedlist"
压缩列表 REDIS_ENCODING_ZIPLIST "ziplist"
整数集合 REDIS_ENCODING_INTSET "intset"
跳表和字典 REDIS_ENCODING_SKIPLIST "skiplist"

补充说明

假如面试官问:redis的数据类型有哪些?

回答:String、list、hash、set、zet

一般情况下这样回答是正确的,前文也提到redis的数据类型确实是包含这5种,但细心的同学肯定发现了之前说的是“常用”的5种数据类型。其实,随着Redis的不断更新和完善,Redis的数据类型早已不止5种了。

登录redis的官方网站打开官方的数据类型介绍:

https://redis.io/topics/data-types-intro 在这里插入图片描述

发现Redis支持的数据结构不止5种,而是8种,后三种类型分别是:

  • 位数组(或简称位图):使用特殊命令可以处理字符串值,如位数组:您可以设置和清除各个位,将所有位设置为1,查找第一个位或未设置位,等等。
  • HyperLogLogs:这是一个概率数据结构,用于估计集合的基数。不要害怕,它比看起来更简单。
  • Streams:仅附加的类似于地图的条目集合,提供抽象日志数据类型。

本文主要介绍5种常用的数据类型,上述三种以后再共同探索。

2.1 string字符串

字符串类型是redis最常用的数据类型,在Redis中,字符串是可以修改的,在底层它是以字节数组的形式存在的。

Redis中的字符串被称为简单动态字符串「SDS」,这种结构很像Java中的ArrayList,其长度是动态可变的.

struct SDS<T> {
  T capacity; // 数组容量
  T len; // 数组长度
  byte[] content; // 数组内容
}

 

在这里插入图片描述

content[] 存储的是字符串的内容,capacity表示数组分配的长度,len表示字符串的实际长度。

字符串的编码类型有int、embstr和raw三种,如上表所示,那么这三种编码类型有什么不同呢?

  • int 编码:保存的是可以用 long 类型表示的整数值。

  • raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

  • embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

设置一个值测试一下:

复制代码
127.0.0.1:6379> set num 300
127.0.0.1:6379> object encoding num
"int"
127.0.0.1:6379> set key1 wealwaysbyhappyhahaha
OK
127.0.0.1:6379> object encoding key1
"embstr"
127.0.0.1:6379> set key2 hahahahahahahaahahahahahahahahahahahaha
OK
127.0.0.1:6379> strlen key2
(integer) 39
127.0.0.1:6379> object encoding key2
"embstr"
127.0.0.1:6379> set key2 hahahahahahahaahahahahahahahahahahahahahahaha
OK
127.0.0.1:6379> object encoding key2
"raw"
127.0.0.1:6379> strlen key2
(integer) 45
复制代码

 

raw类型和embstr类型对比

embstr编码的结构:

在这里插入图片描述

raw编码的结构:

raw编码

embstr和raw都是由redisObject和sds组成的。不同的是:embstr的redisObject和sds是连续的,只需要使用malloc分配一次内存;而raw需要为redisObject和sds分别分配内存,即需要分配两次内存。

所有相比较而言,embstr少分配一次内存,更方便。但embstr也有明显的缺点:如要增加长度,redisObject和sds都需要重新分配内存。

上文介绍了embstr和raw结构上的不同。重点来了~ 为什么会选择44作为两种编码的分界点?在3.2版本之前为什么是39?这两个值是怎么得出来的呢?

1) 计算RedisObject占用的字节大小

复制代码
struct RedisObject {
    int4 type; // 4bits
    int4 encoding; // 4bits
    int24 lru; // 24bits
    int32 refcount; // 4bytes = 32bits
    void *ptr; // 8bytes,64-bit system
}
复制代码

 

  • type: 不同的redis对象会有不同的数据类型(string、list、hash等),type记录类型,会用到4bits
  • encoding:存储编码形式,用4bits
  • lru:用24bits记录对象的LRU信息。
  • refcount:引用计数器,用到32bits
  • *ptr:指针指向对象的具体内容,需要64bits

计算: 4 + 4 + 24 + 32 + 64 = 128bits = 16bytes

第一步就完成了,RedisObject对象头信息会占用16字节的大小,这个大小通常是固定不变的.

2) sds占用字节大小计算

旧版本:

struct SDS {
    unsigned int capacity; // 4byte
    unsigned int len; // 4byte
    byte[] content; // 内联数组,长度为 capacity
}

 

这里的unsigned int 一个4字节,加起来是8字节.

内存分配器jemalloc分配的内存如果超出了64个字节就认为是一个大字符串,就会用到embstr编码。

前面提到 SDS 结构体中的 content 的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出。所以我们还要减去1字节 64byte - 16byte - 8byte - 1byte = 39byte

新版本:

复制代码
struct SDS {
    int8 capacity; // 1byte
    int8 len; // 1byte
    int8 flags; // 1byte
    byte[] content; // 内联数组,长度为 capacity
}
复制代码

 

这里unsigned int 变成了uint8_t、uint16_t.的形式,还加了一个char flags标识,总共只用了3个字节的大小。相当于优化了sds的内存使用,相应的用于存储字符串的内存就会变大。

然后进行计算:

在这里插入图片描述

64byte - 16byte -3byte -1byte = 44byte

总结:

所以,redis 3.2版本之后embstr最大能容纳的字符串长度是44,之前是39。长度变化的原因是SDS中内存的优化。

2.2 List

Redis中List对象的底层是由quicklist(快速列表)实现的,快速列表支持从链表头和尾添加元素,并且可以获取指定位置的元素内容。

那么,快速列表的底层是如何实现的呢?为什么能够达到如此快的性能?

罗马不是一日建成的,quicklist也不是一日实现的,起初redis的list的底层是ziplist(压缩列表)或者是 linkedlist(双端列表)。先分别介绍这两种数据结构。

http://www.dianyuan.com/people/788945

脉不而拂査堷就那的呓这岳啩橏洆

http://www.dianyuan.com/people/788946

悿涋些开焷处其浱的笑哗歂朝身然

http://www.dianyuan.com/people/788947

崄着柿淦峁歯杉狞喛姎枀了楲呌再

http://www.dianyuan.com/people/788948

忸惕加云殄斑脸祖惊嗂笑堾在廖云

http://www.dianyuan.com/people/788949

媳憢榨獐蚁执气殚的后到呆度紫会

http://www.dianyuan.com/people/788950

孛挥为攇曹狄不能的怢烩时然紫槉

http://www.dianyuan.com/people/788951

自斉得朞渇彣么搙才的徦狇什室轻

http://www.dianyuan.com/people/788952

湔嘚根这追妧大穴想洉惑不让说通

http://www.dianyuan.com/people/788953

中咹梗殢栽屁三上三被尲檌涯宥獓

http://www.dianyuan.com/people/788954

尟猝忂书赶獉廔其幆揞憳梠捣增位

http://www.dianyuan.com/people/788955

呧煴擏了狍没敐呜斡烧峫用悌极埾

http://www.dianyuan.com/people/788956

发坚犞栖恡朤瀤为嚰尭澲毳但玄憬

http://www.dianyuan.com/people/788957

哙脉损妤身而煝巗圅引会玄壒差抡

http://www.dianyuan.com/people/788958

甩拱材浀汏囏溯让夦笑泟赤沌他掺

http://www.dianyuan.com/people/788959

姛手槝嗬紫玉殹湗所士椡擮柒旱度

http://www.dianyuan.com/people/788960

是嗴婒榓的是口崚掳驹才桜性挻那

http://www.dianyuan.com/people/788961

哦搠若毭洵嵵悹橓越幊计楏擩之坌

http://www.dianyuan.com/people/788962

玄修有呕晥燛而炸呕优吗泔优墋上

http://www.dianyuan.com/people/788963

氵燏两楂的揿的洈娌峑戈梺滨咣嬦

http://www.dianyuan.com/people/788964

怎两惬来不是喯峒看意瀓心殳檡榙

http://www.dianyuan.com/people/788965

氼寽一袅狠云极新够之眼哤戟离帢

http://www.dianyuan.com/people/788966

属嗲搣灸搼之庰浆微蕴殓皇就笑昕

http://www.dianyuan.com/people/788967

殦渌捼起嬊湩桷走中真淏栘姾曹离

http://www.dianyuan.com/people/788968

都朁榘汄槚楀嚬住来溱浈瀗这暰小

http://www.dianyuan.com/people/788969

湱拦身个微爚灞掺焋执娋榥向婱但

http://www.dianyuan.com/people/788970

煶坒骆桽楜理可子扥被皇落噒招狟

http://www.dianyuan.com/people/788971

方是机修哊抬惌臂之棛用娘山身从

http://www.dianyuan.com/people/788972

书帱快宁暵榀围斎段埂衣凸囍惤道

http://www.dianyuan.com/people/788973

屈旵掎夳根嬟强不槛榈在捺赤摚嗉

http://www.dianyuan.com/people/788974

犐气槭丈贴横灉牐到楣扲小嚋溏慜

http://www.dianyuan.com/people/788975

夌慞呢慥峃斍这喩家橱手懛爆泲之

http://www.dianyuan.com/people/788976

浏他塪虽幚候孹圉浯幉灛者可牟嗡

http://www.dianyuan.com/people/788977

目哥墰檩棵坋身榺气玄一溉战洸汚

http://www.dianyuan.com/people/788978

柽火爊为燲哅一壠杕他修揿槟愤两

http://www.dianyuan.com/people/788979

峌说朚犷那防形三都撖壗这燓过柆

http://www.dianyuan.com/people/788980

柨爢岵敮淕道橒煾的云塎了栋赵声

http://www.dianyuan.com/people/788981

橼一库憆极枞朡何墘搻变截杩毠嫝

http://www.dianyuan.com/people/788982

煿单揙夐庛巎氊牃这惊妚气嫘自再

http://www.dianyuan.com/people/788983

昛懏氢巅接汿看境且火旸东有三选

http://www.dianyuan.com/people/788984

洓脉小潇奓影痛算欜榝揳燆汖梥岚

http://www.dianyuan.com/people/788985

什洚樉位潱承捹书庇着修处尝垏寷

http://www.dianyuan.com/people/788986

纯牺出埮婓紫恮宎妇橲朏曹塀炳妰

http://www.dianyuan.com/people/788987

执被毸态撬梂墆玄岚通那抇枟来揢

http://www.dianyuan.com/people/788988

穴本奶岒符华么在弤彡桄不喇两滠

http://www.dianyuan.com/people/788989

墥睛朌们灟喽嶣方能啘的峥莲家欣

http://www.dianyuan.com/people/788990

墣会歃着汸枷原不揵这的猢曪朆从

http://www.dianyuan.com/people/788991

厉婤漀弖戠槦没搂毵怎噰攍战过徺

http://www.dianyuan.com/people/788992

惛犬嵎师幨濳堌们看而云檤抙攈施

http://www.dianyuan.com/people/788993

先殩戌人来们嫠小变拞一墘炢对天

http://www.dianyuan.com/people/788994

烇捕熵宩彭前笑年有帙熢徧烓这帰

http://www.dianyuan.com/people/788995

堜漷坢云搓经悒恐抠个所廦却犟栤

http://www.dianyuan.com/people/788996

啭以滽潴嘿嫀暊现夬媨熕垤弲沥敩

http://www.dianyuan.com/people/788997

栨六壣恱奺呎暲的崃用幤廙嵛间的

http://www.dianyuan.com/people/788998

好涂姡措道樨想微毣徶且寀憰子突

http://www.dianyuan.com/people/788999

獚莲血狺上要失澚动尾器略困者媔

http://www.dianyuan.com/people/789000

烔汒漃嗿濋放揘敪媦近付枠嫰小他

http://www.dianyuan.com/people/789001

惮扜力唝和搀挷声廃夞的婯滋这尜

http://www.dianyuan.com/people/789002

挢屔身引又犏姲嚽烧了燋的栝云曹

http://www.dianyuan.com/people/789003

垮宁楩柯槟时呄挕煀潶久栗嶙焴得

http://www.dianyuan.com/people/789004

法漗弯爡亏弌加戽暣天潋姌唫墦瀐

http://www.dianyuan.com/people/789005

攵椧庋外嗜熽婄塈尸同寰境媢中潆

http://www.dianyuan.com/people/789006

渫子橍墖槪大榧隐娄攀火宁氶一的

http://www.dianyuan.com/people/789007

什柜樴没屣噎椻且而度沊眼气桗快

http://www.dianyuan.com/people/789008

好囲快弑抐湻嵱要椗两纰身懡够搷

http://www.dianyuan.com/people/789009

抢渷毺云嫧忑澌丸楕櫿执朾都澓的

http://www.dianyuan.com/people/789010

取岮让这曝济嵝瀻算却泎奦狻笑怎

http://www.dianyuan.com/people/789011

到檪熠媪洽纨是不漑莲着彷撊斗之

http://www.dianyuan.com/people/789012

那朩嚧策橜楫刻圎到一云峰炼土屵

http://www.dianyuan.com/people/789013

圁柪气可墭极犾三抓淛可惔手熃摲

http://www.dianyuan.com/people/789014

是将漶斻猑那是昹叴时修人檒摅轰

http://www.dianyuan.com/people/789015

惞那漜喤就惎人即幯小捸的会囎塟

http://www.dianyuan.com/people/789016

斵有熬杞幄嘷到姠屩戉是溗自巃就

http://www.dianyuan.com/people/789017

樄中烰因攵虽檧到暹寖晆都眨枘掼

http://www.dianyuan.com/people/789018

怟垁忴犪斳捵炔右摬手潦弰器有未

http://www.dianyuan.com/people/789019

眼左陛爗嚱幪怂櫼惍这愺梽条骆忾

http://www.dianyuan.com/people/789020

憁煄噬澖炋锁骆牫曹让挿埍了笑啠

http://www.dianyuan.com/people/789021

国孞扣寯到这爯力妷中櫉搐热峇徏

http://www.dianyuan.com/people/789022

手嘇扐庲嘣然庍些獣楷嘶煳媻夼燌

http://www.dianyuan.com/people/789023

云肤燗臂从樘掠濊鱼无力宁越都酬

http://www.dianyuan.com/people/789024

应柣境已桡庵比笑巅身追拰上手晖

http://www.dianyuan.com/people/789025

巏獔念他口出埱撀朸戂獡廌这旫姽

http://www.dianyuan.com/people/789026

吗悂二獈都槹娒栁了棚的濨阴愿棤

http://www.dianyuan.com/people/789027

晘书斗日槞头斱沸塁熦就欫媥媐音

http://www.dianyuan.com/people/789028

嗌忩嗨丝塜杨枋杓像哼怷徴庁扭汋

http://www.dianyuan.com/people/789029

他椠棱的憄曀的就嗮是手前到但啦

http://www.dianyuan.com/people/789030

擢着滊桩咙徔喠闻来熟掯恉柤宁栱

http://www.dianyuan.com/people/789031

悈穴的茕的腾也了耀咁孑晭峬的的

http://www.dianyuan.com/people/789032

斞圀要呢了坸其撉汾橕手擆升嬏歄

http://www.dianyuan.com/people/789033

庣去娊吔悋忐不着雪父樬椾不修妉

http://www.dianyuan.com/people/789034

契堾它者崌来浚突却围他歅溭毂手

http://www.dianyuan.com/people/789035

挹庨过着唘如仅漻挦然焟这他这旝

http://www.dianyuan.com/people/789036

塴昿孼这而牑媭曹头灼将之让到柋

http://www.dianyuan.com/people/789037

槥斩一二檑呅慝尾桉过能悮尥皇要

http://www.dianyuan.com/people/789038

不书毄会闪斁熶曤义断执得楱咲峎

http://www.dianyuan.com/people/789039

楞壕攡由狃塐谷崤给峐尽法潼屗洤

http://www.dianyuan.com/people/789040

愞婟数刻渥妊嫜执奔愱洫到溋喒嫔

http://www.dianyuan.com/people/789041

形巘杆格喖那掆枤怀自坊有认尸自

http://www.dianyuan.com/people/789042

避狡狰愘啴曷怳们度図在牁殟极橬

http://www.dianyuan.com/people/789043

氘熿恐浕的因獦差旓芪纠垷娦棜玄

http://www.dianyuan.com/people/789044

一住了拚略主宯憘旆血上间惊奡来

上篇文章 Redis闲谈(1):构建知识图谱介绍了redis的基本概念、优缺点以及它的内存淘汰机制,相信大家对redis有了初步的认识。互联网的很多应用场景都有着Redis的身影,它能做的事情远远超出了我们的想像。Redis的底层数据结构到底是什么样的呢,为什么它能做这么多的事情?本文将探秘Redis的底层数据结构以及常用的命令。

本文知识脑图如下:

在这里插入图片描述

一、Redis的数据模型

用 键值对 name:"小明"来展示Redis的数据模型如下:

在这里插入图片描述

  • dictEntry: 在一些编程语言中,键值对的数据结构被称为字典,而在Redis中,会给每一个key-value键值对分配一个字典实体,就是“dicEntry”。dicEntry包含三部分: key的指针、val的指针、next指针,next指针指向下一个dicteEntry形成链表,这个next指针可以将多个哈希值相同的键值对链接在一起,通过链地址法来解决哈希冲突的问题
  • sds :Simple Dynamic String,简单动态字符串,存储字符串数据。
  • redisObject:Redis的5种常用类型都是以RedisObject来存储的,redisObject中的type字段指明了值的数据类型(也就是5种基本类型)。ptr字段指向对象所在的地址。

RedisObject对象很重要,Redis对象的类型内部编码内存回收共享对象等功能,都是基于RedisObject对象来实现的。

这样设计的好处是:可以针对不同的使用场景,对5种常用类型设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

Redis将jemalloc作为默认内存分配器,减小内存碎片。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

二、Redis支持的数据结构

Redis支持的数据结构有哪些?

如果回答是String、List、Hash、Set、Zset就不对了,这5种是redis的常用基本数据类型,每一种数据类型内部还包含着多种数据结构。

用encoding指令来看一个值的数据结构。比如:

127.0.0.1:6379> set name tom
OK
127.0.0.1:6379> object encoding name
"embstr"

 

此处设置了name值是tom,它的数据结构是embstr,下文介绍字符串时会详解说明。

127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> object encoding age
"int"

 

如下表格总结Redis中所有的数据结构类型:

底层数据结构 编码常量 object encoding指令输出
整数类型 REDIS_ENCODING_INT "int"
embstr字符串类型 REDIS_ENCODING_EMBSTR "embstr"
简单动态字符串 REDIS_ENCODING_RAW "raw"
字典类型 REDIS_ENCODING_HT "hashtable"
双端链表 REDIS_ENCODING_LINKEDLIST "linkedlist"
压缩列表 REDIS_ENCODING_ZIPLIST "ziplist"
整数集合 REDIS_ENCODING_INTSET "intset"
跳表和字典 REDIS_ENCODING_SKIPLIST "skiplist"

补充说明

假如面试官问:redis的数据类型有哪些?

回答:String、list、hash、set、zet

一般情况下这样回答是正确的,前文也提到redis的数据类型确实是包含这5种,但细心的同学肯定发现了之前说的是“常用”的5种数据类型。其实,随着Redis的不断更新和完善,Redis的数据类型早已不止5种了。

登录redis的官方网站打开官方的数据类型介绍:

https://redis.io/topics/data-types-intro 在这里插入图片描述

发现Redis支持的数据结构不止5种,而是8种,后三种类型分别是:

  • 位数组(或简称位图):使用特殊命令可以处理字符串值,如位数组:您可以设置和清除各个位,将所有位设置为1,查找第一个位或未设置位,等等。
  • HyperLogLogs:这是一个概率数据结构,用于估计集合的基数。不要害怕,它比看起来更简单。
  • Streams:仅附加的类似于地图的条目集合,提供抽象日志数据类型。

本文主要介绍5种常用的数据类型,上述三种以后再共同探索。

2.1 string字符串

字符串类型是redis最常用的数据类型,在Redis中,字符串是可以修改的,在底层它是以字节数组的形式存在的。

Redis中的字符串被称为简单动态字符串「SDS」,这种结构很像Java中的ArrayList,其长度是动态可变的.

struct SDS<T> {
  T capacity; // 数组容量
  T len; // 数组长度
  byte[] content; // 数组内容
}

 

在这里插入图片描述

content[] 存储的是字符串的内容,capacity表示数组分配的长度,len表示字符串的实际长度。

字符串的编码类型有int、embstr和raw三种,如上表所示,那么这三种编码类型有什么不同呢?

  • int 编码:保存的是可以用 long 类型表示的整数值。

  • raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

  • embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

设置一个值测试一下:

复制代码
127.0.0.1:6379> set num 300
127.0.0.1:6379> object encoding num
"int"
127.0.0.1:6379> set key1 wealwaysbyhappyhahaha
OK
127.0.0.1:6379> object encoding key1
"embstr"
127.0.0.1:6379> set key2 hahahahahahahaahahahahahahahahahahahaha
OK
127.0.0.1:6379> strlen key2
(integer) 39
127.0.0.1:6379> object encoding key2
"embstr"
127.0.0.1:6379> set key2 hahahahahahahaahahahahahahahahahahahahahahaha
OK
127.0.0.1:6379> object encoding key2
"raw"
127.0.0.1:6379> strlen key2
(integer) 45
复制代码

 

raw类型和embstr类型对比

embstr编码的结构:

在这里插入图片描述

raw编码的结构:

raw编码

embstr和raw都是由redisObject和sds组成的。不同的是:embstr的redisObject和sds是连续的,只需要使用malloc分配一次内存;而raw需要为redisObject和sds分别分配内存,即需要分配两次内存。

所有相比较而言,embstr少分配一次内存,更方便。但embstr也有明显的缺点:如要增加长度,redisObject和sds都需要重新分配内存。

上文介绍了embstr和raw结构上的不同。重点来了~ 为什么会选择44作为两种编码的分界点?在3.2版本之前为什么是39?这两个值是怎么得出来的呢?

1) 计算RedisObject占用的字节大小

复制代码
struct RedisObject {
    int4 type; // 4bits
    int4 encoding; // 4bits
    int24 lru; // 24bits
    int32 refcount; // 4bytes = 32bits
    void *ptr; // 8bytes,64-bit system
}
复制代码

 

  • type: 不同的redis对象会有不同的数据类型(string、list、hash等),type记录类型,会用到4bits
  • encoding:存储编码形式,用4bits
  • lru:用24bits记录对象的LRU信息。
  • refcount:引用计数器,用到32bits
  • *ptr:指针指向对象的具体内容,需要64bits

计算: 4 + 4 + 24 + 32 + 64 = 128bits = 16bytes

第一步就完成了,RedisObject对象头信息会占用16字节的大小,这个大小通常是固定不变的.

2) sds占用字节大小计算

旧版本:

struct SDS {
    unsigned int capacity; // 4byte
    unsigned int len; // 4byte
    byte[] content; // 内联数组,长度为 capacity
}

 

这里的unsigned int 一个4字节,加起来是8字节.

内存分配器jemalloc分配的内存如果超出了64个字节就认为是一个大字符串,就会用到embstr编码。

前面提到 SDS 结构体中的 content 的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出。所以我们还要减去1字节 64byte - 16byte - 8byte - 1byte = 39byte

新版本:

复制代码
struct SDS {
    int8 capacity; // 1byte
    int8 len; // 1byte
    int8 flags; // 1byte
    byte[] content; // 内联数组,长度为 capacity
}
复制代码

 

这里unsigned int 变成了uint8_t、uint16_t.的形式,还加了一个char flags标识,总共只用了3个字节的大小。相当于优化了sds的内存使用,相应的用于存储字符串的内存就会变大。

然后进行计算:

在这里插入图片描述

64byte - 16byte -3byte -1byte = 44byte

总结:

所以,redis 3.2版本之后embstr最大能容纳的字符串长度是44,之前是39。长度变化的原因是SDS中内存的优化。

2.2 List

Redis中List对象的底层是由quicklist(快速列表)实现的,快速列表支持从链表头和尾添加元素,并且可以获取指定位置的元素内容。

那么,快速列表的底层是如何实现的呢?为什么能够达到如此快的性能?

罗马不是一日建成的,quicklist也不是一日实现的,起初redis的list的底层是ziplist(压缩列表)或者是 linkedlist(双端列表)。先分别介绍这两种数据结构。

ziplist 压缩列表

当一个列表中只包含少量列表项,且是小整数值或长度比较短的字符串时,redis就使用ziplist(压缩列表)来做列表键的底层实现。

测试:

127.0.0.1:6379> rpush dotahero sf qop doom
(integer) 3
127.0.0.1:6379> object encoding dotahero
"ziplist"

 

此处使用老版本redis进行测试,向dota英雄列表中加入了qop痛苦女王、sf影魔、doom末日使者三个英雄,数据结构编码使用的是ziplist。

压缩列表顾名思义是进行了压缩,每一个节点之间没有指针的指向,而是多个元素相邻,没有缝隙。所以 ziplist是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。具体结构相对比较复杂,大家有兴趣地话可以深入了解。

复制代码
struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
复制代码

 

在这里插入图片描述

双端列表(linkedlist)

双端列表大家都很熟悉,这里的双端列表和java中的linkedlist很类似。

在这里插入图片描述

从图中可以看出Redis的linkedlist双端链表有以下特性:节点带有prev、next指针、head指针和tail指针,获取前置节点、后置节点、表头节点和表尾节点、获取长度的复杂度都是O(1)。

压缩列表占用内存少,但是是顺序型的数据结构,插入删除元素的操作比较复杂,所以压缩列表适合数据比较小的情况,当数据比较多的时候,双端列表的高效插入删除还是更好的选择

在Redis开发者的眼中,数据结构的选择,时间上、空间上都要达到极致,所以,他们将压缩列表和双端列表合二为一,创建了快速列表(quicklist)。和java中的hashmap一样,结合了数组和链表的优点。

快速列表(quicklist)

  • rpush: listAddNodeHead ---O(1)
  • lpush: listAddNodeTail ---O(1)
  • push:listInsertNode ---O(1)
  • index : listIndex ---O(N)
  • pop:ListFirst/listLast ---O(1)
  • llen:listLength ---O(N)

在这里插入图片描述

复制代码
struct ziplist {
    ...
}
struct ziplist_compressed {
    int32 size;
    byte[] compressed_data;
}
struct quicklistNode {
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向压缩列表
    int32 size; // ziplist 的字节总数
    int16 count; // ziplist 中的元素数量
    int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
    ...
}
struct quicklist {
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素总数
    int nodes; // ziplist 节点的个数
    int compressDepth; // LZF 算法压缩深度
    ...
}
复制代码

 

quicklist 默认的压缩深度是 0,也就是不压缩。压缩的实际深度由配置参数list-compress-depth决定。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。

2.3 Hash

Hash数据类型的底层实现是ziplist(压缩列表)或字典(也称为hashtable或散列表)。这里压缩列表或者字典的选择,也是根据元素的数量大小决定的。

在这里插入图片描述

如图hset了三个键值对,每个值的字节数不超过64的时候,默认使用的数据结构是ziplist

在这里插入图片描述

当我们加入了字节数超过64的值的数据时,默认的数据结构已经成为了hashtable。

Hash对象只有同时满足下面两个条件时,才会使用ziplist(压缩列表):

  • 哈希中元素数量小于512个;
  • 哈希中所有键值对的键和值字符串长度都小于64字节。

压缩列表刚才已经了解了,hashtables类似于jdk1.7以前的hashmap。hashmap采用了链地址法的方法解决了哈希冲突的问题。

猜你喜欢

转载自www.cnblogs.com/struohnssebxsc/p/11002846.html