Redis系列(十)Redis对象系统

前言

其实关于本文,我犹豫再三。

  1. 对象系统值得写一篇文章吗?从技术上来讲,当然是值。但是对于我们大部分人来说,它都是隐身的。
  2. 写的话,顺序放在哪里?在 Redis 系列(九)底层数据结构之五种基础数据类型的实现中其实就提到了,那么应该在此之前先介绍它吗?

结论:想那么多屁事,写就完事了。

介绍

正如上一篇文章提到的,Redis 不是生硬的使用前面介绍过的数据结构,来实现了字符串,列表,字典等等数据结构,而是精心打造了一个对象系统。

对于 Redis 来说,所有的所谓的数据类型,本质上都是一个对象,而且同一个类型的对象,底层实现编码不一样。

Redis 对象的定义为:

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

类型

对象的 type 属性,记录了对象的类型,这个类型就是我们所熟知的 Reids 的数据类型了,比如字符串,列表,集合,有序集合,散列等。

对于 Redis 数据库中的键值对来讲,键值永远是一个字符串对象,值可以是很多种。

编码和底层数据结构

对象的 ptr 指针,指向对象的底层数据结构,而这个数据结构是什么,则由 encoding 来决定。它可以是以下任意一种:

  • REDIS_ENCODING_INT
    long 类型的整数
  • REDIS_ENCODING_ENBSTR
    embstr 编码的简单动态字符串(不知道的可以去看上一篇文章)
  • REDIS_ENCODING_RAW
    简单动态字符串
  • REDIS_ENCODING_HT
    字典
  • REDIS_ENCODING_LINKEDLIST
    双端链表
  • REDIS_ENCODING_ZIPLIST
    压缩列表
  • REDIS_ENCODING_INTSET
    整数集合
  • REDIS_ENCODING_SKIPLIST
    跳跃表和字典

每种类型的对象都至少可以使用两种不同的编码。如下表:

2020-01-16-20-48-29

五种常见的对象类型

对于我们而言,工作中最常用以及面试中最常被问到的五种数据类型,他们的底层分别使用了什么编码及数据结构,多种编码之间的切换条件是怎样的?

这些问题你都可以在上一篇文章中找到答案。敬请查看 Redis 系列(九)底层数据结构之五种基础数据类型的实现

类型检查与命令多态

如果读者熟悉 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. 会用到对象上一次被访问的时间。

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

2020-01-16-21-19-56

总结

这篇文章大概了讲了一下 Redis 中的对象系统设计,及对象系统可以用来做什么。

  • 可以为数据类型的多种实现方式提供土壤
  • 类型检查与命令多态
  • 基于引用计数的内存回收
  • 对象共享,节省内存
  • 记录对象的空转时长,用于 LRU 等。

参考文章

《Redis 的设计与实现(第二版)》


完。

联系我

最后,欢迎关注我的个人公众号【 呼延十 】,会不定期更新很多后端工程师的学习笔记。
也欢迎直接公众号私信或者邮箱联系我,一定知无不言,言无不尽。


以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客或关注微信公众号 < 呼延十 >------>呼延十

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页