Redis的常用指令、五种数据类型和底层原理,一篇带你搞懂数据如何存储在redis内存中。

一、数据结构及原理

      Redis内部使用了一个redisObject对象来表示所有的key-value。
在这里插入图片描述
      可以看到Redis内部封装了几个属性,分别是typeencodingptrptr,vm

数据类型:type

      type用来表示这个redisObject是什么属于那种类型,比如type=string,代表value存储的是一个普通的字符串。(一共有五个:string、list、set、zset、hash)

编码方式:encoding

      用来表示type的底层数据结构是用什么方式来实现的。比如Java中的List接口有三个实现类,可以由ArrayList、LinkedList、Vector来实现。

数据指针:ptr

      指向底层数据结构的指针

虚拟内存:vm

      只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该功能是默认关闭的状态。

1. RedisObject如何表示String

      字符串的encoding有三种方式:

  1. int
  2. raw
  3. embstr
1.1 int

      如果一个字符串String保存的是整数值,比如 set num 99999 那么这个整数值可以用long类型标识。那么该字符串的redisObject会把99999这个数值保存在ptr中,并将encoding设置为int。(当String对象的value全是数字,就会使用int编码)

1.2 raw

      如果字符串String保存的是一个字符串值。并且这个字符串大于44个字节5.0版本之前是39, 4.0版本之后是44),那么该RedisObject会使用一个 简单动态字符串(SDS) 来保存这个字符串值,并将RedisObject的encoding设为raw。
      因为长度字节大于39,他的字节大小就不确定,所以RedisObject和字符串的内存是分开分配的。RedisObject的内存地址和sds的内存地址是不连续的。
在这里插入图片描述

1.3 embstr

      如果字符串String保存的是一个小于44字节(4.0版本之前是39, 5.0版本之后是44) 的字符串,那么字符串使用embstr编码的方式来保存这个字符串。
      用embstr存储的话,一般字符串的存储内存很小,因此redis一次性分配redisObject和sds的内存,并且连续。
在这里插入图片描述

1.4 raw与embstr的对比
  1. embstr创建字符串对象(redisObject)的次数只需要一次,而raw是两次,因为RedisObject和sds是分开分配的。
  2. 同理,embstr调用释放内存的函数也是一次,且raw编码的字符串对象少一次。
  3. 由于embstr编码是内存连续的,raw是不连续的,所以存储速度是embstr更快一点。

2.RedisObject如何表示List

      列表对象list的编码方式encoding有两种,分别是:ziplistlinkedlist

2.1 ziplist 压缩列表

      压缩列表是为节省内存而设计的数据结构(是redis独创的)。优点是节省内存,缺点是比其他数据结构要消耗更多时间。所以尽量少使用ziplist。
      当列表长度小于521,并且所有元素的长度都小于64个字节时,使用压缩列表,否则使用LinkedList储存。

      压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,优点类似Java的ArrayList底层数组实现,能够很好地利用空间局部性提升内存访问效率。

整体数据结构如下:
在这里插入图片描述

  1. zlbytes 长度为4字节,记录整个压缩列表占用的内存字节数,在进行分配内存重新分配或者计算zllen位置时使用。
  2. zltail 长度为4字节,记录整个压缩列表起点距离起始位置的偏移字节数,可以通过他快速定位到结尾,而无需遍历链表。
  3. zllen 长度为2字节,记录了列表包含的字节数量,但是当节点数量大于65535时,节点的数量就行需要遍历才能得到。
  4. entry 长度不定,用来存储列表中的节点
  5. zlend 长度1字节,特殊字符标识结尾
节点构成

    压缩列表的节点是不定的,因为他可以根据存储的内容,动态调整其占用空间的大小。
    节点可以保存一个字节数组或者一个整数值,其中字节数组可以是:

  • 长度小于63(2^6 -1)字节的字节数组
  • 长度小于16383(2^14 -1)字节的字节数组
  • 长度小于等于4294967295(2^32 -1)字节的字节数组

整数值则可以是:

  • 4位长,介于0到12之间的无符号整数
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数
节点数据结构

在这里插入图片描述

  1. previous_entry_length 记录前一个节点长度
  2. encoding 记录节点的content属性所保存的数据类型及长度
  3. content 负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由
2.2 linkedlist 列表

      当列表少于 512且 每个元素都少于64个字节,那么就用ziplist存储。否则就用linkedlist存储
在这里插入图片描述

3.RedisObject如何表示Set

      set的encoding编码方式有两种:intsethashtable

3.1 intset

      当 集合的长度少于 512 时,并且所有元素都是整数,使用 intset存储。否则使用 hashtable
在这里插入图片描述

3.2 hashtable

在这里插入图片描述

      hashtable编码的底层实现是字典,字典的每个键是字符串对象,只不过值都是空(NULL)。

4.RedisObject如何表示ZSet

      zset的encoding编码方式有两种,分别是:ziplistskiplist

4.1 ziplist

      当 zset的长度少于128,并且所有元素的长度都少于64字节时,用ziplist存储。每个节点,前面是字符串,后面是分数值。分数值小的靠近表头,大的靠近表位。
在这里插入图片描述

4.2 skiplist

      redis的skiplist是由字典dict跳表组成。

zset的结构体定义如下:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;

跳表

      比如说,在原始的链表中如果要查询结点15,那么一共就需要从结点1开始,到结点15,一共查询15个结点才能找到结点15。(跳表必须是排好序的)

      那么如果我们额外加一层索引层呢?如下图,我们从下图的 第一级索引层 开始查找,那么要找到结点15,只需要找 结点 1-4-7-10-14-14-15,只需要查询七个结点即可。这就是跳表,可以大大地方便查询。
在这里插入图片描述

      当然,那还能不能加第二层、第三层索引层呢?当然可以。如下图,我们从第二级索引层开始,查询结点15。那么查询顺序为 1-7-14-14-14-15,一共需要查询6个结点,查询效率又提高了。
在这里插入图片描述
      这种通过对链表加多级索引的机构,就是跳表了。跳表 每层索引层的结点数目都是其前一层的一半。 因为这样的话,查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这样的话,会存在一个问题,就是 当新插入一个结点的时候,会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。 如果硬要维持这种2:1的关系话,那么插入和删除结点后,都必须要对整个跳表重新调整,这会降低效率。

skiplist

      因此 skiplist 为了解决这个问题并不要求 相邻上下链表结点个数必须按照2:1的关系。而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个skiplist的过程:

在这里插入图片描述
      每一节点的层数都是随机出来的,而且新插入的一个节点不受其他层节点的影响。因此,插入操作只需要修改插入节点前后的指针,而不是对节点进行调整。这就降低了插入操作的复杂度。

skiplist的字典dict 和 跳表
  • dict用于记录字符串对象和分数,即查询字符串对象对应的分数。
  • 跳表则用来根据分数查取对应的字符串。

那为什么skiplist编码要同时用字典和跳表来实现

  • 字典查询分值的时间复杂度是O(1)。但是无序。
  • 跳表的优点是有序,但是查询的时间复杂度为O(logn)。
  • 虽然采用两个结构,但是集合的元素成员和分值是共享的,两种结构都通过指针指向同一地址,所以不会存在内存浪费。
redis跳表的数据结构
  1. 包含一个头结点header和尾结点tail
  2. length表示的节点数。
  3. level表示skiplist的总层数,即所有的节点层数的最大值。

在这里插入图片描述

在这里插入图片描述

5.RedisObject如何表示hash

      hash中的encoding有两种:ziplisthashtable

5.1 ziplist

      当哈希对象保存的键值对数量少于512,且所有键值对的长度都少于64字节时,使用压缩列表保存
      在压缩列表中,每当有新的键值对要加入哈希对象时,程序会先将保存了 键的列表节点 推入到压缩列表的末尾,然后再将保存了值的压缩列表节点推入到压缩列表结尾,因为:保存了同一键值对的两个节点总是紧挨在一起

5.2 hashtable

      若哈希对象保存的键值对个数大于512,并且其中有键值对大于64个字节,就使用hashtable保存
在这里插入图片描述

二、常用指令

在这里插入图片描述

String的常用指令

set name ljh
get name
getrange name 0 -1         截取字符串,0对应start,1对应end
getset name new_ljh       设置值,返回旧值
mset key1 value1 key2 value2            批量设置
mget key1 key2            批量获取
setnx key value           不存在就插入(not exists)
setex key time value      过期时间(expire)
setrange key index value  从index开始替换value
incr age        执行+1
incrby age 10   执行+10
decr age        执行-1
decrby age 10   执行-10
incrbyfloat     增减浮点数
append          追加
strlen          长度
getbit/setbit/bitcount/bitop    位操作

list的常用指令

lpush mylist a b c  左插入
rpush mylist x y z  右插入
lrange mylist 0 -1  数据集合
lpop mylist  弹出元素
rpop mylist  弹出元素
llen mylist  长度
lrem mylist count value  根据值来删除(count表示要删除多少个值为value的元素)
lindex mylist 2          指定索引的值
lset mylist 2 n          索引设值
ltrim mylist 1 2         截取指定的元素集合
linsert mylist before 1 value   在值为1前面插入value值
linsert mylist after 1 value    在值为1的后面插入value值
rpoplpush list list2     将list的最后一个元素移到list2中

set的常用指令

sadd myset e            添加一个元素
smembers myset       数据集合
srem myset e        删除
sismember myset e 判断元素是否在集合中
scard key_name       获取set集合中元素个数
sdiff | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
srandmember          随机获取集合中的元素
spop                 从集合中弹出一个元素

zset的常用指令

zadd zset 1 one          添加一个元素(1表示score,用作排序使用)
zadd zset 2 two            
zadd zset 3 three
zincrby zset 1 one              分数+1
zscore zset two                  获取分数
 zrange zset 0 -1                   获取全部的值
zrange zset 0 -1 withscores     获取全部值并附带分数
zrangebyscore zset 10 25 withscores 分数在某个范围的值
zrangebyscore zset 10 25 withscores limit 1 2 分页
Zrevrangebyscore zset 10 25 withscores  指定范围的值从大到小排序
zcard zset  元素数量
Zcount zset 获得指定分数范围内的元素个数
Zrem zset one two        删除一个或多个元素
Zremrangebyrank zset 0 1  按照排名范围删除元素
Zremrangebyscore zset 0 1 按照分数范围删除元素

hash的常用指令

hset myhash name ljh      添加一个键值对
hget myhash name          取出值
hmset myhash name ljh age 20 note "i am notes"      批量键值对
hmget myhash name age note   
hgetall myhash               获取所有的键值对
hexists myhash name          是否存在
hsetnx myhash score 100      不存在的话创建,存在的话修改
hincrby myhash id 2          增加2
hdel myhash name             删除
hkeys myhash                 只取key
hvals myhash                 只取value
hlen myhash                  长度

三、应用场景

String的使用场景

  1. 缓存功能:String字符串是最常用的数据类型,不仅仅是redis,各个语言都是最基本类型,因此,利用redis作为缓存,配合其它数据库作为存储层,利用redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  2. 计数器:许多系统都会使用redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  3. 统计多单位的数量:eg,uid:gongming count:0 根据不同的uid更新count数量。
  4. 共享用户session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存cookie,这两种方式做有一定弊端,1)每次都重新登录效率低下 2)cookie保存在客户端,有安全隐患。这时可以利用redis将用户的session集中管理,在这种模式只需要保证redis的高可用,每次用户session的更新和获取都可以快速完成。大大提高效率。

List的使用场景

  1. 消息队列:reids的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
  2. 文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

Set的使用场景

  1. 标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。
  2. 共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。
  3. 统计网站的独立IP。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。

ZSet的使用场景

  1. 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  2. 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

Hash的使用场景

  1. 由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。
  2. 经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值