“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的基石~谢谢大家,动手点个赞呗。