Redis学习(原理篇)

前言

读完《Redis设计与实现》这本书之后,感觉讲得很好很详细,特此进行一些常用点的记录总结,以供之后复习回顾。

对象

Redis的主要数据结构是简单动态字符串SDS、双端链表、字典、压缩列表、整数集合、跳跃表(分别对应Redis数据类型String、List、Hash、Set和ZSet的底层实现),但是Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建一个对象系统,这个对象系统中包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象五种类型的对象,并且每种对象都用到了至少一种数据结构(Redis不同数据类型数据以不同类型对象的形式分别使用至少一种数据结构存储于内存中构成数据库)【即给一个数据类型相当于new了一个对象,这个对象类型根据数据类型来定,数据存储的格式根据数据的类型和大小定,主要定的就是最终的存储数据结构】

通过不同类型的对象,Redis可以在执行命令前先根据对象的类型(type)来判断这个对象是否可以执行给定的命令。

对象系统的作用如下:

  • Redis在执行命令前,根据对象类型判断对象是否可执行给定的命令(类型检查)
  • 可针对不同的使用场景,为对象设置多种不同的数据结构实现从而优化对象在不同场景下的使用效率(多态实现)
  • 实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象时,这个对象占用的内存会被自动释放
  • 实现了基于引用计数技术实现对象共享机制,该机制在适当条件下可通过让多个数据库键共享同一个对象来节约内存
  • 对象带有访问时间记录信息,用于计算数据库键的空转时长(当前时间-时间记录变量)。在服务器启用maxmemory功能情况下,空转时长较大的键可能优先会被服务器删除

Redis数据库中的每个键值对中的键和值都是以对象形式定义存储,其中对象主要通过type、encoding、ptr、refcount和lru属性进行定义不同类型不同编码方式(对应不同存储结构不同命令实现方式),基于type在命令执行前进行类型检查、基于encoding对于不同编码方式的对象同一命令有不同实现方式(多态的一种体现方式,另一种是基本键类型同一命令对于不同类型对象键实现方式不同)、基于refcount引用计数技术共享对象(Redis自身会创建1-9999的字符串对象用于共享)和定期回收内存(每访问一次,这个内存的refcount++)、基于lru可以计算对象的空转时间并定期清除空转时间长的键以释放内存。

数据库

Redis服务器将所有数据库保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每一项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库(字典)。其中redisServer中有一个属性dbnum作用是在初始化服务器时决定应该创建多少个数据库,dbnum属性值由服务器配置的database选项决定,默认为16即Redis服务器默认会创建16个数据库。

每个Redis客户端都有自己的目标数据库,每当客户端执行数据库写/读命令时,目标数据库会成为这些命令的操作对象。默认情况下Redis客户端的目标数据库为0号数据库,但客户端可通过执行SELECT命令来切换目标数据库。

在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针,指向redisServer.db数组中的其中一个元素即客户端的目标数据库。

对Redis数据的CRUD操作即对数据库db[n]下字典结构中对应数据的CRUD操作。Redis是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示。其中redisDb结构的dict字典保存数据库中的所有键值对,也称这个dict字典为键空间(key space)。【因为Redis数据库的键空间是一个字典,故所有对数据库的CRUD操作(如添加一个键值对到数据库;从数据库中删除一个键值对;在数据库中获取某个键值对等)都是通过对键空间字典进行操作实现的。】

  • 添加一个新键值对到数据库实际上就是将一个新键值对添加到键空间字典中,其中键为字符串对象,值为任意一种类型的Redis对象(String、List、Hash、Set、ZSet)。
  • 删除数据库中的一个键实际上是在键空间字典中删除键所对应的键值对对象(删除整个键+值)。

  • 对一个数据库键更新实际上是对键空间字典中键所对应的值对象进行更新,根据值对象的类型不同更新的具体方法不同。(Hash数据类型中hset 同一个key 不同field 不同value表示对这个key进行更新,实际上是在key下添加一个新的field-value键值对)

  • 对一个数据库键取值实际上是在键空间中取出键所对应的值对象,根据值对象类型不同取值方法有所不同。

  • FLUSHDB:通过删除键空间中的所有键值对实现

  • RANDOMKEY:通过在键空间中随机返回一个键实现(而非键对应的值)

  • DBSIZE:通过返回键空间中包含的键值对的数量实现

Redis服务器实际中使用的是惰性删除和定期删除两种策略,通过配合使用这两种策略,服务器可很好合理使用CPU时间和避免浪费内存空间间取得平衡。

 

发布/订阅

当一个客户端执行SUBSCRIBE命令订阅某个或某些频道时,这个客户端与被订阅频道间建立起一种订阅关系。

Redis将所有频道的订阅关系都保存在服务器状态即redisServer结构pubsub_channels字典中,这个字典的键是某个被订阅的频道,键的值则是一个链表,链表中记录了所有订阅这个频道的客户端

每当客户端执行SUBSCRIBE命令订阅某个/某些频道时,服务器会将客户端与被订阅的频道在pubsub_channels字典(频道有了客户端订阅后即有了订阅者后才会出现在pubsub_channels中,如果只是服务器发布了频道但是无人订阅这个频道并不会存入pubsub_channels字典中)中进行关联。根据频道是否已经有其他订阅者,关联操作可分为两种情况进行:

  • 如果频道已经有其他订阅者,那么其在pubsub_channels字典中必定有相应的订阅者链表,程序需要做的就是将新的订阅者(客户端)键入订阅者链表的末尾即可
  • 如果频道中无其他订阅者,那么必然不存在于pubsub_channels字典中。程序首先在pubsub_channels字典中为频道创建一个键,键名为频道名,设置该键的值为空链表,然后将客户端添加到链表成为链表的第一个元素

客户端使用UNSUBSRCIBE命令退订频道,操作与SUBSCRIBE恰恰相反,当一个客户端退订某个/某些频道时,服务器将从pubsub_channels中解除客户端与被退订频道之间的关联。

  • 程序根据被退订频道的名字,在pubsub_channels字典中找到频道对应的订阅者链表,然后从链表中删除退订客户端的信息
  • 如果删除客户端信息后,频道的订阅者链表成为空链表,则说明这个频道此时已无订阅者,程序将从pubsub_channels字典中删除频道对应的键

 

服务器将所有频道的订阅关系保存在服务器状态的pubsub_channels属性中(该属性是一个字典结构),类似的服务器将所有模式的订阅关系保存在服务器状态的pubsub_patterns属性中(该属性是一个链表结构)。

每当客户端执行PSUBSCRIBE命令订阅某个/某些模式时,服务器会对每个被订阅的模式执行两个操作。

  • 新建一个pubsubPattern结构,将结构中的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端
  • 将pubsubPattern结构添加到pubsub_patterns链表的表尾

客户端退订模式使用PUNSUBSCRIBE命令,操作与PSUBSCRIBE命令相反。当一个客户端退订某个/某些模式时,服务器将在pubsub_patterns链表中查找并删除那些pattern属性为被退订模式,且client属性为执行退订命令的客户端的pubsubPattern结构(根据pattern和client查询pubsub_patterns链表中的目标pubsubPattern结构并删除)

当一个Redis客户端执行PUBLISH <channel> <message>命令将消息message发送给频道channel时,服务器需要执行两个操作,分别是发送给频道的所有订阅者和发送给匹配该频道的所有模式的所有订阅者。

Redis2.8后新增PUBSUB命令,客户端可通过此命令来查看频道/模式的相关信息(如订阅的某频道/模式当前有多少订阅者)。其中PUBSUB命令有三个子命令PUBSUB CHANNELS、PUBSUB NUMSUB和PUBSUB NUMPAT,这三个子命令都是通过读取pubsub_channels字典和pubsub_patterns链表中的信息来实现的。

【注:PUBSUB CHANNELS通过读取pubsub_channels字典中的键实现;PUBSUB NUMSUB通过读取pubsub_channels字典中的某个键对应的链表长度实现;PUBSUB NUMPAT通过读取pubsub_patterns链表的长度实现】

持久化

Redis的RDB持久化方式就是要保存Redis的数据库状态,即将非空数据库及其键值对保存在硬盘上以确保数据的完备性(Redis是内存数据库,这些数据库状态时存储在内存中的,所以需要持久化机制来防止服务器宕机后服务器进程退出导致服务器中的数据库状态消失从而造成损失)。

Redis默认将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中,可通过配置dir和dbfilename分别制定快照文件的存储路径和文件名。过程如下:

  • Redis使用fork函数(创建进程)复制一份当前进程(父进程)的副本(子进程)
  • 父进程继续接收并处理客户端发来的命令,子进程开始将内存中的数据写入硬盘中的临时文件
  • 当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此一次快照操作完成(新的RDB文件中存储的是fork时的内存数据)

即先创建子进程,子进程将存储内存数据的临时文件写入硬盘,写入完毕后临时文件替换硬盘中原有的RDB(子进程写入硬盘缓冲区,然后持久化到硬盘并替换硬盘中的旧RDB)

AOF持久化主要是通过保存当前数据库的写命令到硬盘上完成的(append、write、sync),当数据恢复的时候服务器直接从硬盘上将AOF文件载入并读取执行其中的写命令即可恢复数据库状态。因为AOF文件是存放写命令并且以协议格式存放,随写命令越来越多文件体积会越来越大,为减轻硬盘负担Redis支持定期AOF文件重写,重写主要是服务器进程开一个子进程给一个数据副本并且将副本中数据库状态转为增命令保存在重写的AOF文件中,为确保重写前后数据库状态一致会设置一个AOF重写缓冲区,当重写完毕后服务器暂停对客户端的服务去将AOF重写缓冲区的写命令追加到重写的AOF文件中并覆盖掉硬盘上原有的AOF文件完成重写(开子进程-子进程重写-父进程将新写命令发送到AOF重写缓冲区-追加到AOF文件-覆盖原AOF文件)【重写过程中AOF持久化会继续保持,所以新增写命令也会发送到AOF缓冲区,等待flushAppendOnlyFile函数启动进行文件写入和同步】。

复制

Redis中用户可通过执行SLAVEOF命令或设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,前者为从服务器,后者为主服务器。经复制操作后主从服务器的数据库状态保持一致,并且写操作也会同步(例如对主服务器执行写操作即增/改/删操作,从服务器也会执行由主服务器发过来的增/改/删命令)。

在Redis2.8版本以前,复制功能分为同步(sync)和命令传播(command propagate)两个操作。

为解决旧版复制功能在处理断线重复制情况的低效问题,Redis从2.8版本后使用PSYNC替代SYNC命令来执行复制时的同步操作。

PSYNC命令分为完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式。

事务

Redis通过MULTI、EXEC、WATCH等命令实现事务(transaction)功能,事务提供一种将多个命令打包然后一次性、按顺序执行的机制,并且在事务执行(处于事务状态的客户端执行EXEC命令后)期间,服务器不会中断事务,会将事务中的所有命令都执行完毕才去处理来自客户端的命令请求(此时客户端发送来的命令请求应该存储在客户端的输入缓冲区中等待单进程的服务器进程空闲后进行读取处理)。

【注:在一个客户端执行MULTI命令后,此客户端从非事务状态切换为事务状态,之后的命令都被服务器发送到该客户端放入事务队列中等待执行(此时其他非事务状态的客户端正常发送请求服务器正常执行命令)。等处于事务状态的客户端执行EXEC命令后,服务器暂停与其他客户端的交互专一执行事务队列中的命令,执行完毕后此客户端转为非事务状态,服务器恢复与客户端的交互(类似于排队的暂时离开,等回来以后就优先执行暂时离开的)】

一个事务从开始到结束通常经历三个阶段,分别是

  • 事务开始:MULTI命令标志事务的开始。MULTI命令将执行该命令的客户端从非事务状态切换至事务状态(此转换通过在客户端状态的flags属性中打开REDIS_MULTI标识完成)
  • 命令入队:当一个客户端处于非事务状态时,发送的请求命令会立即被服务器执行。但是当客户端处于事务状态时,服务器会根据这个客户端发来不同命令执行不同操作。若客户端发送EXEC、DISCARD、WATCH、MULTI命令之一,服务器立即执行命令;若客户端发送其他命令,服务器并不立即执行而是将其放入一个事务队列(FIFO)(保存于这个处于事务状态的客户端的multiState事务状态结构中)中,然后向客户端返回QUEUED回复
  • 事务执行:当处于事务状态的客户端向服务器发送EXEC命令时,EXEC命令立即被服务器(服务器进程会暂停当前的普通与客户端请求命令处理任务)执行。执行过程是:服务器遍历这个客户端的事务队列(commands指针),执行队列中保存的所有命令(multiCmd数组),最后将执行结果全部返回(创建一个回复队列存储命令执行结果)给此客户端

WATCH命令是个乐观锁optimistc locking,可以在EXEC命令执行前监视任意数量的数据库键,在EXEC命令执行期间检查被监视的键(事务队列命令中包含的键)是否有至少一个已经被修改,如果有则服务器拒绝执行事务,并向客户端返回代表事务执行失败的空回复(nil).

Redis数据库保存一个watched_keys字典,字典的键是被WATCH命令监视的数据库键,字典的值为一个链表,这个链表记录所有监视相应数据库键的客户端。(执行watch 键名1 命令意味着在数据库的watched_keys字典中会添加一个键名=键名1,值为一个包含执行这条命令的客户端名的链表;如果整个键名本身存在,则直接将整个客户端添加至链表表尾即可表示客户端使用WATCH命令对键的监视

在Redis中所有对数据库状态(键值对)进行修改的命令(如SET、LPUSH、RPUSH、HSET、HMSET、LPOP、RPOP、ZADD、SADD、ZREM、DEL、FLUSHDB等),执行调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,以此表示该客户端的事务安全性已经被破坏。当服务器接收到客户端发送来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务(如果打开则表示客户端所监视的键中至少有一个键已经被修改,事务已经不再安全,服务器会拒绝执行该客户端提交的事务;如果没有被打开则表示客户端所监视的键没有键被修改(或者客户端对事务中的键没有执行WATCH监视命令),事务依旧安全,服务器将正常执行该客户端提交的事务)

【注:每对某个键执行一个写命令,命令执行完毕后(这个命令肯定是非事务状态的客户端执行的,因为处于事务状态的客户端的命令都会写入事务队列中等待一次性顺序处理)就会去调用touchWatchKey函数去检查数据库中watched_keys字典中是否有此键,如果有则遍历打开对应链表中客户端的REDIS_DIRTY_CAS标识表示该客户端事务安全性已被破坏】

总结

此次对于Redis的概念、特点、适用场景、数据类型及底层存储、数据库、发布/订阅、复制、持久化、事务以及一些位运算、排序、慢日志等高级功能进行较为认真的学习,此外后续还需要对Redis的客户端、服务端、集群、Sentinel哨兵模式进行进一步的学习,如有机会根据遇到的实际问题结合理论知识进行总结。

发布了41 篇原创文章 · 获赞 9 · 访问量 9756

猜你喜欢

转载自blog.csdn.net/qq_38586378/article/details/104647451