前言
其实关于本文,我犹豫再三。
- 对象系统值得写一篇文章吗?从技术上来讲,当然是值。但是对于我们大部分人来说,它都是隐身的。
- 写的话,顺序放在哪里?在 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
跳跃表和字典
每种类型的对象都至少可以使用两种不同的编码。如下表:
五种常见的对象类型
对于我们而言,工作中最常用以及面试中最常被问到的五种数据类型,他们的底层分别使用了什么编码及数据结构,多种编码之间的切换条件是怎样的?
这些问题你都可以在上一篇文章中找到答案。敬请查看 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
. 会用到对象上一次被访问的时间。
同时,我们也可以手动的查看某一个对象的空转时长。空转时长=当前时间-最后一次访问时间
.
总结
这篇文章大概了讲了一下 Redis 中的对象系统设计,及对象系统可以用来做什么。
- 可以为数据类型的多种实现方式提供土壤
- 类型检查与命令多态
- 基于引用计数的内存回收
- 对象共享,节省内存
- 记录对象的空转时长,用于 LRU 等。
参考文章
《Redis 的设计与实现(第二版)》
完。
联系我
最后,欢迎关注我的个人公众号【 呼延十 】,会不定期更新很多后端工程师的学习笔记。
也欢迎直接公众号私信或者邮箱联系我,一定知无不言,言无不尽。
以上皆为个人所思所得,如有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文链接。
联系邮箱:huyanshi2580@gmail.com
更多学习笔记见个人博客或关注微信公众号 < 呼延十 >------>呼延十
来源:CSDN
作者:呼延十
链接:https://blog.csdn.net/qq_24629159/article/details/104152156