携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情
前置tips
接着上篇提到redis的hash结构扩容计算新的数组长度时,不妨分析下Java中HashMap的计算方式
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
原理如上图所示:将输入的值减一,分别无符号左移1,2,4,8,16位进行或操作.首先无符号左移的目的是将最存在1的最高位左移一位,保证或操作时低位也为1.如
n=9
n-1 0000 1000
n>>>1 0000 0100
| 0000 1000
= 0000 1100
n>>>2 0000 0010
| 0000 1100
= 0000 1110
n>>>4 0000 0001
| 0000 1110
= 0000 1111
n>>>8 0000 0000
| 0000 1111
= 0000 1111
从上图可知始终保证非0位为1的某个值经过计算后其后续低位始终为1. 此处为什么不进行无符号右移32位呢.因为int取值范围为4个字节,总共32个bit.由于最高位为符号位,那么只有31个bit可用.
SDS
在C语言中,字符串是以’\0’字符结尾(NULL结束符)的字符数组来存储的,通常表达为字符指针的形式(char *)。它不允许字节0出现在字符串中间,因此,它不能用来存储任意的二进制数据。 Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示,这么做的原因是:
- 字符串长度获取时间复杂度为o(1),而C语言获取长度需要遍历整个字符串
- 二进制安全,c中的字符串以数组存储,以'\0'代表结束,也就是说一旦数组中存储'\0'的字符,那么原有的字符串将会发生改变
- 动态扩容,sds分配长度始终是大于等于使用的长度
- 惰性空间释放.
SDS 组成结构
在Redis中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: 表示字符串的真正长度(不包含NULL结束符在内)。
- alloc: 表示字符串的最大容量(不包含最后多余的那个字节)。
- flags: 总是占用一个字节。其中的最低3个bit用来表示header的类型。header的类型共有5种,在sds.h中有常量定义。
- buf: 表示实际存储数据的数组
首先当我们获取到一个sds时,必须要知道其底层的结构.由flags来区分.
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5)
return SDS_TYPE_5;
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}
结合上述代码可以发现不同的类型对应的长度:
- 长度在0和2^5-1之间,选用SDS_TYPE_5类型的header。
- 长度在2^5和2^8-1之间,选用SDS_TYPE_8类型的header。
- 长度在2^8和2^16-1之间,选用SDS_TYPE_16类型的header。
- 长度在2^16和2^32-1之间,选用SDS_TYPE_32类型的header。
- 长度大于2^32的,选用SDS_TYPE_64类型的header。能表示的最大长度为2^64-1。
图中展示了两个sds字符串s1和s2的内存结构,一个使用sdshdr8类型的header,另一个使用sdshdr16类型的header.
sds的字符指针(s1和s2)就是指向真正的数据(字符数组)开始的位置,而header位于内存地址较低的方向。在sds.h中有一些跟解析header有关的宏定义.
当然,使用SDS_HDR之前我们必须先知道到底是哪一种header,这样我们才知道SDS_HDR第1个参数应该传什么。由sds字符指针获得header类型的方法是,先向低地址方向偏移1个字节的位置,得到flags字段。比如,s1[-1]和s2[-1]分别获得了s1和s2的flags的值。然后取flags的最低3个bit得到header的类型。
- 由于s1[-1] == 0x01 == SDS_TYPE_8,因此s1的header类型是sdshdr8。
- 由于s2[-1] == 0x02 == SDS_TYPE_16,因此s2的header类型是sdshdr16。
SDS 编码.
SDS存储不同数据会对应不同的编码encoding.取值范围:
- int: 存储数值类型
- raw: 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
- embstr: 当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。
embstr
44个字节如何计算的: 由于cpu缓存行大小为64字节,RedisObject元数据大小8个字节,ptr为8字节,flag为1个字节,'\0'结束符1个字节,len.alloc均为1个字节.(此处redis版本需要大于等于3.2版本). 在Redis3.2版本以前的sds结构如下:
- len: 表示使用的长度 4个字节
- free: 表示buf没有使用的长度.4个字节
- buf: 数组,真正存储数据的地方,以'0'结尾 计算embstre长度为64-16-4-4-1=39.
embstr编码集只读
当设置一个embstr的编码集时,对其进行追加字符串后并未达到embstr的最大长度,直接变为raw编码集; 再对embstr编码集的字符串进行修改命令时,总会变成一个raw编码集的字符串; redis的编码转换过程不可逆 只能从小的编码转换到大的编码(不包括set)**
set a asasasasasasasasasasasasas
"OK"
object encoding a
"embstr"
append a a
object encoding a
"raw"`