redis源码学习_整数集合

redis里面的整数集合保存的都是整数,有int_16、int_32和int_64这3种类型,和C++中的set容器差不多。

同时具备如下特点:

1、set里面的数不重复,均为唯一。

2、set里面的数是从小到大有序的,这在后面的intsetAdd函数中可以看到。

然后由于我们可以同时存储int_16、int_32和int_64这3种类型,一开始只能为一种类型。假设为int_32,那么我们要插入一个int_16类型的数,只需要找到位置直接插入就可以了;但是我们要插入一个int_64类型的数,我们需要先升级,然后再插入。之所以要升级是为了可以有足够的空间存下位数更多的整数,一开始不直接搞成int_64是为了节省内存空间,按需升级非常灵活,既可以节省空间,又可以同时存在不同类型(int_16、int_32和int_64)的整数,一举两得!

主要总结一下intset.c和intset.h里面的关键结构体和函数。

先来看一下intset的结构体吧

 1 typedef struct intset {
 2
 3     /*
 4  虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组的真正类型取决于 encoding 属性的值:
 5 如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )。
 6 如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t类型的整数值 (最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。
 7 如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t类型的整数值 (最小值为 -9,223,372,036,854,775,808 ,最大值为9,223,372,036,854,775,807 )。
 8  */
 9     // 编码方式
10     uint32_t encoding;
11     // 集合包含的元素数量
12     uint32_t length;
13     // 保存元素的数组
14     int8_t contents[];
15 } intset;

函数intsetAdd是里面精华部分,在看它之前我们先看一下它用到的一些函数

_intsetValueEncoding:得到实际的类型,即对应的是int_16、int_32和int_64中的哪一个

 1 /* Return the required encoding for the provided value.
 2  *
 3  * 返回适用于传入值 v 的编码方式
 4  *
 5  * T = O(1)
 6  */
 7 static uint8_t _intsetValueEncoding(int64_t v) {
 8     if (v < INT32_MIN || v > INT32_MAX)
 9         return INTSET_ENC_INT64;
10     else if (v < INT16_MIN || v > INT16_MAX)
11         return INTSET_ENC_INT32;
12     else
13         return INTSET_ENC_INT16;
14 }

_intsetSet:在指定位置上面插数

 1 /* Set the value at pos, using the configured encoding.
 2  *
 3  * 根据集合的编码方式,将底层数组在 pos 位置上的值设为 value 。
 4  *
 5  * T = O(1)
 6  */
 7 static void _intsetSet(intset *is, int pos, int64_t value) {
 8
 9     // 取出集合的编码方式
10     uint32_t encoding = intrev32ifbe(is->encoding);
11
12     // 根据编码 ((Enc_t*)is->contents) 将数组转换回正确的类型
13     // 然后 ((Enc_t*)is->contents)[pos] 定位到数组索引上
14     // 接着 ((Enc_t*)is->contents)[pos] = value 将值赋给数组
15     // 最后, ((Enc_t*)is->contents)+pos 定位到刚刚设置的新值上
16     // 如果有需要的话, memrevEncifbe 将对值进行大小端转换
17     if (encoding == INTSET_ENC_INT64) {
18         ((int64_t*)is->contents)[pos] = value;
19         memrev64ifbe(((int64_t*)is->contents)+pos);
20     } else if (encoding == INTSET_ENC_INT32) {
21         ((int32_t*)is->contents)[pos] = value;
22         memrev32ifbe(((int32_t*)is->contents)+pos);
23     } else {
24         ((int16_t*)is->contents)[pos] = value;
25         memrev16ifbe(((int16_t*)is->contents)+pos);
26     }
27 }

关于里面的memrevXXXifbe函数就是个宏

1 #if (BYTE_ORDER == LITTLE_ENDIAN)
2 #define memrev16ifbe(p)
3 #define memrev32ifbe(p)
4 #define memrev64ifbe(p)
5 #else
6 #define memrev16ifbe(p) memrev16(p) //高低位对换
7 #define memrev32ifbe(p) memrev32(p) //高低位对换
8 #define memrev64ifbe(p) memrev64(p) //高低位对换
9 #endif

补充一下大小端的知识:

大端模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端模式:是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

在裘宗燕翻译的《程序设计实践》里,这对术语并没有翻译为“大端”和小端,而是“高尾端”和“低尾端”,这就好理解了:如果把一个数看成一个字符串,比如11223344看成"11223344",末尾是个‘\0‘,‘11‘到‘44‘个占用一个存储单元,那么它的尾端很显然是44,前面的高还是低就表示尾端放在高地址还是低地址,它在内存中的放法非常直观,如下图:

  “高/低尾端”比“大/小端”更不容易让人迷惑。在这两对形容词中,恰好“高”和“大”对应,“低”和“小”对应;既然高尾端对应的是大端,低尾端对应的是小端,那么当你再见到大端和小端这一对术语,就可以在脑中把它们转化成高尾端和低尾端,这时凭着之前的理解,甚至不用回忆,想着高低的字面含义就能回想起它们的含义。

  理解之后,总结一下,记忆的方法是:

    (数据看成字符串)大端——高尾端,小端——低尾端

  稍一思索什么是“高”、什么是"低","尾端"又是什么,问题迎刃而解,再不用担心被“大端”和“小端”迷惑。用这种方式,是时候放弃原先的死记硬背和容易把自己绕进去而发生迷惑的理解了。

16bit宽的数0x1234在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:


内存地址


0x4000


0x4001


存放内容


0x34


0x12

而在Big-endian模式CPU内存中的存放方式则为:


内存地址


0x4000


0x4001


存放内容


0x12


0x34

32bit宽的数0x12345678在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:


内存地址


0x4000


0x4001


0x4002


0x4003


存放内容


0x78


0x56


0x34


0x12

而在Big-endian模式CPU内存中的存放方式则为:


内存地址


0x4000


0x4001


0x4002


0x4003


存放内容


0x12


0x34


0x56


0x78

如何判断系统是大端还是小端呢?

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 int main(int argc, char **argv)
 4 {
 5     union {
 6         short s;
 7         char c[sizeof(short)];
 8     } un;
 9     un.s = 0x0102;
10     if(sizeof(short)==2) {
11         if(un.c[0]==1 && un.c[1] == 2)
12             printf("big-endian\n");
13         else if (un.c[0] == 2 && un.c[1] == 1)
14             printf("little-endian\n");
15         else
16             printf("unknown\n");
17     } else
18         printf("sizeof(short)= %d\n",sizeof(short));
19     exit(0);
20 }

扯远了,我们再回来接着看啊~~~

intsetUpgradeAndAdd:升级函数,这算是set里面比较难的函数了。

 1 /* Upgrades the intset to a larger encoding and inserts the given integer.
 2  *
 3  * 根据值 value 所使用的编码方式,对整数集合的编码进行升级,
 4  * 并将值 value 添加到升级后的整数集合中。
 5  *
 6  * 返回值:添加新元素之后的整数集合
 7  *
 8  * T = O(N)
 9  */
10 static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
11
12     // 当前的编码方式
13     uint8_t curenc = intrev32ifbe(is->encoding);
14
15     // 新值所需的编码方式
16     uint8_t newenc = _intsetValueEncoding(value);
17
18     // 当前集合的元素数量
19     int length = intrev32ifbe(is->length);
20
21     // 根据 value 的值,决定是将它添加到底层数组的最前端还是最后端
22     // 注意,因为 value 的编码比集合原有的其他元素的编码都要大
23     // 所以 value 要么大于集合中的所有元素,要么小于集合中的所有元素
24     // 因此,value 只能添加到底层数组的最前端或最后端
25     int prepend = value < 0 ? 1 : 0;
26
27     /* First set new encoding and resize */
28     is->encoding = intrev32ifbe(newenc);
29
30     is = intsetResize(is,intrev32ifbe(is->length)+1);
31
32     /* Upgrade back-to-front so we don‘t overwrite values.
33      * Note that the "prepend" variable is used to make sure we have an empty
34      * space at either the beginning or the end of the intset. */
35     while(length--)
36         _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
37
38     /* Set the value at the beginning or the end. */
39     // 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
40     if (prepend)
41         _intsetSet(is,0,value);
42     else
43         _intsetSet(is,intrev32ifbe(is->length),value);
44
45     // 更新整数集合的元素数量
46     is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
47
48     return is;
49 }

intsetSearch:找数函数,找插入数的位置,有就不插,没有才插,用了二分查找,呵呵!

 1 static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
 2     int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
 3     int64_t cur = -1;
 4
 5     /* The value can never be found when the set is empty */
 6     if (intrev32ifbe(is->length) == 0) {
 7         if (pos) *pos = 0;
 8         return 0;
 9     } else {
10         /* Check for the case where we know we cannot find the value,
11          * but do know the insert position. */
12         // 因为底层数组是有序的,如果 value 比数组中最后一个值都要大
13         // 那么 value 肯定不存在于集合中,
14         // 并且应该将 value 添加到底层数组的最末端
15         if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
16             if (pos) *pos = intrev32ifbe(is->length);
17             return 0;
18         // 因为底层数组是有序的,如果 value 比数组中最前一个值都要小
19         // 那么 value 肯定不存在于集合中,
20         // 并且应该将它添加到底层数组的最前端
21         } else if (value < _intsetGet(is,0)) {
22             if (pos) *pos = 0;
23             return 0;
24         }
25     }
26
27     // 在有序数组中进行二分查找
28     while(max >= min) {
29         mid = (min+max)/2;
30         cur = _intsetGet(is,mid);
31         if (value > cur) {
32             min = mid+1;
33         } else if (value < cur) {
34             max = mid-1;
35         } else {
36             break;
37         }
38     }
39
40     // 检查是否已经找到了 value
41     if (value == cur) {
42         if (pos) *pos = mid;
43         return 1;
44     } else {
45         if (pos) *pos = min;
46         return 0;
47     }
48 }

intsetMoveTail:移动函数

 1 static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
 2
 3     void *src, *dst;
 4
 5     uint32_t bytes = intrev32ifbe(is->length)-from;
 6
 7     uint32_t encoding = intrev32ifbe(is->encoding);
 8
 9     //这里的移动可就是批量移动的了,见后面的memmove
10     if (encoding == INTSET_ENC_INT64) {
11         src = (int64_t*)is->contents+from;
12         dst = (int64_t*)is->contents+to;
13         bytes *= sizeof(int64_t);
14     } else if (encoding == INTSET_ENC_INT32) {
15         src = (int32_t*)is->contents+from;
16         dst = (int32_t*)is->contents+to;
17         bytes *= sizeof(int32_t);
18     } else {
19         src = (int16_t*)is->contents+from;
20         dst = (int16_t*)is->contents+to;
21         bytes *= sizeof(int16_t);
22     }
23
24     memmove(dst,src,bytes);
25 }

看了前面那么多,我们在看函数intsetAdd,那就太简单了

 1 intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
 2
 3     // 计算编码 value 所需的长度
 4     uint8_t valenc = _intsetValueEncoding(value);
 5     uint32_t pos;
 6
 7     // 默认设置插入为成功
 8     if (success) *success = 1;
 9
10     /* Upgrade encoding if necessary. If we need to upgrade, we know that
11      * this value should be either appended (if > 0) or prepended (if < 0),
12      * because it lies outside the range of existing values. */
13     if (valenc > intrev32ifbe(is->encoding)) {
14         /* This always succeeds, so we don‘t need to curry *success. */
15         return intsetUpgradeAndAdd(is,value);
16     } else {
17         /* Abort if the value is already present in the set.
18          * This call will populate "pos" with the right position to insert
19          * the value when it cannot be found. */
20         if (intsetSearch(is,value,&pos)) {
21             if (success) *success = 0;
22             return is;
23         }
24
25         //将 value 添加到整数集合中并为 value 在集合中分配空间
26         is = intsetResize(is,intrev32ifbe(is->length)+1);
27
28         if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
29     }
30
31     // 将新值设置到底层数组的指定位置中
32     _intsetSet(is,pos,value);
33
34     // 增一集合元素数量的计数器
35     is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
36
37     // 返回添加新元素后的整数集合
38     return is;
39 }

再看一个删除函数intsetRemove,也是上面各种操作的结合体,也比较简单了

 1 intset *intsetRemove(intset *is, int64_t value, int *success) {
 2     uint8_t valenc = _intsetValueEncoding(value);
 3     uint32_t pos;
 4     if (success) *success = 0;
 5
 6
 7     if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
 8         uint32_t len = intrev32ifbe(is->length);
 9
10         /* We know we can delete */
11         if (success) *success = 1;
12
13         /* Overwrite value with tail and update length */
14         if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
15
16         is = intsetResize(is,len-1);
17
18         is->length = intrev32ifbe(len-1);
19     }
20
21     return is;
22 }

最后我们再求个长度就结束了吧 zzzZZZ……

1 size_t intsetBlobLen(intset *is) {
2     return sizeof(intset)+intrev32ifbe(is->length)*intrev32ifbe(is->encoding);
3 }

关于大小端的参考材料:

http://blog.csdn.net/zhaoshuzhaoshu/article/details/37600857/

http://www.cnblogs.com/wi100sh/p/4899460.html

时间: 2024-10-12 08:15:28

redis源码学习_整数集合的相关文章

redis源码学习_链表

redis的链表是双向链表,该链表不带头结点,具体如下: 主要总结一下adlist.c和adlist.h里面的关键结构体和函数. 链表节点结构如下: 1 /* 2 * 双端链表节点 3 */ 4 typedef struct listNode { 5 6 // 前置节点 7 struct listNode *prev; //如果是list的头结点,则prev指向NULL 8 9 // 后置节点 10 struct listNode *next;//如果是list尾部结点,则next指向NULL

redis源码学习_字典

redis中字典有以下要点: (1)它就是一个键值对,对于hash冲突的处理采用了头插法的链式存储来解决. (2)对rehash,扩展就是取第一个大于等于used * 2的2 ^ n的数作为新的hash表大小:缩紧就是取第一个大于等于used的2 ^ n的数作为新的hash表大小.后面会介绍到dict结构体中是有dictht ht[2]这个成员变量的,为什么是2个呢?就是为了做rehash时交替使用的.那么何时扩展,何时缩紧呢?有个负载因子的概念(负载因子 = used / size,注意这里面

redis源码学习_简单动态字符串

SDS相比传统C语言的字符串有以下好处: (1)空间预分配和惰性释放,这就可以减少内存重新分配的次数 (2)O(1)的时间复杂度获取字符串的长度 (3)二进制安全 主要总结一下sds.c和sds.h中的关键函数 1.sdsmapchars 1 /* Modify the string substituting all the occurrences of the set of 2 * characters specified in the 'from' string to the corresp

Redis源码学习-Lua脚本

Redis源码学习-Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开发环境搭建>. 要注意的是:在Cygwin中安装Lua解析器后,SublimeClang插件就能识别出可饮用的Lua头文件了,因为Build System中我们已经配置过"-I", "D:\\cygwin64\\usr\\include",而新安装的Lua头文件会

Redis源码学习:字符串

Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串,还用来当做缓冲区,例如AOF缓冲区或输入缓冲区等.如下所示,整数len和free分别表示buf数组中已使用的长度和剩余可用的长度,buf是一个原生C字符串,以\0结尾. sds就是sdshdr中char buf[]的别名,后面能看到,各种操作函数的入参和返回值都是sds而非sdshdr.那sdshd

Redis源码学习-AOF

前言 网络上也有许多介绍redis的AOF机制的文章,但是从宏观上介绍aof的流程,没有具体分析在AOF过程中涉及到的数据结构和控制机制.昨晚特别看了2.8源码,感觉源码中的许多细节是值得细细深究的.特别是list *aof_rewrite_buf_blocks结构.仔细看源码,会发现原来看网络文章多的到的领会是片面的,最好的学习还是得自己动手... 原文链接: http://blog.csdn.net/ordeder/article/details/39271543 作者提及的AOF简化的流程

Redis源码学习

Redis内置数据结构之双向链表list http://blog.csdn.net/xiejingfa/article/details/50938028 Redis内置数据结构之字符串sds http://blog.csdn.net/xiejingfa/article/details/50972592 Redis内置数据结构之字典dict http://blog.csdn.net/xiejingfa/article/details/51018337 Redis内置数据结构之压缩列表ziplist

redis源码学习(客户端)

大概介绍 redis 客户端设计主要是存储客户的链接,请求,请求解析的命令,执行结果.先看server的结构和client的结构,server里面有多个client,相当于一个服务端可以连多个客户端,服务端根据事件触发模式依次处理客户端的请求. server结构 struct redisServer { /* General */ // 配置文件的绝对路径 char *configfile; /* Absolute config file path, or NULL */ // serverCr

redis源码学习(集群)

集群是一种分布式的思想,把数据存储到各个节点上去提供服务.分布式一个重要的步骤,就是分片.那redis集群是怎么分片的呢,以及集群服务的稳定性和可靠性怎么保证.下面就来剖析下. 1. 集群是怎么创建的,分片怎么设计的? 分片原理: redis的核心数据是个hash table,分片是按照key值来划分,再reds里面叫槽(slot),集群中的各个节点都会分一些槽,用于存储它们的数据. 客户端在请求集群的服务时候,通过查询的key,算出一个hash值,再使用hash值%槽数得到第几个槽,再通过对应