V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hansonwang99
V2EX  ›  程序员

Redis 字符串类型内部编码剖析

  •  
  •   hansonwang99 ·
    hansonwang99 · 2018-08-09 08:04:25 +08:00 · 2415 次点击
    这是一个创建于 2297 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Profile


    概述

    我们平时用 Redis 都是处于用户层面,我们可能会不加思索地操作一个 key-value 对来方便地存取数据,感觉方便之至。但你知道这些数据在背后是如何存储以及编码的吗? 了解清楚了这个问题,将对我们更加高效地使用 Redis 具有指导意义。本文开始我们将结合 Redis 源码来逐个探讨 Redis 五大数据类型的内部编码机制。

    • 实验环境:Redis 4.0.10

    注: 本文首发于 My 公众号 CodeSheep ,可 长按扫描 下面的 小心心 来订阅 ↓ ↓ ↓

    CodeSheep · 程序羊



    Redis 数据类型内部编码概况

    对于 Redis 的常用 5 种数据类型( String、Hash、List、Set、sorted set ),每种数据类型都提供了 最少两种 内部的编码格式,而且每个数据类型内部编码方式的选择 对用户是完全透明的,Redis 会根据数据量自适应地选择较优化的内部编码格式。

    如果想查看某个键的内部编码格式,可以使用 OBJECT ENCODING keyname 指令来进行,比如:

    127.0.0.1:6379> 
    127.0.0.1:6379> set foo bar
    OK
    127.0.0.1:6379> 
    127.0.0.1:6379> object encoding foo  // 查看某个 Redis 键值的编码
    "embstr"
    127.0.0.1:6379> 
    127.0.0.1:6379> 
    

    Redis 的每个键值内部都是使用一个名字叫做 redisObject 这个 C 语言结构体保存的,其代码如下:

    redisObject 结构体

    解释如下:

    • type:表示键值的数据类型,包括 String、List、Set、ZSet、Hash
    • encoding:表示键值的内部编码方式,从 Redis 源码看目前取值有如下几种:
    #define OBJ_ENCODING_RAW 0        /* Raw representation */
    #define OBJ_ENCODING_INT 1        /* Encoded as integer */
    #define OBJ_ENCODING_HT 2         /* Encoded as hash table */
    #define OBJ_ENCODING_ZIPMAP 3     /* Encoded as zipmap */
    #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
    #define OBJ_ENCODING_ZIPLIST 5    /* Encoded as ziplist */
    #define OBJ_ENCODING_INTSET 6     /* Encoded as intset */
    #define OBJ_ENCODING_SKIPLIST 7   /* Encoded as skiplist */
    #define OBJ_ENCODING_EMBSTR 8     /* Embedded sds string encoding */
    #define OBJ_ENCODING_QUICKLIST 9  /* Encoded as linked list of ziplists */
    
    • refcount:表示该键值被引用的数量,即一个键值可被多个键引用

    本文我们就从 Redis 最基本的 String 类型的内部编码开始探讨!



    String 类型的内部编码情况

    字符串是 Redis 最基本的数据类型,Redis 中字符串对象的编码可以是 intraw 或者 embstr 中的某一种,分别介绍如下:

    • int 编码:保存 long 型的 64 位有符号整数
    • embstr 编码:保存长度小于 44 字节的字符串
    • raw 编码:保存长度大于 44 字节的字符串

    我们不妨来做个实验实际看一下:

    String 的各种内部编码格式

    实际情况就是 Redis 内部会根据用户给的不同键值而使用不同的编码格式,而这一切对用户完全透明!

    Redis 是使用 SDS(“简单动态字符串”)这个结构体来存储字符串,代码里定义了 5 种 SDS 结构体:

    struct __attribute__ ((__packed__)) sdshdr5 {
        unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr8 {
        uint8_t len; /* used */
        uint8_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr16 {
        uint16_t len; /* used */
        uint16_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr32 {
        uint32_t len; /* used */
        uint32_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr64 {
        uint64_t len; /* used */
        uint64_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    

    可以看出,除了结构体字段数据类型的不同,其字段含义相差无几,其中:

    • len:字符串的长度(实际使用的长度)
    • alloc:分配内存的大小
    • flags:标志位,低三位表示类型,其余五位未使用
    • buf:字符数组

    了解了这些基本的数据结构以后,我们就来看看上面例子中:

    • set foo 123
    • set foo abc
    • set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx

    这三种情形下 Redis 内部到底是怎么存数据的!



    INT 编码格式

    命令示例: set foo 123

    当字符串键值的内容可以用一个 64 位有符号整形 来表示时,Redis 会将键值转化为 long 型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。

    OBJ_ENCODING_INT 编码类型内部的内存结构可以形象地表示如下:

    set foo 123 时键值的内存结构

    而且 Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set 字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!

    因此,当执行如下指令时:

    set key1 100
    set key2 100
    

    其实 key1key2 这两个键值都直接引用了一个 Redis 预先已建立好的共享 redisObject 对象,就像下面这样:

    共享对象

    源码之前,了无秘密,我们再对照下面的源码,来理解一下上述过程

    INT 编码的源码



    EMBSTR 编码格式

    命令示例: set foo abc

    Redis 在保存长度小于 44 字节的字符串时会采用 OBJ_ENCODING_EMBSTR 编码方式,口说无凭,我们来瞅瞅源码:

    EMBSTR 编码的判断条件

    从上述代码中很容易看出,对于长度小于 44 的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示嵌入式的 String。从内存结构上来讲 即字符串 sds 结构体与其对应的 redisObject 对象分配在 同一块连续的内存空间,这就仿佛字符串 sds 嵌入在 redisObject 对象之中一样,这一切从下面的代码即可清楚地看到:

    embedded string

    因此,对于指令 set foo abc 所设置的键值,其内存结构示意图如下:

    set foo abc 时的键值内存结构



    RAW 编码格式

    指令示例: set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx

    正如指令示例,当字符串的键值为长度大于 44超长字符串 时,Redis 则会将键值的内部编码方式改为 OBJ_ENCODING_RAW 格式,这与上面的 OBJ_ENCODING_EMBSTR 编码方式的不同之处在于 此时动态字符串 sds 的内存与其依赖的 redisObject 的 内存不再连续 了,以 set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx 为例,其键值的内存结构如下所示:

    set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx 时键值的内存结构

    到此就讲完了最基本的 String 数据类型的内部编码情况,怎么样,还是挺好理解的吧!

    后续我们将继续剖析 Redis 中 Hash 数据类型的内部编码格式。



    后 记

    由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!

    作者更多的 SpringBt 实践文章在此:


    如果有兴趣,也可以抽点时间看看作者一些关于容器化、微服务化方面的文章:



    12 条回复    2018-08-09 16:50:03 +08:00
    CYKun
        1
    CYKun  
       2018-08-09 08:45:30 +08:00 via Android
    请问题图的钟表是在哪儿买的?
    x7395759
        2
    x7395759  
       2018-08-09 09:06:15 +08:00
    请问题图的西瓜好吃吗?
    misaka19000
        3
    misaka19000  
       2018-08-09 09:09:22 +08:00 via Android
    请问题图的显示器是什么型号的
    thomaswang
        4
    thomaswang  
       2018-08-09 09:13:31 +08:00
    也看 redis 源码,也有这样的装备,键盘的颜色我一定要统一的
    hotea
        5
    hotea  
       2018-08-09 09:26:16 +08:00
    请问题图的键盘是啥牌子的?
    newtype0092
        6
    newtype0092  
       2018-08-09 09:48:17 +08:00
    请问题图的 PS4 里插的什么盘?
    Zzdex
        7
    Zzdex  
       2018-08-09 09:50:40 +08:00
    题图的键盘啥牌子
    hansonwang99
        8
    hansonwang99  
    OP
       2018-08-09 11:49:08 +08:00 via iPhone
    你们的问题都跑偏了
    VinKing
        9
    VinKing  
       2018-08-09 11:52:42 +08:00
    感谢楼主画这么好看的图,文章也很长知识。不过在 `RAW 编码格式` 部分 redisObject 这个图的 ptr 部分其实不严谨。
    nickr
        10
    nickr  
       2018-08-09 12:49:06 +08:00
    看 画的图 是在 windows 上画的? 那楼顶的照片有欺诈嫌疑..
    m3dull
        11
    m3dull  
       2018-08-09 15:17:37 +08:00
    写的很清楚啊,我要转载
    hansonwang99
        12
    hansonwang99  
    OP
       2018-08-09 16:50:03 +08:00 via iPhone
    感谢楼上的宝宝,还请注明出处
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2706 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 12:30 · PVG 20:30 · LAX 04:30 · JFK 07:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.