携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
1.五种数据类型的原理
Redis使用redisObject对象来表示所有的key和value,redisObject的信息如下:
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 属性中
以下两种实现都是基于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 属性
这样在遍历结点时就知道每个结点的长度了,这样就是一个简单的压缩列表结构
2.linkedlist
当不满足压缩列表条件时使用,编码方式:
4.哈希(Hash)
4.1.介绍
Redis 中的 Hash 是一个键值对集合,是一个 String 类型的 field 和 value 的映射表
-
常用命令:
hget:通过 key 值,从 hash 里取对应的 valuehset:往 hash 里,添加 key-valuehmget:一次性获取多个 key 的 value
4.2.原理
Hash 有两种 encoding:
1.ziplist
当保存的键值对数量少于512,且所有的键值对都少于64时,使用压缩列表保存;当有新的键值对加入 Hash 中时,程序先将保存了键的结点推入表尾,再将保存了值的结点推入表尾,因此保存了同一键值对的两个结点总是紧挨在一起
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
6.有序集合(zset)
6.1.介绍
在 set 的基础上添加可排序字段,该字段可以重复
常用命令:
- 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存储,此时每个结点前面是字符串后面是分数值,小的在表头,大的在表末
2.skiplist
Redis 的 skiplist 是由字典 dict 和跳表组成的
- 跳表
原始链表中查询结点15要查询15个结点,当加入一层索引:
只需查询7个结点,再加一层索引:
此时只需查询6个结点了;
跳表每层索引的结点都是前一层的一半,过程类似于二分查找;但是维持这种关系在删除和插入结点时要对整个跳表进行调整,会降低效率
- skiplist 不要求这种关系,而是每个结点随机出一个层数,把这个结点链入从第一层至随机出的层:
这样新插入一个结点不会影响其他结点的层数,只需要修改插入结点的前后指针,降低了操作的复杂度
skiplist 中:
- dict 用于记录字符串对象和分数,查询字符串对应分数
- 跳表用来根据分数查询对应字符串
好处:
- 字典的查询分值时间复杂度是 O(1),但是无序
- 跳表有序但是查询的时间复杂度是 O(logn)
- 两个结构中元素成员和分值是共享的,通过指针指向同一地址
结构如下: