栏目说明:“Redis为什么快”记录我对Redis原理的学习过程,以及一些心得体会。也许没有那么”深入“,但一定通俗易懂。个人理解仅供参考。
作者:Kingston 8GB
微信公众号:百香先生
1 Redis的用途
Redis是开发中非常常用的中间件,在后端开发岗位的面试中也很常被问到。它是一种非关系型数据库(NoSQL),存储的是键值对数据——一个Redis命令即可说明。
127.0.0.1:6379> SET key:name misterpassion
OK
这样就把(“key:name”, “misterpassion”)这样一个键值对存进Redis了。
让我们先来看看Redis官网开篇的介绍:
机翻之后更适合中国人体质:
根据官网的描述,我们可以大致概括出以下几点:
- Redis是一种数据库,可以类比MySQL,不同的是Redis的数据保存在内存中,访问速度很快;而MySQL的数据保存在磁盘上;因为主内存的访问速度远远高于磁盘,这样Redis就可以直接从内存中读取和写入数据,无需进行磁盘 I/O 操作。
- Redis除了用作数据库以存储数据,还可以作为MySQL的缓存,以及消息中间件、流引擎;如此多的应用场景或许归因于Redis强大的性能;
- Redis根据不同的数据存储需要,设计了不同的存储数据结构,或许也是为了提升性能;但MySQL只是用关系表进行存储;
- 尽管Redis的数据存在于内存,但似乎有办法把数据定期保存到磁盘上,这样即使断电数据也不容易丢失;
- Redis支持分区集群部署,节点之间的数据复制是异步的,保证了在分布式Redis环境下,一个Redis挂掉了还能保证服务基本可用。
在这段介绍中,官方并没有明确提及Redis“快”,但又好像处处都在说“快”。可以看出,Redis为了提升性能,在细节上做了很多考量。
事实上,Redis在效率上碾压MySQL,然而,要想回答“Redis为什么快”这个问题,这些还远远不够,本文先从内存存储说起。
2 Redis数据在内存中的存储
让我们从头开始介绍。计算机组成告诉我们,若想提升访问速度,可以采用一种叫“缓存”的机制,英文名“Cache”。
根据内存金字塔模型,缓存的作用是通过采用更快的(靠近塔尖的)物理存储器,保存较常被访问的数据。这种多层存储的机制可以达到性能与存储容量的平衡,提升整体的访问速度。
缓存的存在,和程序的局部性原理密不可分,让GPT3.5带我们回顾一下吧。
时间局部性(Temporal Locality):时间局部性指的是程序倾向于访问最近使用过的数据项的趋势。换句话说,如果程序一次访问了某个内存位置,那么在不久的将来,它很可能会再次访问相同的位置。
这种局部性原理是基于对程序中循环、迭代和函数调用等常见模式的观察得出的。通过利用时间局部性,计算机系统可以将最近使用的数据项保留在高速缓存或寄存器中,以提供更快的访问速度。
空间局部性(Spatial Locality):空间局部性指的是程序在某个时间点上访问的内存位置很可能与其附近的内存位置有关。当程序访问某个内存位置时,它很可能会接下来访问相邻的内存位置。
这种局部性原理是基于对程序中数组、结构体和循环遍历等常见访问模式的观察得出的。通过利用空间局部性,计算机系统可以预取附近的内存位置,以提高数据访问的效率。
举个例子,“页表”是一种保存于内存中的数据结构,保存进程的虚拟地址与物理内存地址的页号映射,这个映射过程很追求效率;而根据程序的局部性原理,下一次大概率会很快地访问内存中临近的数据,因此我们可以把刚访问到的页表项放入“快表”(英文名TLB,是CPU Cache的组成部分),构建缓存以提升下次访问的速度。
Redis在MySQL上干的也是类似的事情,只不过是在用内存做磁盘的缓存。
至此,我们明白了为什么把数据用Redis缓存在内存上能提升访问速度。然而,不同于MySQL直观存在于磁盘上的数据文件(或数据+索引文件),Redis的数据文件我们看不见,那么它是如何组织数据的呢?
Redis是用C语言写的,数据库的结构体定义如下:
typedef struct redisDb {
int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; //存储数据库所有的key-value
dict *expires; //存储key的过期时间
dict *blocking_keys;//blpop 存储阻塞key和客户端对象
dict *ready_keys;//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
dict *watched_keys;//存储watch监控的的key和客户端对象
} redisDb;
(图源:知乎@思碎)
可见在一个Redis数据库中(一个Redis服务器默认有16个数据库),dict字典主要存储所有的键值对。
dict字典即为一个HASHTABLE数据结构(dictht),每当插入一个键值对(dictEntry),就会根据哈希函数计算出在哈希表(dictEntry*数组)中的存放位置。特别地,如果多个键值对映射到了哈希表的同一位置,则通过头插法维护一个链表存储它们。可以对照下图看一下。
(图源:微信公众号@牛牛码特)
再补充几句,dictht的字段含义如下:
- table:指向哈希表(dictEntry数组),是HASHTABLE内键值对的实际存放位置。
- size:哈希表的大小,即dictEntry*数组的长度。
- sizemask:哈希掩码,默认值为(size-1),和哈希函数一起用于哈希位置的计算。
- used:哈希表里实际存了多少个键值对,当超过/低于一定值,会触发哈希表的扩容/缩容。
这种结构的好处是可以快速查找到某个key对应的值,时间复杂度为O(1)。
这也是Redis查询,即get类命令那么快的原因之一。
下期:【Redis为什么快】02 内存存储(中)