1.Redis特性
1)速度快:数据存放在内存上、基于C语言实现、单线程架构预防多线程竞争问题;
2)基于键值对的数据结构,支持5中数据结构:字符串(string)、列表(list)、哈希(hash)、集合(set)、有序集合,同时在字符串基础上演变出位图(Bitmaps)和HyperLogLog;
3)丰富的功能:键过期、发布订阅、Lua脚本、事务、流水线;
4)持久化:RDB和AOF两种策略将内存数据保存到硬盘;
5)主从复制
2.基础指令
-
keys *
:查看所有键,需要遍历数据库O(n); -
dbsize
:查看数据库键总数,直接获取内置的键总数变量O(1); -
exists key
:查看键是否存在,存在返回1,否则0; -
del key [key ..]
:删除键,返回删除成功的个数; -
expire key seconds
:对键添加过期时间,超期自动删除键; -
ttl key
:返回键的剩余过期时间v,若v大于0,则代表键的过期时间,若v等于-1,则代表没有设置过期时间,若等于-2,则表示键不存在; -
type key
:返回键的数据结构类型,若键不存在返回none; -
object encoding
:查询内部编码;
3.单线程架构
Redis使用单线程架构和I/O多路复用模型来实现高性能的内存数据库。为什么单线程还能这么快?
a)纯内存访问;
b)非阻塞I/O,使用epoll作为I/O多路复用技术的实现,再加上Redis自身的时间处理模型将epoll中的连接、读写、关闭都转换为事件;
c)单线程避免了线程切换和竞态产生的消耗;
4.数据结构的运用及其实现
请参考:"Redis5种数据结构的运用及实现"(https://www.jianshu.com/p/7d207d057d46 )
5.对象
基于上述数据结构构建了一个对象系统,字符串对象、列表对象、哈希对象、集合对象和有序集合对象。使用基于引用计数的内存回收机制(不使用自动释放)和对象共享机制(多库键共享对象)。
Redis中每个对象都由一个redisObject结构表示,有三个属性和保存数据有关,分别是:type属性、encoding属性和ptr属性,一个与内存回收有关的属性refcount,一个lru属性记录对象最后一次被命令程序访问的时间;
- 类型(type):字符串对象、列表对象、哈希对象、集合对象和有序集合对象的其中一个,键总是字符串对象;
- 编码(encoding):对象的ptr指针指向对象的底层实现数据结构,由encoding属性决定,主要包括int、embstr、raw、ht、linkedlist、ziplist、intset、skiplist;
- 引用计数(refcount):由于C不具备自动内存回收功能,Redis自己构建了一个引用计数技术来实现内存回收机制,创建对象时,引用计数被初始化为1,当对象被一个新程序使用时,计数值加以,不再使用时减一,当计数值变为0时,对象所占用的内存会被释放;同时引用计数属性还有对象共享的作用,Redis在初始化时创建了一万个字符串对象,包含了从0到9999的所有整数,当需要使用这些对象时,服务器会直接使用这些共享对象,并且引用计数加一,而不是新创建对象;
字符串对象
编码可以是int、embstr、raw:
- int:如果一个字符串对象保存的是可由long表示的整数值,会将该整数值保存在ptr属性里面,并将编码设置为int。若修改后不再是整数值而是一个字符串,则转为raw;
- raw:如果字符串对象保存的是一个长度>32字节的字符串值,使用SDS保存该字符串,并将编码设置为raw;
- embstr(只读):如果字符串对象保存的是一个长度<=32字节的字符串值,使用embstr编码方式来保存;embstr专门用于保存短字符串的优化编码方式,raw和embstr都使用redisObject结构和sdshdr结构来表示字符串对象,但raw会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr则通过调用一次内存分配函数来分配一块连续的空间,因此在创建和释放时embstr都只需调用一次内存分配/释放,且embstr保存在连续的内存中,更好的利用了缓存。embstr字符串对象无法修改,没有任何修改程序,对他的修改实际上是转成raw后进行修改;
列表对象
编码可以是ziplist和linkedlist,当列表对象保存的元素个数不小于512个或者有元素的长度大于64字节则使用linkedlist(上限可改):
- ziplist:使用压缩列表作为底层实现,每个压缩列表节点节点保存一个列表元素;
- linkedlist:使用双端链表作为底层实现,每个双端链表节点保存了一个字符串对象;
哈希对象
编码可以是ziplist、hashtable,当键值字符串长度不小于64字节或者键值对数量不小于512个则使用hashtable编码。
- ziplist编码的哈希对象使用压缩列表作为底层实现,当有新的键值加入时,先在表尾推入键,然后推入值,键值总是紧挨在一起;
- hashtable编码的哈希对象使用字典作为底层实现,字典的键值均为字符串对象;
集合对象
编码可以是intset、hashtable,当集合对象保存的都是整数值,且集合元素不超过512个时,使用intset编码:
- intset编码的集合对象使用整数集合作为底层实现,所有元素都被保存在整数集合里面;
- hashtable编码的集合对象使用字典作为底层实现,每个键都是一个字符串对象,代表一个集合元素,而值则全部被设置为NULL;
有序集合对象
编码可以是ziplist、skiplist,若元素数量小于128个并且所有元素长度都小于64字节,则使用ziplist编码:
- ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素成员,第二节点保存元素分值,各个元素按分值大小从小到大排序;
- skiplist编码的有序集合对象使用zset作为底层实现,一个zset同时包含一个字典和一个跳跃表,跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点保存一个集合元素;字典则为有序集合创建了一个成员到分值的映射,键值对保存的是集合元素的成员(键)和分值(值),通过字典可以O(1)的查找给定成员的分值。跳跃表和字典会通过指针共享相同元素的成员和分值,也就不会因此浪费内存;
6.数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,初始化服务器时,根据结构中的dbnum属性确定创建多少个数据库(默认16)。每个Redis客户端都有自己的目标数据库(默认0),由客户端状态redisClient结构的db属性记录客户端当前的目标数据库,可通过SELECT命令来切换目标数据库。
键空间
Redis中每个数据库由一个redis.h/redisDb结构表示,其中的dict字典保存了数据库中的所有键值对,称这个字典为键空间,键空间的键也是数据库的键,是一个字符串对象,键空间的值即数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象、有序集合对象。
读写键空间的维护操作
- 读取一个键后(读写都要读),会根据键是否存在来更新服务器键空间的命中次数(keyspace_hits)和不命中次数(keyspace_misses);
- 读取一个键后,会更新键的LRU(最后一次使用)时间,用于计算键的闲置时间;
- 若服务器在读取一个键时发现键已过期,则先删除这个过期键;
- 若服务器使用WATCH命令监视了某个键,那么对被监视键进行修改后,会将这个键标记为脏,从而让事务程序注意到这个键已经被修改;
- 服务器每修改一个键,会将脏键计数器值加1,该计数器会触发服务器的持久化以及复制操作;
- 若服务器开启了数据库通知功能,那么对键进行修改了之后,服务器按配置发送相应的数据库通知;
键过期
通过EXPIRE
命令,客户端可以以秒或毫秒精度为数据库中的某个键设置过期时间,过期时间是一个UNIX时间戳;TTL
命令接受一个带生存时间或过期时间的键,返回这个键的剩余生存时间。
在redisDb结构的expires字典保存了数据库中所有键的过期时间,称为过期字典。过期字典的键是一个指针,指向键空间中的某个键对象;过期字典的值是一个Long类型的整数,保存了键所指向的数据库键的过期时间(毫米精度的UNIX时间戳)。通过直接查询字典的过期时间与当前时间对比来判断键是否过期。
过期键删除策略
Redis服务器实际使用惰性删除和定期删除策略,通过配合使用两种删除策略,很好地在使用CPU和避免内存浪费之间取得了平衡;
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作;
- 惰性删除:放任键过期不管,每次从键空间获取键时,都检查取得的键是否过期,如果过期则删除该键,没有则返回该键;
- 定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键;
7.RDB持久化
RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态(将非空数据库以及它们的键值对统称为数据库状态)。
RDB文件的创建与载入
RDB文件的载入工作在服务器启动时自动执行的,创建RDB文件有两个命令可以用于生成RDB文件:
-
SAVE
:阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求; -
BGSAVE
:与SAVE
的直接阻塞服务器进程不同,BGSAVE命令会派生出一个子进程来创建RDB文件,Redis服务器仍然可以继续处理客户端的命令请求。但是对于SAVE、BGSAVE命令会被拒绝,防止同时调用rdbsave函数产生竞争条件;对于BGREWRITEAOF,如果BGSAVE正在执行,则BGREWRITEAOF会被延迟到前者执行完成,如果BGREWRITEAOF正在执行,BGSAVE会被拒绝;
由于AOF文件的更新频率通常比RDB文件的更新频率高,所以:
- 如果服务器开启了AOF持久化功能,那么服务器会使用AOF文件来还原数据库状态;
- 只有AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态,由
rdb.c/rdbLoad
函数完成;
自动间隔性保存
BGSAVE可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save
选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
save 900 1 //服务器在900秒之内,对数据库进行了至少1次修改
save 300 10 //服务器在300秒之内,对数据库进行了至少10次修改
save 60 10000 //服务器在60秒之内,对数据库进行了至少10000次修改
保存条件保存在服务器状态redisServer结构中的saveparams属性中,saveparams
是1个数组,数组中的每个元素都是一个savepara
结构,代表1个保存条件;
struct savaparam {
//秒数
time_t seconds;
//修改数
int changes;
}
dirty计数器和lastsave属性
- dirty计数器:记录距离上一次成功执行SAVE或BGSAVE命令之后,服务器对数据库状态进行了多少次修改;
- lastsave属性:一个UNIX时间戳,记录了服务器上一次成功执行SAVE或BGSAVE命令的时间;
Redis服务器周期性操作函数serverCron
,默认每个100毫秒执行一次对保存条件的检查,会遍历检查saveparams数组内的所有保存条件,只要有任意一个条件被满足,那么服务器就会执行BGSAVE命令。
RDB文件结构
REDIS | db_version | databases | EOF | check_sum |
---|
注:全大写单词表示常量,全小写表示变量和数据
- REDIS:RDB文件的最开头部分,长度5字节,保存着"REDIS"这5个字符,方便在载入时快速检查文件是否RDB文件;
- db_version:长度4字节,值是一个字符串表示的整数,记录了RDB文件的版本号;
- databases:包含0个或任意多个数据库,以及各个数据库中的键值对数据。若服务器的数据库状态为空,则对应这部分也为空;若数据库状态非空,则根据数据库所保存键值对的数量、类型和内容不同,对应长度也不同;
- EOF:长度1字节,标志着RDB文件正文内容的结束;
- check_sum:长度8字节,无符号整数,保存着一个校验和,由前面4部分的内容进行计算得出;
每个非空数据库在RDB文件中都可以保存为SELECTDB
、db_number
、key_value_pairs
;
SELECTDB:长度1字节的常量,表示接下来要读入的将是一个数据库号码;
db_number:保存着1个数据库号码,根据号码的不同,长度可以是1,2,5字节,用于切换数据库,使对应数据库的键值对能存入正确;
-
key_value_pairs:保存了数据库中的所有键值对数据,若键值对有过期时间,则过期时间也会和键值对保存在一起。键值对由[EXPIRETIME_MS、ms]、TYPE、key、value组成:
- EXPIRETIME_MS:(若无过期时间则不存在此属性) 常量,长度1字节,告知程序接下来读入的将是以毫秒为单位的过期时间;
- ms:(若无过期时间则不存在此属性) 8字节的带符号整数,记录一个以毫秒为单位的UNIX时间戳,代表键值对的过期时间;
- TYPE:记录了value的类型,长度为1字节;
- key:字符串对象,以REDIS_RDB_TYPE_STRING类型编码,长度随内容变化;
- value:根据TYPE类型的不同和保存内容的不同,结构和长度也会不同;
value的编码
-
字符串对象(REDIS_RDB_TYPE_STRING)
字符串对象的编码可以是REDIS_ENCODING_INT
和REDIS_ENCODING_RAW
。- 若为
REDIS_ENCODING_INT
,说明对象保存的是长度超过32位的整数,编码对象按[EBCODING,integer]
形式保存,ENCODING可以是ENCODING_RDB_ENC_INT8/16/32
,分别代表使用8,16,32位来保存整数值 - 若为
REDIS_ENCODING_RAW
编码,代表对象所保存的是1个字符串值,根据字符串长度不同,有压缩(字符串长度>20字节)和不压缩(字符串长度<=20字节,直接原样保存)两种方法保存;- 对于没有被压缩的字符串,RDB程序会以[len string]结构来保存该字符串,string部分保存了字符串值本身,len保存了字符串值的长度;
- 对于压缩后的字符串,RDB程序会以一下结构保存字符串的长度。REDIS_RDB_ENC_LZF常量标志着字符串已经被LZF算法压缩过,会根据压缩后长度compressed_len、原长度origin_len、压缩后字符串compressed_string对字符串进行解压;
- 若为
REDIS_RDB_ENC_LZF | compressed_len | origin_len | compressed_string |
---|
2.列表对象
Type的值为REDIS_RDB_TYPE_LIST,那么value保存的就是一个REDIS_ENCODING_LINKEDLIST编码的列表对象,结构如下,list_length记录了列表的长度,item代表列表的项,每个列表项都是一个字符串对象。
list_length | item1 | item2 | … | itemN |
---|
3.集合对象
Type的值为REDIS_RDB_TYPE_SET,那么value保存的就是一个REDIS_ENCODING_HT编码的集合对象,结构如下,set_size记录了集合的大小,elem代理集合元素,每一个集合都是一个字符串对象。
set_size | elem1 | elem2 | … | elemN |
---|
4.哈希表对象
Type的值为REDIS_RDB_TYPE_HASH,那么value保存的是一个REDIS_ENCODING_HT编码的集合对象,结构如下,hash_size记录了哈希表的大小,key_value_pair代表哈希表中的键值对,键和值均为字符串对象。
hash_size | key_value_pair1 | key_value_pair2 | … | key_value_pairN |
---|
5.有序集合对象
Type的值为REDIS_RED_TYPE_ZSET,那么value保存的是REDIS_ENCODING_SKIPLIST编码的有序集合对象,结构如下,sorted_set_size记录了有序集合的大小,element代表有序集合中的元素,每个元素分为成员和分值,成员是一个字符串对象,分值则是一个double类型的浮点数。
sorted_set_size | element1 | element2 | … | elementN |
---|
6.整数集合(INTSET编码)
Type的值为REDIS_RDB_TYPE_SET_INTSET,那么value保存的是一个整数集合对象,保存方法是将整数集合转换为字符串对象,读取时再由字符串对象转回整数集合。
7.ZIPLIST编码的列表、哈希表或者有序集合
Type的值为REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST或者REDIS_RDB_TYPE_ZSET_ZIPLIST,那么value保存的是一个压缩列表对象,保存方法是将压缩列表转换一个字符串对象后保存到RDB文件,读取时再由字符串转换回原来的压缩列表对象,再根据TYPE的值设置压缩列表对象的类型。
8.AOF持久化
RDB持久化通过保存数据库中的键值对来记录数据库状态,AOF持久化通过保存Redis服务器执行的写命令来记录服务器状态。服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。
1)AOF持久化功能的实现
- 命令追加:当AOF持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
- AOF文件的写入和同步:Redis的服务器进程就是一个事件循环,循环中的文件事件负责接收客户端的命令请求并向客户端发送命令回复,时间事件则负责执行定时任务。
当处理文件事件时执行了写命令,会使其被追加到aof_buf缓冲区里面,所以服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数考虑是否需要将aof_buf缓冲区中的内容写入到AOF文件里面。
由服务器配置的appendfsync选项的值决定是否将缓冲区中的内容同步到AOF文件中。
appendfsync | flushAppendOnlyFile函数行为 |
---|---|
always | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件 |
everysec(默认) | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件,若上次同步距离现在超过一秒,再次同步,该同步操作由一个线程专门负责执行 |
no | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件,但不对其进行同步,何时同步由操作系统决定 |
2)AOF文件的载入和数据还原
AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。详细步骤如下:
- 创建一个不带网络连接的伪客户端,因为Redis的命令只能在客户端上下文中执行;
- 从AOF文件中分析并读取出一条写命令;
- 使用伪客户端执行被读出的写命令;
- 重复步骤2,3,直到AOF文件中的所有写命令都被处理完毕;
3)AOF重写
随着服务器运行,AOF文件记录的内容越来越多、体积越来越大。为了解决AOF文件体积膨胀问题,Redis提供了AOF文件重写功能,使新的AOF代替旧AOF,两者保存的数据库状态相同,但新AOF不包含任何浪费空间的冗余命令。
AOF文件重写的实现:重写功能并不需要读取和分析旧AOF文件,仅需对数据库从读取键值,并通过一条写命令来表示,这样就可以将多次写入的数据通过一条写入命令表示,节省了AOF文件的空间。但是当重写程序处理集合、列表、哈希表等时,会先检查元素数量,若超过REDIS_AOF_REWRITE_ITEMS_PER_CMD
(默认64)时,会用多条命令来记录该集合,每条命令设置的元素数量也为64个。
AOF后台重写
由于Redis服务器使用单个线程来处理命令请求,所以直接由服务器直接调用aof_rewrite
会使服务器在重写期间无法执行客户端请求。
因此Redis将重写程序放到子进程里执行,这样服务器进程可以继续处理请求命令;且子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免使用锁的情况下,保证数据安全性。但子进程重写期间,服务器进程新处理的命令会对现有数据库状态进行修改,使得数据库状态与重写后的AOF文件所保存的数据库状态不一致。因此在子进程重写期间,服务器进程需要执行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到AOF缓冲区;
- 将执行后的写命令追加到AOF重写缓冲区;
这样就保证了AOF缓冲区的内容会定期被写入和同步到AOF文件;从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区。当子进程完成AOF重写之后,会向服务器进程发生信号,服务器进程接到信号执行信号处理函数:
- 将AOF重写缓冲区的内容写入到新AOF,此时AOF和服务器保存的数据库状态一致;
- 对新AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧AOF的替换;
9.事件
1)文件事件
Redis服务器通过套接字与客户端/其他服务器进行连接,文件事件就是服务器对套接字操作的抽象。文件事件处理器主要由套接字、I/O多路复用程序、文件事件分派器以及事件处理器构成。
- 文件事件处理器使用I/O多路复用来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器;
- 当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生来处理这些事件;
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字放到一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。
文件事件的处理器
- 连接应答处理器:用于对连接服务器监听套接字的客户端进行应答,当Redis服务器进行初始化时,会将此连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来;
- 命令请求处理器:负责从套接字中读入客户端发生的命令请求内容,当客户端通过连接应答处理器成功连接到服务器后,会将客户端套接字的AR_READABLE事件和命令请求处理器关联起来;
- 命令回复处理器:负责将服务器执行命令后得到的命令回复通过套接字返回给客户端,当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE时间和命令回复处理器关联;
2)时间事件
Redis的时间事件分为定时事件、周期性事件,一个时间事件主要由以下三个属性组成:
- id:服务器为时间事件创建的全局唯一ID;
- when:毫秒精度的UNIX时间戳,记录了时间事件的到达时间;
- timeProc:时间事件处理器,当时间事件到达时,服务器会调用相应的处理器来处理时间;
一个时间事件是定时还是周期主要取决于时间事件处理器的返回值,服务器将所有时间事件都放在一个无序链表(不按when属性排序)中,每当时间事件执行器运行时,遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
- 返回
ae.h/AE_NOMORE
,则为定时事件,在达到一次之后就会被删除,之后不再到达; - 返回非
AE_NOMORE
的整数,则为周期事件,当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新;
3)事件的调度和执行
由于服务器同时存在文件事件和时间事件两种事件类型,所有服务器必须对这两种事件进行调度,决定何时处理何种事件。
- aeApiPoll函数的最大阻塞事件由到达时间最接近当前时间的时间事件决定,这样既可避免服务器对时间事件进行频繁的轮询,也可确保aeApiPoll不会阻塞过长时间;
- 由于文件事件时随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,则服务器再次等待并处理文件事件;
- 对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。尽可能的减少程序的阻塞时间,并在有需要时主动让出执行权,降低造成事件饥饿的可能性;