分布式键值系统redis思考一

“Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.” —— Redis是一个开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。(摘自官网)
好,逼装完了,我们依然像之前分析mysql一样先从底层说起,那么redis的底层的数据结构是怎样的呢。

未安装redis的可以参照
https://blog.csdn.net/xk4848123/article/details/105315869

redis数据结构

Redis 有 5 种基础数据结构,它们分别是:string(字符串)、list(列表)、hash(字典)、set(集合) 和 zset(有序集合)。这 5 种是 Redis 相关知识中最基础、最重要的部分,下面我们结合源码以及一些实践来给大家分别讲解一下。
在这里插入图片描述

数据结构操作演示

1.字符串string

redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。

127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> get counter
(nil)
127.0.0.1:6379> set counter 1
OK
127.0.0.1:6379> INCRBY counter
(error) ERR wrong number of arguments for 'incrby' command
127.0.0.1:6379> INCRBY counter 1
(integer) 2
127.0.0.1:6379> INCR counter 1
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> INCR counter
(integer) 3

分布式场景中可以用incr来作计数器,因为redis网络处理是单线程的。

2.列表list

127.0.0.1:6379> LPUSH list 1
(integer) 1
127.0.0.1:6379> RPUSH list 2
(integer) 2
127.0.0.1:6379> LPOP list
"1"
127.0.0.1:6379> LPOP list
"2"
127.0.0.1:6379> 
127.0.0.1:6379> LPUSH list a
(integer) 1
127.0.0.1:6379> LPUSH list b
(integer) 2
127.0.0.1:6379> LPUSH list last
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "last"
2) "b"
3) "a"
127.0.0.1:6379> 

1.LPUSH,RPUSH分别是从头入队、从尾入队

2.LPOP,RPOP分别是从头出队、从尾出队

3.LRANGE 范围

3.哈希

127.0.0.1:6379> HSET books java "think in java"
(integer) 1
127.0.0.1:6379> HSET books python "python cookbook"
(integer) 1
127.0.0.1:6379> HGETALL books
1) "java"
2) "think in java"
3) "python"
4) "python cookbook"
127.0.0.1:6379> 
127.0.0.1:6379> HGET books java
"think in java"
127.0.0.1:6379> HSET books java "head first java"
(integer) 0
127.0.0.1:6379> HGETALL books
1) "java"
2) "head first java"
3) "python"
4) "python cookbook"
127.0.0.1:6379> HMSET books java "effetive  java" python "learning python"
OK

4.set集合(无序去重)

127.0.0.1:6379> SADD books1 python
(integer) 1
127.0.0.1:6379> SADD books1 java
(integer) 1
127.0.0.1:6379> SADD books1 java
(integer) 0
127.0.0.1:6379> SADD books1 golang
(integer) 1
127.0.0.1:6379> SMEMBERS books1
1) "golang"
2) "java"
3) "python"
127.0.0.1:6379> SCARD books1
(integer) 3
127.0.0.1:6379> SPOP books1
"golang"

5.有序列表 zset

127.0.0.1:6379> ZADD books2 1586098912 "task1"
(integer) 1
127.0.0.1:6379> ZADD books2 1586099912 "task2"
(integer) 1
127.0.0.1:6379> ZADD books2 1586109912 "task3"
(integer) 1
127.0.0.1:6379> ZRANGE books2 0 1
1) "task1"
2) "task2"
127.0.0.1:6379> ZADD books2 1586097912 "task4"
(integer) 1
127.0.0.1:6379> ZRANGE books2 0 1
1) "task4"
2) "task1"
127.0.0.1:6379> ZRANGE books2 0 -1
1) "task4"
2) "task1"
3) "task2"
4) "task3"
127.0.0.1:6379> ZRVRANGE books2 0 -1
(error) ERR unknown command `ZRVRANGE`, with args beginning with: `books2`, `0`, `-1`, 
127.0.0.1:6379> ZREVRANGE books2 0 -1
1) "task3"
2) "task2"
3) "task1"
4) "task4"
127.0.0.1:6379> ZCARD books2
(integer) 4

ZRANGEBYSCORE命令可以用来实现延时队列,这时score记未来想要执行的某个时间戳。

更多数据结构

6. 布隆过滤器

在https://github.com/RedisBloom/RedisBloom下载最新的release源码,在编译服务器进行解压编译:

tar zxvf RedisBloom-2.2.2.tar.gz
cd RedisBloom-2.2.2
make
cp ./redisbloom.so /usr/local/redis/src/

关闭redis-server

127.0.0.1:6379> shutdown
not connected> exit
[root@localhost bin]# ps -ef|grep redis
root       8097   6020  0 08:17 pts/0    00:00:00 grep --color=auto redis

重启redis,指定了默认的容量与容错率

./redis-server /usr/local/redis/conf/redis.conf --loadmodule /usr/local/redis/src/rebloom.so INITIAL_SIZE 10000000 ERROR_RATE 0.0001
127.0.0.1:6379> BF.MADD test user2 user2
1) (integer) 1
2) (integer) 0
127.0.0.1:6379> BF.MADD test user2 user3
1) (integer) 0
2) (integer) 1
127.0.0.1:6379> BF.MADD test user4 user5
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> 
127.0.0.1:6379> BF.EXISTS test user1
(integer) 1
127.0.0.1:6379> BF.EXISTS test user6
(integer) 0
127.0.0.1:6379> BF.MEXISTS test user1 user6
1) (integer) 1
2) (integer) 0

1,BF.ADD {key} {item}
单条添加元素
向Bloom filter添加一个元素,如果该key不存在,则创建该key(过滤器)。
如果项是新插入的,则为“1”;如果项以前可能存在,则为“0”。

2,BF.MADD {key} {item} [item…]
批量添加元素
布尔数(整数)的数组。返回值为0或1的范围的数据,这取决于是否将相应的输入元素新添加到过滤器中,或者是否已经存在。

3,BF.EXISTS {key} {item}
判断单个元素是否存在
如果存在,返回1,否则返回0

4,BF.MEXISTS {key} {item} [item…]
判断多个元素是否存在
布尔数(整数)的数组。返回值为0或1的范围的数据,这取决于是否将相应的元是否已经存在于key中。

7.HyperLogLog计算基数

127.0.0.1:6379> PFADD hyperloglog a
(integer) 1
127.0.0.1:6379> PFADD hyperloglog b
(integer) 1
127.0.0.1:6379> PFADD hyperloglog c
(integer) 1
127.0.0.1:6379> PFADD hyperloglog b
(integer) 0
127.0.0.1:6379> PFADD hyperloglog c
(integer) 0
127.0.0.1:6379> PFCOUNT hyperloglog
(integer) 3
127.0.0.1:6379> PFADD hyperloglog1 d
(integer) 1
127.0.0.1:6379> PFADD hyperloglog1 c
(integer) 1
127.0.0.1:6379> PFMERGE hyper hyperloglog hyperloglog1
OK
127.0.0.1:6379> PFCOUNT hyper
(integer) 4

PFADD (新增操) 丶PFCOUNT( 计算基数,就是几个不重复的元素)丶PFMERGE(合并)等操作

redis数据结构底层知识

OBJECT ENCODING key可以查看底层数据结构

127.0.0.1:6379> LRANGE list 0 -1
1) "last"
2) "b"
3) "a"
127.0.0.1:6379> OBJECT ENCODING list
"quicklist"
127.0.0.1:6379> OBJECT ENCODING hello
"embstr"
127.0.0.1:6379> set hello 1
OK
127.0.0.1:6379> OBJECT ENCODING hello
"int"
127.0.0.1:6379> set hello nihao
OK
127.0.0.1:6379> OBJECT ENCODING hello
"embstr"

在这里插入图片描述
Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redisObject 结构来表示:

typedef struct redisObject{
     //类型
     unsigned type:4;
     //编码
     unsigned encoding:4;
     //指向底层数据结构的指针
     void *ptr;
     //引用计数
     int refcount;
     //记录最后一次被程序访问的时间
     unsigned lru:22;
 
}rob

j

1.SDS

Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。
在这里插入图片描述
图片来源:《Redis设计与实现》

上面对于 SDS 数据类型的定义:
1、len 保存了SDS保存字符串的长度
2、buf[] 数组用来保存字符串的每个元素
3、free j记录了 buf 数组中未使用的字节数量

好处:
1、常数复杂度获取字符串长度
2、杜绝缓冲区溢出( len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展)
3、减少修改字符串的内存重新分配次数(空间预分配和惰性空间释放)
4、二进制安全(len 属性表示的长度来判断字符串是否结束而不依赖空字符,因为二进制文件可能包括空字符)

SDS 除了保存数据库中的字符串值以外,SDS 还可以作为缓冲区(buffer):包括 AOF 模块中的AOF缓冲区以及客户端状态中的输入缓冲区。

2.链表

在这里插入图片描述
Redis链表特性:
1、双端(有head、tail指针,获取这两个节点时间复杂度都为O(1))
2、无环
3、带链表长度计数器( len 属性获取链表长度的时间复杂度为 O(1))
4、多态(链表节点使用void*指针来保存节点值,可以保存各种不同类型的值)

3.字典

字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。
Redis 中的字典相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 “数组 + 链表” 的链地址法来解决部分 哈希冲突
源码定义如 dict.h/dictht 定义:

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    // 内部有两个 dictht 结构
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry 结构的指针,而每个dictEntry结构保存着一个键值对

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

实际上字典结构的内部包含两个hashtable,通常情况下只有一个 hashtable是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁。
1丶渐进式rehash
什么叫渐进式 rehash?也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的。

2、触发扩容的条件
1、服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。

大家思考一个文件,redis是单线程的,那么它是在什么时候渐进式rehash的呢。

4.跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的(O(n/2))。
在这里插入图片描述
1、有很多层
2、上一层有的元素,下一层必有元素
3、最底层有所有的元素

更多关于跳表的知识可以参考:
https://blog.csdn.net/sunxianghuang/article/details/52221913

5.整数集合

整数集合(intset)是Redis用于保存整数值的集合抽象数据类型,它可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中不会出现重复元素。

typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
 
}intset;

需要注意的是虽然 contents 数组声明为 int8_t 类型,但是实际上contents 数组并不保存任何 int8_t 类型的值,其真正类型有 encoding 来决定。
1、升级(当我们新增的元素类型比原集合元素类型的长度要大时,需要对整数集合进行升级,才能将新元素放入整数集合中)
2、降级(升级后不可降级)

6.压缩列表

压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
  压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
  在这里插入图片描述
压缩列表的每个节点构成如下:
在这里插入图片描述
1、previous_entry_length
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节:
如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面
如果前一个节点的长度大于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一个字节会被设置为oxFE,而之后的四个字节则用于保存前一个节点的长度。
因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址计算出前一个节点的起始地址。
压缩列表从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的previous_entry_length属性,程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。
2、encoding
节点的encoding属性记录了节点的content属性所保存数据的类型以及长度
3、content
节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

注意压缩列表可能会出连锁更新问题,大家可以去思考一下为什么,着重看下previous_entry_length的机制。

总结

以上介绍的简单字符串、链表、字典、跳跃表、整数集合、压缩列表等数据结构就是Redis底层的一些数据结构。
小伙伴们不管是后面讲到的线程模型也好,分布式锁或是lua脚本,还是高可用架构,没有这些底层数据作支撑都是扯犊子,可谓是redis的基石~谢谢大家,动手点个赞呗。

  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值