1. 存储的结构
    在 redis 字符串对象 String 的介绍中,我们知道 redis 对于字符串的存储共有 3 种存储形式,其存储的内存结构如以下图片示例:

OBJ_ENCODING_INT: 保存的字符串长度小于 20,并且是可以解析为 long 类型的整数值,那么存储方式就是直接将 redisObject 的 ptr 指针指向这个整数值

OBJ_ENCODING_EMBSTR: 长度小于 44 (OBJ_ENCODING_EMBSTR_SIZE_LIMIT)的字符串将以简单动态字符串(SDS) 的形式存储在 redisObject 中,但是redisObject 对象头会和 SDS 对象连续存在一起

OBJ_ENCODING_RAW: 字符串以简单动态字符串(SDS) 的形式存储,redisObject 对象头和 SDS 对象在内存地址上一般是不连续的两块内存

2. 数据存储源码分析
2.1 数据存储过程
在 Redis 6.0 源码阅读笔记(1)-Redis 服务端启动及命令执行 中我们已经知道客户端保存字符串的 set 命令将会调用到 t_string.c#setCommand() 函数,其源码实现如下:

该方法中有以下两个重点函数被调用,本节主要关注 tryObjectEncoding() 函数

tryObjectEncoding() 将客户端传输过来的需保存的字符串对象尝试进行编码,以节约内存
setGenericCommand() 将 key-value 保存到数据库中

void setCommand(client *c) {......c->argv[2] = tryObjectEncoding(c->argv[2]);setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

object.c#tryObjectEncoding() 函数逻辑很清晰,可以看到主要进行了以下几个操作:

当字符串长度小于 20 并且可以被解析为 long 类型数据时,这个数据将以整数形式保存,并以 robj->ptr = (void*) value 这种直接赋值的形式存储
当字符串长度小于等于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 配置并且还是 raw 编码时,调用 createEmbeddedStringObject() 函数将其转化为 embstr 编码
这个字符串对象已经不能进行转码了,只好调用 trimStringObjectIfNeeded() 函数尝试从字符串对象中移除所有空余空间

robj *tryObjectEncoding(robj *o) {long value;sds s = o->ptr;size_t len;/* Make sure this is a string object, the only type we encode* in this function. Other types use encoded memory efficient* representations but are handled by the commands implementing* the type. */serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);/* We try some specialized encoding only for objects that are* RAW or EMBSTR encoded, in other words objects that are still* in represented by an actually array of chars. */if (!sdsEncodedObject(o)) return o;/* It's not safe to encode shared objects: shared objects can be shared* everywhere in the "object space" of Redis and may end in places where* they are not handled. We handle them only as values in the keyspace. */if (o->refcount > 1) return o;/* Check if we can represent this string as a long integer.* Note that we are sure that a string larger than 20 chars is not* representable as a 32 nor 64 bit integer. */len = sdslen(s);if (len <= 20 && string2l(s,len,&value)) {/* This object is encodable as a long. Try to use a shared object.* Note that we avoid using shared integers when maxmemory is used* because every object needs to have a private LRU field for the LRU* algorithm to work well. */if ((server.maxmemory == 0 ||!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&value >= 0 &&value < OBJ_SHARED_INTEGERS){decrRefCount(o);incrRefCount(shared.integers[value]);return shared.integers[value];} else {if (o->encoding == OBJ_ENCODING_RAW) {sdsfree(o->ptr);o->encoding = OBJ_ENCODING_INT;o->ptr = (void*) value;return o;} else if (o->encoding == OBJ_ENCODING_EMBSTR) {decrRefCount(o);return createStringObjectFromLongLongForValue(value);}}}/* If the string is small and is still RAW encoded,* try the EMBSTR encoding which is more efficient.* In this representation the object and the SDS string are allocated* in the same chunk of memory to save space and cache misses. */if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {robj *emb;if (o->encoding == OBJ_ENCODING_EMBSTR) return o;emb = createEmbeddedStringObject(s,sdslen(s));decrRefCount(o);return emb;}/* We can't encode the object...** Do the last try, and at least optimize the SDS string inside* the string object to require little space, in case there* is more than 10% of free space at the end of the SDS string.** We do that only for relatively large strings as this branch* is only entered if the length of the string is greater than* OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */trimStringObjectIfNeeded(o);/* Return the original object. */return o;
}

object.c#createEmbeddedStringObject() 函数实现 embstr 编码也很简单,主要步骤如下:

首先调用 zmalloc() 函数申请内存,可以看到此处不仅申请了需要存储的字符串的内存及 redisObject 的内存,还申请了 SDS 实现结构体之一 sdshdr8 的内存,这也就是上文所说embstr 编码只申请一次内存,并且redisObject 对象头会和 SDS 对象连续存在一起的由来
将 redisObject 对象的 ptr 指针指向 sdshdr8 开始的内存地址
填充 sdshdr8 对象各个属性,包括 len 字符串长度,alloc 字符数组容量,实际存储字符串的 buf 字符数组

robj *createEmbeddedStringObject(const char *ptr, size_t len) {robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);struct sdshdr8 *sh = (void*)(o+1);o->type = OBJ_STRING;o->encoding = OBJ_ENCODING_EMBSTR;o->ptr = sh+1;o->refcount = 1;if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;} else {o->lru = LRU_CLOCK();}sh->len = len;sh->alloc = len;sh->flags = SDS_TYPE_8;if (ptr == SDS_NOINIT)sh->buf[len] = '\0';else if (ptr) {memcpy(sh->buf,ptr,len);sh->buf[len] = '\0';} else {memset(sh->buf,0,len+1);}return o;
}

raw 编码字符串的创建可参考 object.c#createRawStringObject() 函数,其涉及到两次内存申请,sds.c#sdsnewlen() 申请内存创建 SDS 对象,object.c#createObject() 申请内存创建 redisObject 对象

robj *createRawStringObject(const char *ptr, size_t len) {return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

从检测容量大小的的函数t_string.c#checkStringLength()看,字符串最大长度为 512M,超出该数值将报错

static int checkStringLength(client *c, long long size) {if (size > 512*1024*1024) {addReplyError(c,"string exceeds maximum allowed size (512MB)");return C_ERR;}return C_OK;
}

2.2 简单动态字符串 SDS
2.2.1 SDS 结构体
SDS(简单动态字符串) 在 Redis 中是实现字符串存储的工具,本质上依然是字符数组,但它不像C语言字符串那样以‘\0’来标识字符串结束

传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止 。即当读一个字符串时,只要遇到’\0’就认为到达末尾,忽略’\0’以后的所有字符。另外其获得字符串长度的做法是遍历字符串,遇零则止,时间复杂度为O(n),比较低效

SDS 的实现结构定义在 sds.h 中,其定义如下。因为 SDS 判断是否到达字符串末尾的依据是表头的 len 属性,所以能高效计算字符串长度并快速追加数据

sds 结构一共有 5 种 Header 定义,目的是为不同长度的字符串提供不同大小的 Header,以节省内存。以 sdshdr8 为例,其 len 属性为 uint8_t 类型,占用内存大小为 1 字节,则存储的字符串最大长度为256。Header 主要包含以下几个属性:

len: 字符串真正的长度,不包含空终止字符
alloc: 除去表头和终止符的 buf 数组长度,也就是最大容量
flags: 标志 header 的类型
buf: 字符数组,实际存储字符

/* Note: sdshdr5 is never used, we just access the flags byte directly.* However is here to document the layout of type 5 SDS strings. */
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[];
};

2.2.2 SDS 容量调整
SDS 扩容的函数是sds.c#sdsMakeRoomFor(),当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。以下为源码实现:

字符串在长度小于 SDS_MAX_PREALLOC(1024*1024,也就是1MB,定义在 sds.h) 之前,采用 2 倍扩容,也就是保留 100% 的冗余空间。当长度超过 SDS_MAX_PREALLOC 之后,每次扩容只会多分配 SDS_MAX_PREALLOC 大小的冗余空间,避免加倍扩容后的冗余空间过大导致浪费

sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;type = sdsReqType(newlen);/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */newsh = s_malloc(hdrlen+newlen+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, newlen);return s;
}

SDS 缩容函数为sds.c#sdsclear(),从源码实现来看,其主要有以下操作,也就是它并不释放实际占用的内存,体现出一种惰性策略

重置 SDS 表头的 len 属性值为 0
将结束符放到 buf 数组最前面,相当于惰性地删除 buf 中的内容

/* Modify an sds string in-place to make it empty (zero length).
* However all the existing buffer is not discarded but set as free space
* so that next append operations will not require allocations up to the
* number of bytes previously available. */
void sdsclear(sds s) {sdssetlen(s, 0);s[0] = '\0';
}

Redis 6.0 源码阅读笔记(4) -- String 数据类型源码分析相关推荐

  1. 代码分析:NASM源码阅读笔记

    NASM源码阅读笔记 NASM(Netwide Assembler)的使用文档和代码间的注释相当齐全,这给阅读源码 提供了很大的方便.按作者的说法,这是一个模块化的,可重用的x86汇编器, 而且能够被 ...

  2. CI框架源码阅读笔记4 引导文件CodeIgniter.php

    到了这里,终于进入CI框架的核心了.既然是"引导"文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http:// ...

  3. Yii源码阅读笔记 - 日志组件

    2015-03-09 一 By youngsterxyf 使用 Yii框架为开发者提供两个静态方法进行日志记录: Yii::log($message, $level, $category); Yii: ...

  4. AQS源码阅读笔记(一)

    AQS源码阅读笔记 先看下这个类张非常重要的一个静态内部类Node.如下: static final class Node {//表示当前节点以共享模式等待锁static final Node SHA ...

  5. 【Flink】Flink 源码阅读笔记(15)- Flink SQL 整体执行框架

    1.概述 转载:Flink 源码阅读笔记(15)- Flink SQL 整体执行框架 在数据处理领域,无论是实时数据处理还是离线数据处理,使用 SQL 简化开发将会是未来的整体发展趋势.尽管 SQL ...

  6. libreCAD源码阅读笔记1

    libreCAD源码阅读笔记1 一 前言: 正如官网(https://www.librecad.org)所说,libreCAD是一个开源的CAD制图软件,可以运行在Windows.Apple.Linu ...

  7. libreCAD源码阅读笔记4

    libreCAD源码阅读笔记4 前言 总的来说,程序主窗口QC_ApplicationWindow使用QMdiArea作为多文档主界面,每个文档QC_MDIWindow使用RS_Document作为数 ...

  8. HashMap源码阅读笔记

    HashMap是Java编程中常用的集合框架之一. 利用idea得到的类的继承关系图可以发现,HashMap继承了抽象类AbstractMap,并实现了Map接口(对于Serializable和Clo ...

  9. [Linux] USB-Storage驱动 源码阅读笔记(一)

    USB-Storage驱动 源码阅读笔记--从USB子系统开始 最近在研究U盘的驱动,遇到很多难以理解的问题,虽然之前也参考过一些很不错的书籍如:<USB那些事>,但最终还是觉得下载一份最 ...

  10. Android TV TIF源码阅读笔记

                                   Android TV TIF源码阅读笔记 1.SystemSever.java if (mPackageManager.hasSystem ...

最新文章

  1. java web 利用ajax 异步向后台提交数据
  2. 数据结构之并查集Union-Find Sets
  3. 如何使用点击超链接的方式打开Android手机上的应用
  4. 光模块功能失效的原因有哪些?
  5. 动态语言会淘汰静态语言吗?
  6. 内存的工作原理(一)
  7. 青岛工学院计算机专业分数线,青岛工学院分数线
  8. springboot导出excel_在SpringBoot中如何在一分钟内实现快速导出Excel
  9. 蓝桥杯---史丰收速算
  10. 神仙项目,轻松上手了解前后端分离!
  11. 美国计算机硕士毕业在美薪资,美国计算机硕士留学费用全都在这了!
  12. 去掉input记录密码时的背景颜色
  13. 恒天然NZMP品牌干酪在2018年国际奶酪大赛中荣获八枚奖牌
  14. 《数据库系统概念》——关系数据库
  15. 最年轻的Kaggle Master:永远不要复制别人的工作!
  16. 2011-12-24
  17. Three.js 教程
  18. WinForm引用ActiveX组件,对Com组件的学
  19. html5 电子白板 直播,HTML5 canvas 做画板画图 可以做电子白板
  20. 1+X Web前端证书中级备考攻略

热门文章

  1. adb服务无法开启问题解决方法
  2. Javascript中call()和apply()的用法 ----1
  3. Unity3d场景漫游---iTween实现
  4. Android 如何全局获取Context
  5. SQL Server 2012大数据导入Oracle的解决方案
  6. windows phone (24) Canvas元素A
  7. arcgis 接口转换与.Net框架
  8. .NET / C# basic
  9. web安全day33:人人都要懂的LNMP--nginx的配置和文件理解
  10. 在sqlyog中创建MySQL触发器简单实例