Redis的对象系统


1.概述

Redis的对象系统包括string、hash、list、set、zset这五种类型的对象,每种对象都用到了至少一种我们前面介绍的数据结构。

使用对象的好处:

  1. Redis可以在执行命令前,可以根据对象的类型来判断一个对象是否可以执行给定的命令
  2. 可以针对不同的使用场景,为对象设置不同的数据结构实现,从而优化对象在不同使用场景下的使用效率

除此之外,对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当条件下,通过让多个数据库键共享同一个对象来节约内存。

最后,Redis的对象记录了访问时间信息,在服务器启用了maxmemory功能的情况下,根据访问时间计算出空转时长较大的那些键可能会优先被服务器删除。

本文接下来将逐一介绍以上提到的Redis对象系统的各个特性。

2.对象类型与编码

Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)

Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr:

typedef struct redisObiect {
	// 类型
	unsigned type:4;
	// 编码
	unsigned encoding:4;
	// 指向底层数据结构的指针
	void *ptr;
    
    // ....
} robj;

2.1类型

对象的type属性记录了对象的类型,这个属性的值可以是下表中的一个:

类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

对于redis数据库保存的键值对来说,键总是一个字符串对象,值可以是五种数据类型中的一种。

2.2编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。

encoding属性记录了对象所使用的编码,也就是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以是下表列出的常量中的一个。

编码常量 编码所对应的底层数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典

通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为对象设置不同的编码,从而优化对象在某一场景下的效率。

举个例子,在列表对象包含的元素比较少时,redis使用压缩列表作为列表的底层实现:

  • 因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表可以更快的被载入内存
  • 随着列表对象包含的元素越来越多,使用压缩列表保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上

3.string对象

3.1简介

存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,string 类型是二进制安全的,可以包含任何数据,比如图片或者序列化的对象

存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息

存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用

Redis 所有操作都是原子性的,采用单线程机制,命令是单个顺序执行,无需考虑并发带来影响,原子性就是有一个失败则都失败

字符串对象可以是 int、raw、embstr 三种实现方式


3.2操作

指令操作:

  • 数据操作:

    set key value			#添加/修改数据添加/修改数据
    del key					#删除数据
    setnx key value			#判定性添加数据,键值为空则设添加
    mset k1 v1 k2 v2...		#添加/修改多个数据,m:Multiple
    append key value		#追加信息到原始信息后部(如果原始信息存在就追加,否则新建)
  • 查询操作

    get key					#获取数据,如果不存在,返回空(nil)
    mget key1 key2...		#获取多个数据
    strlen key				#获取数据字符个数(字符串长度)
  • 设置数值数据增加/减少指定范围的值

    incr key					#key++
    incrby key increment		#key+increment
    incrbyfloat key increment	#对小数操作
    decr key					#key--
    decrby key increment		#key-increment
  • 设置数据具有指定的生命周期

    setex key seconds value  		#设置key-value存活时间,seconds单位是秒
    psetex key milliseconds value	#毫秒级

注意事项:

  1. 数据操作不成功的反馈与数据正常操作之间的差异

    • 表示运行结果是否成功

      • (integer) 0 → false ,失败

      • (integer) 1 → true,成功

    • 表示运行结果值

      • (integer) 3 → 3 个

      • (integer) 1 → 1 个

  2. 数据未获取到时,对应的数据为(nil),等同于null

  3. 数据最大存储量:512MB

  4. string 在 Redis 内部存储默认就是一个字符串,当遇到增减类操作 incr,decr 时会转成数值型进行计算

  5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了Redis 数值上限范围,将报错
    9223372036854775807(java 中 Long 型数据最大值,Long.MAX_VALUE)

  6. Redis 可用于控制数据库表主键 ID,为数据库表主键提供生成策略,保障数据库表的主键唯一性

单数据和多数据的选择:

  • 单数据执行 3 条指令的过程:3 次发送 + 3 次处理 + 3 次返回
  • 多数据执行 1 条指令的过程:1 次发送 + 3 次处理 + 1 次返回(发送和返回的事件略高于单数据)

3.3实现

字符串对象的编码可以是 int、raw、embstr 三种

  • int:字符串对象保存的是整数值,并且整数值可以用 long 类型来表示,那么对象会将整数值保存在字符串对象结构的 ptr 属性面(将 void * 转换成 long),并将字符串对象的编码设置为 int(浮点数用另外两种方式)

  • raw:字符串对象保存的是一个字符串值,并且值的长度大于 39 字节,那么对象将使用简单动态字符串(SDS)来保存该值,并将对象的编码设置为 raw

  • embstr:字符串对象保存的是一个字符串值,并且值的长度小于等于 39 字节,那么对象将使用 embstr 编码的方式来保存这个字符串值,并将对象的编码设置为 embstr

    上图所示,embstr 与 raw 都使用了 redisObject 和 sdshdr 来表示字符串对象,但是 raw 需要调用两次内存分配函数分别创建两种结构,embstr 只需要一次内存分配来分配一块连续的空间

embstr 是用于保存短字符串的一种编码方式,对比 raw 的优点:

  • 内存分配次数从两次降低为一次,同样释放内存的次数也从两次变为一次
  • embstr 编码的字符串对象的数据都保存在同一块连续内存,所以比 raw 编码能够更好地利用缓存优势(局部性原理)

int 和 embstr 编码的字符串对象在条件满足的情况下,会被转换为 raw 编码的字符串对象:

  • int 编码的整数值,执行 APPEND 命令追加一个字符串值,先将整数值转为字符串然后追加,最后得到一个 raw 编码的对象
  • Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以 embstr 对象实际上是只读的,执行修改命令会将对象的编码从 embstr 转换成 raw,操作完成后得到一个 raw 编码的对象

某些情况下,程序会将字符串对象里面的字符串值转换回浮点数值,执行某些操作后再将浮点数值转换回字符串值:

redis> SET pi 3.14 
OK 
redis> OBJECT ENCODING pi
"embstr" 
redis> INCRBYFLOAT pi 2.0 # 转为浮点数执行增加的操作
"5. 14" 
redis> OBJECT ENCODING pi 
"embstr" 

3.4应用

主页高频访问信息显示控制,例如新浪微博大 V 主页显示粉丝数与微博数量

  • 在 Redis 中为大 V 用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略

    set user:id:3506728370:fans 12210947
    set user:id:3506728370:blogs 6164
    set user:id:3506728370:focuses 83
  • 使用 JSON 格式保存数据

    user:id:3506728370 → {"fans":12210947,"blogs":6164,"focuses":83}
  • key的设置约定:表名 : 主键名 : 主键值 : 字段名

    表名 主键名 主键值 字段名
    order id 29437595 name
    equip id 390472345 type
    news id 202004150 title

4.hash

4.1简介

数据存储需求:对一系列存储的数据进行编组,方便管理,典型应用存储对象信息

数据存储结构:一个存储空间保存多个键值对数据

hash 类型:底层使用哈希表结构实现数据存储

Redis 中的 hash 类似于 Java 中的 Map<String, Map<Object,object>>,左边是 key,右边是值,中间叫 field 字段,本质上 hash 存了一个 key-value 的存储空间

hash 是指的一个数据类型,并不是一个数据

  • 如果 field 数量较少,存储结构优化为压缩列表结构(有序)
  • 如果 field 数量较多,存储结构使用 HashMap 结构(无序)

4.2操作

指令操作:

  • 数据操作

    hset key field value		#添加/修改数据
    hdel key field1 [field2]	#删除数据,[]代表可选
    hsetnx key field value		#设置field的值,如果该field存在则不做任何操作
    hmset key f1 v1 f2 v2...	#添加/修改多个数据
  • 查询操作

    hget key field				#获取指定field对应数据
    hgetall key					#获取指定key所有数据
    hmget key field1 field2...	#获取多个数据
    hexists key field			#获取哈希表中是否存在指定的字段
    hlen key					#获取哈希表中字段的数量
  • 获取哈希表中所有的字段名或字段值

    hkeys key					#获取所有的field	
    hvals key					#获取所有的value
  • 设置指定字段的数值数据增加指定范围的值

    hincrby key field increment		#指定字段的数值数据增加指定的值,increment为负数则减少
    hincrbyfloat key field increment#操作小数

注意事项

  1. hash 类型中 value 只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil)
  2. 每个 hash 可以存储 2^32 - 1 个键值对
  3. hash 类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但 hash 设计初衷不是为了存储大量对象而设计的,不可滥用,不可将 hash 作为对象列表使用
  4. hgetall 操作可以获取全部属性,如果内部 field 过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈

4.3实现

哈希对象的内部编码有两种:ziplist(压缩列表)、hashtable(哈希表、字典)

  • 压缩列表实现哈希对象:同一键值对的节点总是挨在一起,保存键的节点在前,保存值的节点在后

  • 字典实现哈希对象:字典的每一个键都是一个字符串对象,每个值也是

当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型,具体需要满足两个条件:

  • 当键值对数量小于 hash-max-ziplist-entries 配置(默认 512 个)
  • 所有键和值的长度都小于 hash-max-ziplist-value 配置(默认 64 字节)

以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行

ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)


4.4应用

user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83}

对于以上数据,使用单条去存的话,存的条数会很多。但如果用 json 格式,存一条数据就够了。

假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以

可以实现购物车的功能,key 对应着每个用户,存储空间存储购物车的信息


5.list

5.1简介

数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分

数据存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序,允许重复元素

list 类型:保存多个数据,底层使用双向链表存储结构实现,类似于 LinkedList

如果两端都能存取数据的话,这就是双端队列,如果只能从一端进一端出,这个模型叫栈


5.2操作

指令操作:

  • 数据操作

    lpush key value1 [value2]...#从左边添加/修改数据(表头)
    rpush key value1 [value2]...#从右边添加/修改数据(表尾)
    lpop key					#从左边获取并移除第一个数据,类似于出栈/出队
    rpop key					#从右边获取并移除第一个数据
    lrem key count value		#删除指定数据,count=2删除2个,该value可能有多个(重复数据)
  • 查询操作

    lrange key start stop		#从左边遍历数据并指定开始和结束索引,0是第一个索引,-1是终索引
    lindex key index			#获取指定索引数据,没有则为nil,没有索引越界
    llen key					#list中数据长度/个数
  • 规定时间内获取并移除数据

    b							#代表阻塞
    blpop key1 [key2] timeout	#在指定时间内获取指定key(可以多个)的数据,超时则为(nil)
    							#可以从其他客户端写数据,当前客户端阻塞读取数据
    brpop key1 [key2] timeout	#从右边操作
  • 复制操作

    brpoplpush source destination timeout	#从source获取数据放入destination,假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长

注意事项

  1. list 中保存的数据都是 string 类型的,数据总容量是有限的,最多 2^32 - 1 个元素(4294967295)
  2. list 具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈
  3. 获取全部数据操作结束索引设置为 -1
  4. list 可以对数据进行分页操作,通常第一页的信息来自于 list,第 2 页及更多的信息通过数据库的形式加载

5.3实现

在 Redis3.2 版本以前列表对象的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表)

  • 压缩列表实现的列表对象:PUSH 1、three、5 三个元素

  • 链表实现的列表对象:为了简化字符串对象的表示,使用了 StringObject 的结构,底层其实是 sdshdr 结构

列表中存储的数据量比较小的时候,列表就会使用一块连续的内存存储,采用压缩列表的方式实现的条件:

  • 列表对象保存的所有字符串元素的长度都小于 64 字节
  • 列表对象保存的元素数量小于 512 个

以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行

在 Redis3.2 版本 以后对列表数据结构进行了改造,使用 quicklist(快速列表)代替了 linkedlist,quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,既满足了快速的插入删除性能,又不会出现太大的空间冗余


5.4应用

企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出?

  • 依赖 list 的数据具有顺序的特征对信息进行管理,右进左查或者左近左查
  • 使用队列模型解决多路信息汇总合并的问题
  • 使用栈模型解决最新消息的问题

微信文章订阅公众号:

  • 比如订阅了两个公众号,它们发布了两篇文章,文章 ID 分别为 666 和 888,可以通过执行 LPUSH key 666 888 命令推送给我

6.set

6.1简介

数据存储需求:存储大量的数据,在查询方面提供更高的效率

数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询

set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是 O(1),并且值是不允许重复且无序的


6.2操作

指令操作:

  • 数据操作

    sadd key member1 [member2]	#添加数据
    srem key member1 [member2]	#删除数据
  • 查询操作

    smembers key				#获取全部数据
    scard key					#获取集合数据总量
    sismember key member		#判断集合中是否包含指定数据
  • 随机操作

    spop key [count]			#随机获取集中的某个数据并将该数据移除集合
    srandmember key [count]		#随机获取集合中指定(数量)的数据
  • 集合的交、并、差

    sinter key1 [key2...]  					#两个集合的交集,不存在为(empty list or set)
    sunion key1 [key2...]  					#两个集合的并集
    sdiff key1 [key2...]					#两个集合的差集
    
    sinterstore destination key1 [key2...]	#两个集合的交集并存储到指定集合中
    sunionstore destination key1 [key2...]	#两个集合的并集并存储到指定集合中
    sdiffstore destination key1 [key2...]	#两个集合的差集并存储到指定集合中
  • 复制

    smove source destination member			#将指定数据从原始集合中移动到目标集合中

注意事项

  1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份
  2. set 虽然与 hash 的存储结构相同,但是无法启用 hash 中存储值的空间

6.3实现

集合对象的内部编码有两种:intset(整数集合)、hashtable(哈希表、字典)

  • 整数集合实现的集合对象:

  • 字典实现的集合对象:键值对的值为 NULL

当集合对象可以同时满足以下两个条件时,对象使用 intset 编码:

  • 集合中的元素都是整数值
  • 集合中的元素数量小于 set-maxintset-entries配置(默认 512 个)

以上两个条件的上限值是可以通过配置文件修改的


6.4应用

应用场景:

  1. 黑名单:资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密。

    注意:爬虫不一定做摧毁性的工作,有些小型网站需要爬虫为其带来一些流量。

  2. 白名单:对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证

  3. 随机操作可以实现抽奖功能

  4. 集合的交并补可以实现微博共同关注的查看,可以根据共同关注或者共同喜欢推荐相关内容


7.zset

7.1简介

数据存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式

数据存储结构:新的存储模型,可以保存可排序的数据


7.2操作

指令操作:

  • 数据操作

    zadd key score1 member1 [score2 member2]	#添加数据
    zrem key member [member ...]				#删除数据
    zremrangebyrank key start stop 				#删除指定索引范围的数据
    zremrangebyscore key min max				#删除指定分数区间内的数据
    zscore key member							#获取指定值的分数
    zincrby key increment member				#指定值的分数增加increment
  • 查询操作

    zrange key start stop [WITHSCORES]		#获取指定范围的数据,升序,WITHSCORES 代表显示分数
    zrevrange key start stop [WITHSCORES]	#获取指定范围的数据,降序
    
    zrangebyscore key min max [WITHSCORES] [LIMIT offset count]	#按条件获取数据,从小到大
    zrevrangebyscore key max min [WITHSCORES] [...]				#从大到小
    
    zcard key										#获取集合数据的总量
    zcount key min max								#获取指定分数区间内的数据总量
    zrank key member								#获取数据对应的索引(排名)升序
    zrevrank key member								#获取数据对应的索引(排名)降序
    • min 与 max 用于限定搜索查询的条件
    • start 与 stop 用于限定查询范围,作用于索引,表示开始和结束索引
    • offset 与 count 用于限定查询范围,作用于查询结果,表示开始位置和数据总量
  • 集合的交、并操作

    zinterstore destination numkeys key [key ...]	#两个集合的交集并存储到指定集合中
    zunionstore destination numkeys key [key ...]	#两个集合的并集并存储到指定集合中

注意事项:

  1. score 保存的数据存储空间是 64 位,如果是整数范围是 -9007199254740992~9007199254740992
  2. score 保存的数据也可以是一个双精度的 double 值,基于双精度浮点数的特征可能会丢失精度,慎重使用
  3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score 值将被反复覆盖,保留最后一次修改的结果

7.3实现

有序集合对象的内部编码有两种:ziplist(压缩列表)和 skiplist(跳跃表)

  • 压缩列表实现有序集合对象:ziplist 本身是有序、不可重复的,符合有序集合的特性

  • 跳跃表实现有序集合对象:底层是 zset 结构,zset 同时包含字典和跳跃表的结构,图示字典和跳跃表中重复展示了各个元素的成员和分值,但实际上两者会通过指针来共享相同元素的成员和分值,不会产生空间浪费

    typedef struct zset {
        zskiplist *zsl;
        dict *dict;
    } zset;

    使用字典加跳跃表的优势:

    • 字典为有序集合创建了一个从成员到分值的映射,用 O(1) 复杂度查找给定成员的分值
    • 排序操作使用跳跃表完成,节省每次重新排序带来的时间成本和空间成本

    使用 ziplist 格式存储需要满足以下两个条件:

    • 有序集合保存的元素个数要小于 128 个;
    • 有序集合保存的所有元素大小都小于 64 字节

    当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn)

    为什么用跳表而不用平衡树?

    • 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找
    • 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针

    7.4应用

    • 排行榜
    • 对于基于时间线限定的任务处理,将处理时间记录为 score 值,利用排序功能区分处理的先后顺序
    • 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用 score 记录权重

文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录