图解redis对象系统

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 系列

.png)

关于本文,我是有点犹豫。对象系统值得写一篇文章吗?从技术上来讲,当然是值。但对于大部分人使用来说,它都是隐身的,你很少注意它而已。

写的话,顺序放在哪里?在42张图,真正搞懂redis数据类型的底层一文里面其实就提到了,那么自然就是本文重点讲对象系统,也可以回去复习复习。

一 回顾数据结构

简单动态字符串(SDS)

双端链表

双端链表

字典

字典

压缩列表

压缩列表

整数集合

整数集合

Redis 并没有直接使用这些数据结构来实现KV数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合这五种类型对象,然后里面每个对象都使用到了至少一种前边的数据结构。

二 思考一个问题

Redis 中的对象,大都是通过多种数据结构来实现的,为啥会这样设计呢?用一种固定的数据结构来实现,不是更加简单些吗,对吧

Redis这样设计有两个好处:

  • 可以自由改进内部编码,而对外的数据结构和命令是没有影响的,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如Redis3.2版本提供了 quicklist,它结合了 ziplistlinkedlist 两者的优点,为列表类型提供了一种更加优秀的内部编码,而对外部用户来说基本感知不到的,这一点比较像程序设计中的分层架构。
  • 多种内部编码可在不同场景下可以发挥各自的优势,从而优化对象在不同场景下的使用效率。例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换 linkedlist。 (后续文章将根据具体对象介绍);

通过这五种不同类型的对象,Redis 在执行命令之前,它会根据对象类型来判断一个对象是否可以执行完给定的命令。使用对象的另一个好处是,可以根据不同的使用场景,给对象设置多种不同的数据结构实现,来优化对象在不同场景下的使用效率;

三 对象的类型编码

Redis 使用对象来表示数据库中的键和值,每次当我们在Redis的数据库里面新创建一个键值对时候,我们至少会创建两个对象,我想你们已经猜到了。一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。

下面 SET 在数据库将创建一个新的键值对,其中键值对的键是一个包含了字符串" msg"的对象,而键值对的值是一个包含了字符串" hello world"的对象;
127.0.0.1:6379> SET msg "hello world"
OK

Redis 中的每个对象都是同一个模子刻出来的,就是 redisObject 结构来表示,该结构中保存数据相关的三个属性分别是:type、encoding、ptr;

Redis 的 redisObject 定义如下:

/* redis.h */

typedef struct redisObject {
//类型  共有5种常见的值类型  
unsigned type:4; 
unsigned notused:2;

//编码 标名底层数据结构的类型   
unsigned encoding:4;
unsigned lru:LRU_BITS;
    
//引用计数     
int refcount;  

//存储结构指针
void *ptr;  
} robj;

对象的 type 属性记录了对象的类型,这个属性的值是下面常量中的一个:
3
对于 Redis 数据库保存的键值对 来说,键总是一个字符串对象,但是 值 则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象里面的其中一种,因此:

  • 当我们称呼一个数据库键为 “字符串键” 时,我们指的是 “这个数据库键所对应的值为字符串对象”;
  • 当我们称呼一个数据库键为 “列表键” 时,我们指的是 “这个数据库键所对应的值为列表对象”;
TYPE命令的实现方式也与此类似,当我们对一个数据库键执行 TYPE 命令,命令返回结果为数据库键对应的值对象类型,并不是键对象类型:
#键为字符串对象,值为字符串对象
127.0.0.1:6379> SET msg "hello world"
OK

127.0.0.1:6379> TYPE msg
string

# 键为字符串对象,值为列表对象
127.0.0.1:6379> RPUSH numbers 1 3 5
(integer) 3
127.0.0.1:6379> TYPE numbers
list

# 键为字符串对象,值为哈希对象
127.0.0.1:6379> HMSET profile name Tome age 25 career Programmer
OK
127.0.0.1:6379> TYPE profile
hash

# 键为字符串对象,值为集合对象
127.0.0.1:6379> SADD fruits apple banana cherry
(integer) 3
127.0.0.1:6379> TYPE fruits
set

# 键为字符串对象,值为有序集合对象
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
127.0.0.1:6379> TYPE price
zset

四 编码和底层实现

Redis 的 redisObject 定义:

/* redis.h */
​
typedef struct redisObject {
//类型  共有5种常见的值类型  
unsigned type:4; 
unsigned notused:2;
​
//编码 标名底层数据结构的类型   
unsigned encoding:4;
unsigned lru:LRU_BITS;
    
//引用计数     
int refcount;  
​
//存储结构指针
void *ptr;  
} robj;
​

上面代码中对象的ptr指针指向的是对象的底层数据结构,而这些数据结构由对象的 encoding 属性决定。encoding 属性记录对象使用的编码,也即是说这个对象使用了什么数据结构是作为对象的底层实现,那么这个属性的值可以是下面列出的常量的其中一个,一一对应:
5

列出了 TYPE 命令在面对不同类型的值对象时所产生的输出:
4

每种类型的对象都至少使用两种不同的编码,下表列出了类型的对象可以使用的编码:
6
我们可使用 OBJECT ENCODING 命令可以查看一个数据库键的值对象的编码:

127.0.0.1:6379> SET msg "hello wrold"
OK

127.0.0.1:6379> OBJECT ENCODING msg
"embstr"

127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3

127.0.0.1:6379> OBJECT ENCODING numbers
"intset"

127.0.0.1:6379> SADD numbers "seven"
(integer) 1

127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"
 

下面列出了不同编码的对象所对应的 OBJECT ENCODING 命令输出:
7
通过 encoding 属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了 Redis 的灵活性和效率。

因为 Redis 根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率

举个栗子

在列表对象包含的元素比较少时,Redis 用压缩列表作为列表对象的底层来做:

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



其他类型的对象也会通过使用多种不同的编码来进行类似的优化;

Redis 的键对象都是字符串对象,而Redis的值对象主要有字符串、哈希、列表、集合、有序集合几种。

分别对应的内部编码和底层数据结构下图所示:

2

如果读者熟悉 Redis 的命令的话, 就会发现,Redis 的命令设计维度不是单一的。

比如有一类命令只能对指定的数据类型执行。比如 ZADD 及各种 ADD。

而有一些命令是可以对所有类型操作的,比如 TYPE DEL 等等。

为了确保命令可以被正确的执行,Redis 需要进行命令的检查,因为相信用户不会乱用是十分蠢的。

在所有命令被执行之前,Redis 会先检查输入的键的类型是否与命令匹配,这个检查就是用 redisObject 中的 type字段进行的:

如果匹配,则继续执行命令,如果不匹配则返回特定的错误信息。

除了进行类型检查之外,Redis 还应用了对象的类型进行命令的多态。

设想一下,列表对象可以使用 LLEN 命令来求出当前元素的个数,而在以前,列表对象的实现有可能是压缩列表,也可以是双端链表,那么对于他们而言,求出长度的方法肯定是不一样的。

正确步骤

Redis 会首先进行类型检查,之后根据当前对象的编码来决定当前命令应该调用哪个数据结构的 API 以此来实现命令的多态。

五 内存回收

学习 Java 的同志们看到这里是不是倍感亲切,仿佛看到了亲人一样。

众所周知,c 语言是没有自动化的内存管理的,但是 Redis 这么大的系统又不可能完全手动的控制内存使用,所以需要一套自动化的内存回收机制:

Redis 在自己的对象系统中,基于比较熟悉的引用计数实现了内存回收。

在 redisObject 对象中,还有一个额外的书序 refcount

  • 创建对象时,引用计数为 1.
  • 当对象被一个新程序使用时,引用计数+1.
  • 当对象被一个程序抛弃的时候,引用计数-1;
  • 当对象的引用计数为 0, 对象会被回收,它所占用的内存被释放掉。
对于这一块的具体实现我也没看,但是引用计数的原理想必各位都很清楚了,如果不清楚的话随便 google 一下JVM 内存回收基本上都会顺手讲到引用计数的。

六 对象共享

除了用于使用基于引用计数的内存回收之外,对象的引用计数的属性,还可以用来做一些对象共享的工作。

设想一下,首先你创建了一个 kye=a, value=100 的对象,过一会你又创建了个 key=b, value=100 的对象,如此循环往复。内存会无线增大,但是其实保存的只是同一个信息。

这些对象理论上来讲是完全可以进行共享的,即,首先我创建一个 value=100 的对象放在这里,每当你新创一个上面那样的对象时,我就把指针指过来就好了。

Redis 有选择性的这样子做了,当它共享之前,会先给对应的对象的引用计数+1, 之后把指针指过来。

为什么说是有选择性的呢?因为 Redis 只会缓存 0-9999 的数字字符串,如果你创建的键值对的值是这个,Redis 就会直接使用共享对象了。

为什么不多缓存一点呢?最好是把系统中所有相同的值全缓存起来,这样子最省内存了。Redis 不是最缺内存了吗?

是的,这样子当然是省内存,但是 Redis 是一个高性能的内存数据库呀,性能这一块,Redis 是不会这样做的。

想要判断两个对象的值是否相同,如果都是整数,只需要O(1)时间复杂度。那如果都是字符串,那么需要O(N)时间复杂度。 如果都是复杂对象(比如 hash), 那么可能需要 O(N2)时间复杂度

但是 Redis 为了更好的性能,放弃了缓存更加复杂的对象。

七 对象淘汰

RedisObject 还有一个属性叫做 unsigned lru:32;.

从名字我们就可以看出来它是做什么的了,它记录了当前对象最后一次被访问的时间。

这个时间会在 Redis 的内存使用满了之后,Redis 会进行对象的淘汰,其中有一种算法是 LRU. 会用到对象上一次被访问的时间。

我们也可以手动的查看某一个对象的空转时长。空转时长=当前时间-最后一次访问时间.

总结

1)Redis 并没有直接使用数据结构来实现KV数据库,而是基于这些数据结构创建了一个对象系统。这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合这五种类型,然后里面每个对象都用到了至少一种前边的数据结构。

2)可以自由改进内部编码,而对外的数据结构和命令是没有影响的,多种内部编码可在不同场景下可以发挥各自的优势,从而优化对象在不同场景下的使用效率。

3)redisObject 结构来表示,该结构中保存数据相关的三个属性分别是:type、encoding、ptr;

4)Redis 会首先进行类型检查,之后根据当前对象的编码来决定当前命令应该调用哪个数据结构的 API 以此来实现命令的多态。

5)引用计数实现了内存回收,并利用属性来对象共享,以及内存满之后LRU 算法进行对象的淘汰。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
|
2月前
|
存储 消息中间件 NoSQL
Redis 数据结构与对象
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求和数据特点来选择合适的数据结构,并合理地设计数据模型,以充分发挥 Redis 的优势。
59 8
|
1月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
45 0
|
2月前
|
JSON 缓存 NoSQL
Redis 在线查看序列化对象技术详解
Redis 在线查看序列化对象技术详解
45 2
|
4月前
|
缓存 NoSQL Linux
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
142 1
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
|
7月前
|
存储 缓存 NoSQL
深入浅出Redis(一):对象与数据结构
深入浅出Redis(一):对象与数据结构
|
4月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
4月前
|
存储 NoSQL Java
使用redis进行手机验证码的验证、每天只能发送三次验证码 (redis安装在虚拟机linux系统中)
该博客文章展示了如何在Linux虚拟机上使用Redis和Jedis客户端实现手机验证码的验证功能,包括验证码的生成、存储、验证以及限制每天发送次数的逻辑,并提供了测试结果截图。
使用redis进行手机验证码的验证、每天只能发送三次验证码 (redis安装在虚拟机linux系统中)
|
4月前
|
NoSQL 数据可视化 Linux
一文教会你如何在Linux系统中使用Docker安装Redis 、以及如何使用可视化工具连接【详细过程+图解】
这篇文章详细介绍了如何在Linux系统中使用Docker安装Redis,并提供了使用可视化工具连接Redis的步骤。内容包括安装Redis镜像、创建外部配置文件、映射文件和端口、启动和测试Redis实例、配置数据持久化存储,以及使用可视化工具连接和操作Redis数据库的过程。
|
5月前
|
NoSQL Redis 数据安全/隐私保护
macos系统中redis如何设置密码
以上步骤应该可以帮助你在macOS系统的Redis服务中设置密码,确保你的数据存储更加安全。此外,确保你定期检查Redis安全性相关的最佳实践和更新,以保持你的服务安全可靠。
392 3