Redis五种基础数据类型及底层原理

75 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情


1.五种数据类型的原理

Redis使用redisObject对象来表示所有的keyvalueredisObject的信息如下:

Class redisObject {

    String type;//数据类型

    String encoding;//编码方式

    Ptr* obeject;//数据指针
    
    //虚拟内存等其他信息
    ...
}

type:表示属于哪种基本类型,使用type命令可以查看当前 redisObject 的 type 属性,包括string,hash,list,set,sorted set

encoding:表示底层数据结构,使用object encoding命令可以找到对应对象的底层实现,包括raw,int,zipmap,linkedlist,ziplist,intset

ptr:指向底层数据结构的指针

vm:打开了虚拟内存才会分配,默认关闭

  • redis 为什么要设计不同的底层实现呢?

因为 redis 是内存数据库,所有的数据都放在内存中,资源十分珍贵要好好利用,使它尽可能存储多的数据

2.字符串(string)

2.1.简介

string 是 redis 最基本的类型,是二进制安全的,意味着只要能转换成字符串,string 可以包含任何数据,如图片、视频和序列化对象等。一个字符串 value 最多可以是512M。

2.2.常用命令

set key value:添加键值对

get key:查询对应键值

append key value:将给定的value追加到原值的末尾

strlen key:获得值的长度

setnx key value:在key不存在时,设置key的值

2.3.原理

  • 字符串的encoding有三种:int,raw,embstr

1.int

对象值全部是数字(在 long 的最大值范围内,否则将用其他的编码)时,将 encoding 设置为 int ,并将数值保存在 ptr 属性中

image.png

以下两种实现都是基于simple dynamic string实现的

没有使用 C 语言中的 char 数组,因为:

  • char 数组获取字符串长度的时间复杂度是 O(n),而 sds 通过 len 变量可以直接获取长度
  • char 数组、造成缓存区溢出问题,访问到不属于 char 数组的空间:

但是 sds 可以通过 len 和 alloc 来对边界进行检查

  • 可以保存二进制数据,char 数组出现\0就会认为是数组结尾

2.raw

对象是一个字符串值并且大于44个字节时,将 encoding 设置为 raw,使用一个简单动态字符串(SDS)来保存,此时 redisObject 和 sds 的内存地址是不连续的,且分开分配

3.embstr

对象是一个字符串且小于44字节,将 encoding 设置为 embstr ,一次性分配 redisObject 和 sds 的内存,且连续

  • 对比 raw:

    • embstr 创建字符串对象只需一次,raw 需要两次
    • 由于 redisObject 和 sds 是连续的,字符串长度增加时两部分都需要重新分配空间
    • 释放内存时同理
    • 存取速度比 raw 快(内存连续),查找更加方便

2.4.其他

在遇到数值操作时,Redis 会将字符串转换为数值

127.0.0.1:6379> set mynum "2"

OK

127.0.0.1:6379> get mynum

"2"

127.0.0.1:6379> incr mynum

(integer) 3

127.0.0.1:6379> get mynum

"3"

由于 incr 等指令具有原子操作的特性,所以可以利用 incr、incrby、decr、decrby 等指令来实现原子计数的效果

3.列表(list)

3.1.介绍

Redis 中的 list 底层是用压缩链表和双向链表来实现的,所以对于一个有上百万元素的 list 来说,在头部和尾部插入一个新元素,其实际复杂度也是常数级别的;但是 list 元素定位会比较慢。

常用操作

//新建一个 list 叫做 mylist,并在列表头部插入元素"1"

127.0.0.1:6379> lpush mylist "1" 

//返回当前 mylist 中的元素个数

(integer) 1 

//在 mylist 右侧插入元素"2"

127.0.0.1:6379> rpush mylist "2" 

(integer) 2

//在 mylist 左侧插入元素"0"

127.0.0.1:6379> lpush mylist "0" 

(integer) 3

//列出 mylist 中从编号0到编号1的元素

127.0.0.1:6379> lrange mylist 0 1 

1) "0"

2) "1"

//列出 mylist 中从编号0到倒数第一个元素

127.0.0.1:6379> lrange mylist 0 -1 

1) "0"

2) "1"

3) "2"

3.2.原理

list 有两种 encoding:

1.ziplist 压缩列表

在数据量小时使用,优点是节省内存,缺点是消耗更多时间;列表长度少于512,且所有元素长度都小于64个字节时使用

  • zlbytes:记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或计算 zlend 的位置时使用
  • zltail:记录压缩列表表尾结点距离起始地址有多少字节,通过此偏移量无序遍历整个压缩列表就可以确定表尾结点的地址
  • zllen:记录了压缩列表包含的结点数
  • 结点:压缩列表包含的各个结点,结点的长度由结点保存的内容决定
  • zlend:用于标记压缩列表的末端

为了解决数组中内存浪费问题,且保留数组的优势,在不定长度的数组中,在每个结点前都增加一个 length 属性

image.png

这样在遍历结点时就知道每个结点的长度了,这样就是一个简单的压缩列表结构

2.linkedlist

当不满足压缩列表条件时使用,编码方式:

4.哈希(Hash)

4.1.介绍

Redis 中的 Hash 是一个键值对集合,是一个 String 类型的 field 和 value 的映射表

  • 常用命令:

    • hget:通过 key 值,从 hash 里取对应的 value
    • hset:往 hash 里,添加 key-value
    • hmget:一次性获取多个 key 的 value

4.2.原理

Hash 有两种 encoding:

1.ziplist

当保存的键值对数量少于512,且所有的键值对都少于64时,使用压缩列表保存;当有新的键值对加入 Hash 中时,程序先将保存了键的结点推入表尾,再将保存了值的结点推入表尾,因此保存了同一键值对的两个结点总是紧挨在一起

image.png

2.hashtable

当不满足 ziplist 条件时用 hashtable 保存,结构如下:

5.集合(set)

5.1.介绍

Redis 中的集合是无序的,存储结构与 Hash 完全相同,但是仅存储键,不存储值,并且值不允许重复

常用操作:

//向集合myset中加入一个新元素"one"

127.0.0.1:6379> sadd myset "one" 

(integer) 1

127.0.0.1:6379> sadd myset "two"

(integer) 1

//列出集合myset中的所有元素

127.0.0.1:6379> smembers myset 

1) "one"

2) "two"

//判断元素1是否在集合myset中,返回1表示存在

127.0.0.1:6379> sismember myset "one" 

(integer) 1

//判断元素3是否在集合myset中,返回0表示不存在

127.0.0.1:6379> sismember myset "three" 

(integer) 0

//新建一个新的集合yourset

127.0.0.1:6379> sadd yourset "1" 

(integer) 1

127.0.0.1:6379> sadd yourset "2"

(integer) 1

127.0.0.1:6379> smembers yourset

1) "1"

2) "2"

//对两个集合求并集

127.0.0.1:6379> sunion myset yourset 

1) "1"

2) "one"

3) "2"

4) "two"

5.2.原理

set 的 encoding 有两种:

1.intset

当集合的长度小于512,并且所有的元素都是整数时,使用 intset 存储,结构如下:

2.hashtable

与 Hash 的存储完全相同,只是值都为 NULL

image.png

6.有序集合(zset)

6.1.介绍

在 set 的基础上添加可排序字段,该字段可以重复

image.png

常用命令:

  • zadd:添加数据

  • zrem:删除元素

  • zcard:查询数据

  • zrange:根据从小到大排序
127.0.0.1:6379[10]>zrange database 0 2 withscores
1)"mysql"
2)"3"
3)"mongodb"
4)"4"
5)"redis"
6)"5"

withscores 表示根据分数排序,0 2表示排序区间是第0到第2个元素

  • zrevrange:根据从大到小排序

6.2.原理

zset有两种编码:

1.ziplist

当长度小于128并且所有的元素长度都少于64字节时用ziplist存储,此时每个结点前面是字符串后面是分数值,小的在表头,大的在表末

image.png

2.skiplist

Redis 的 skiplist 是由字典 dict 和跳表组成的

  • 跳表

原始链表中查询结点15要查询15个结点,当加入一层索引:

image.png

只需查询7个结点,再加一层索引:

image.png

此时只需查询6个结点了;

跳表每层索引的结点都是前一层的一半,过程类似于二分查找;但是维持这种关系在删除和插入结点时要对整个跳表进行调整,会降低效率

  • skiplist 不要求这种关系,而是每个结点随机出一个层数,把这个结点链入从第一层至随机出的层:

image.png

这样新插入一个结点不会影响其他结点的层数,只需要修改插入结点的前后指针,降低了操作的复杂度

skiplist 中:

  • dict 用于记录字符串对象和分数,查询字符串对应分数
  • 跳表用来根据分数查询对应字符串

好处:

  • 字典的查询分值时间复杂度是 O(1),但是无序
  • 跳表有序但是查询的时间复杂度是 O(logn)
  • 两个结构中元素成员和分值是共享的,通过指针指向同一地址

结构如下:

image.png