`
戈尔-D-罗杰
  • 浏览: 2868 次
文章分类
社区版块
存档分类
最新评论

浅述NoSQl之Redis+内部机制

阅读更多
1      Redis概述
1.1 前言
最近公司的项目用Redis,于是了解一下。本文主要是阐述Redis的内部运行机制。

Redis是一个开源、支持网络、基于内存亦可持久化的日志型、键值对存储数据库。使用ANSI C编写,并提供多种语言的API。

其开发由VMware主持,是最流行的键值对存储数据库之一。

Redis的一大特点就是速度异常快,官方公布的性能测试结果显示,每秒钟可以达到10万次的操作。

1.2 安装和验证
在Redis的官网上,我们下载了redis的稳定版redis-2.8.9.tar.gz。

我们依次执行以下命令:

$ tar xzf redis-2.8.9.tar.gz

$ cd redis-2.8.9

$ make

在执行完以上命令后,会在同级目录下生成src目录。再执行命令:

$ src/redis-server

就启动好了Redis。



快速验证服务是否启动成功可以执行以下命令:

$ src/redis-cli

redis> set foo bar

OK

redis> get foo

"bar"

2      Redis的内部实现
我们主要来关注一下redis的内部实现。

2.1 服务器初始化
Redis服务器除了维持服务器状态之外,最重要的就是将数据结构、数据类型、事务、Lua 环境、事件处理、数据库、持久化等功能模块组合起来。

从启动Redis服务器,到服务器可以接受外来客户端的网络连接这段时间, Redis 需要执行一系列初始化操作。

整个初始化过程可以分为以下六个步骤:

1)   初始化服务器全局状态。

2)   载入配置文件。

3)   创建daemon 进程。

4)   初始化服务器功能模块。

5)   载入数据。

6)   开始事件循环。

以下各个小节将介绍 Redis 服务器初始化的各个步骤。

2.1.1 初始化服务器全局状态
redis.h/redisServer结构记录了和服务器相关的所有数据,这个结构主要包含以下信息:

ü  服务器中的所有数据库。

ü  命令表:在执行命令时,根据字符来查找相应命令的实现函数。

ü  事件状态。

ü  服务器的网络连接信息:套接字地址、端口,以及套接字描述符。

ü  所有已连接客户端的信息。

ü  Lua 脚本的运行环境及相关选项。

ü  实现订阅与发布(pub/sub)功能所需的数据结构。

ü  日志(log)和慢查询日志(slowlog)的选项和相关信息。

ü  数据持久化(AOF 和 RDB)的配置和状态。

ü  服务器配置选项:比如要创建多少个数据库,是否将服务器进程作为daemon 进程来运行,最大连接多少个客户端,压缩结构(zip structure)的实体数量,等等。

ü  统计信息:比如键有多少次命令、不命中,服务器的运行时间,内存占用,等等。

为了简洁起见,上面只列出了单机情况下的 Redis 服务器信息,不包含SENTINEL 、 MONITOR、CLUSTER 等功能的信息。

在这一步,程序创建一个redisServer结构的实例变量server用作服务器的全局状态,并将server的各个属性初始化为默认值。

当server变量的初始化完成之后,程序进入服务器初始化的下一步:读入配置文件。

2.1.2 载入配置文件
Redis.conf文件里各个配置项的意义都有英文注释,这里不再啰嗦。

在初始化服务器的上一步中,程序为server变量(也即是服务器状态)的各个属性设置了默认值,但这些默认值有时候并不是最合适的:

ü  用户可能想使用 AOF 持久化,而不是默认的 RDB 持久化。

ü  用户可能想用其他端口来运行Redis,以避免端口冲突。

ü  用户可能不想使用默认的 16 个数据库,而是分配更多或更少数量的数据库。

ü  用户可能想对默认的内存限制措施和回收策略做调整。

等等。

为了让使用者按自己的要求配置服务器, Redis允许用户在运行服务器时,提供相应的配置文件(config file)或者显式的选项(option), Redis在初始化完server变量之后,会读入配置文件和选项,然后根据这些配置来对server变量的属性值做相应的修改:

1.    如果单纯执行redis-server命令,那么服务器以默认的配置来运行 Redis。

2.    另一方面,如果给 Redis 服务器送入一个配置文件,那么 Redis 将按配置文件的设置来更新服务器的状态。

比如说,通过命令redis-server /etc/my-redis.conf ,Redis 会根据

my-redis.conf文件的内容来对服务器状态做相应的修改。

3.    除此之外,还可以显式地给服务器传入选项,直接修改服务器配置。

举个例子,通过命令redis-server --port 10086,可以让 Redis 服务器端口变更为10086。

4.    当然,同时使用配置文件和显式选项也是可以的,如果文件和选项有冲突的地方,那么优先使用选项所指定的配置值。

举个例子,如果运行命令redis-server /etc/my-redis.conf –port 10086,并且my-redis.conf也指定了port选项,那么服务器将优先使用—port 10086(实际上是选项指定的值覆盖了配置文件中的值)。

2.1.3 创建 daemon 进程
Redis 默认以 daemon 进程的方式运行。

当服务器初始化进行到这一步时,程序将创建 daemon 进程来运行 Redis ,并创建相应的 pid 文件。

2.1.4 初始化服务器功能模块
在这一步,初始化程序完成两件事:

为server变量的数据结构子属性分配内存。

初始化这些数据结构。

为数据结构分配内存,并初始化这些数据结构,等同于对相应的功能进行初始化。

比如说,当为订阅与发布所需的链表分配内存之后,订阅与发布功能就处于就绪状态,随时可以为 Redis 所用了。

在这一步,程序完成的主要动作如下:

  初始化 Redis 进程的信号功能。

  初始化日志功能。

  初始化客户端功能。

  初始化共享对象。

  初始化事件功能。

  初始化数据库。

  初始化网络连接。

  初始化订阅与发布功能。

  初始化各个统计变量。

  关联服务器常规操作(cron job)到时间事件,关联客户端应答处理器到文件事件。

  如果 AOF 功能已打开,那么打开或创建 AOF 文件。

  设置内存限制。

  初始化 Lua 脚本环境。

  初始化慢查询功能。

  初始化后台操作线程。

完成这一步之后,服务器打印出 Redis 的 ASCII LOGO 、服务器版本等信息,表示所有功能模块已经就绪,可以等待被使用了。

虽然所有功能已经就绪,但这时服务器的数据库还是一片空白,程序还需要将服务器上一次执行时记录的数据载入到当前服务器中,服务器的初始化才算真正完成。

2.1.5 载入数据
在这一步,程序需要将持久化在 RDB 或者 AOF 文件里的数据,载入到服务器进程里面。

如果服务器有启用AOF功能的话,那么使用 AOF 文件来还原数据;否则,程序使用 RDB 文件来还原数据。

当执行完这一步时,服务器打印出一段载入完成信息:

[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds

2.1.6 开始事件循环
到了这一步,服务器的初始化已经完成,程序打开事件循环,开始接受客户端连接。

以下是服务器在这一步打印的信息:

[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379

至此初始化完成。

2.2 内部结构
Redis的内部结构如下图所示:




各功能模块说明如下:

File Event: 处理文件事件(在多个客户端中实现多路复用,接受它们发来的命令请求(读事件),并将命令的执行结果返回给客户端(写事件))

Time Event: 时间事件(更新统计信息,清理过期数据,附属节点同步,定期持久化等)

AOF: 命令日志的数据持久化

RDB:实际的数据持久化

Lua Environment : Lua 脚本的运行环境. 为了让 Lua 环境符合 Redis 脚本功能的需求, Redis 对 Lua 环境进行了一系列的修改,包括添加函数库、更换随机函数、保护全局变量,等等

Command table(命令表):在执行命令时,根据字符来查找相应命令的实现函数。

Share Objects(对象共享):

主要存储常见的值:a.各种命令常见的返回值,例如返回值OK、ERROR、WRONGTYPE等字符;b. 小于 redis.h/REDIS_SHARED_INTEGERS (默认1000)的所有整数。通过预分配的一些常见的值对象,并在多个数据结构之间共享对象,程序避免了重复分配的麻烦。也就是说,这些常见的值在内存中只有一份。

Databases:

Redis数据库是真正存储数据的地方。当然,数据库本身也是存储在内存中的。

Databased的数据结构伪代码如下:

typedef struct redisDb {

    // 保存着数据库以整数表示的号码

    int id;



    // 保存着数据库中的所有键值对数据

    // 这个属性也被称为键空间(key space)

    dict *dict;



    // 保存着键的过期信息

    dict *expires;



    // 实现列表阻塞原语,如 BLPOP

    // 在列表类型一章有详细的讨论

    dict *blocking_keys;

    dict *ready_keys;



    // 用于实现 WATCH 命令

    // 在事务章节有详细的讨论

    dict *watched_keys;

} redisDb;

一个数据库,在内存中的数据结构如下图所示:


Database的内容要点包括:

  数据库主要由 dict 和 expires 两个字典构成,其中 dict 保存键值对,而 expires 则保存键的过期时间。

  数据库的键总是一个字符串对象,而值可以是任意一种 Redis 数据类型,包括字符串、哈希、集合、列表和有序集。

  expires 的某个键和 dict 的某个键共同指向同一个字符串对象,而 expires 键的值则是该键以毫秒计算的 UNIX 过期时间戳。

  Redis 使用惰性删除和定期删除两种策略来删除过期的键。

  更新后的 RDB 文件和重写后的 AOF 文件都不会保留已经过期的键。

  当一个过期键被删除之后,程序会追加一条新的 DEL 命令到现有 AOF 文件末尾。

  当主节点删除一个过期键之后,它会显式地发送一条 DEL 命令到所有附属节点。

  附属节点即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,这样可以保证主节点和附属节点的数据总是一致的。

  数据库的 dict 字典和 expires 字典的扩展策略和普通字典一样。它们的收缩策略是:当节点的填充百分比不足 10% 时,将可用节点数量减少至大于等于当前已用节点数量。

2.3 客户端连接到服务器
当Redis服务器完成初始化之后,它就准备好可以接受外来客户端的连接了。

当一个客户端通过套接字函数connect到服务器时,服务器执行以下步骤:

1.   服务器通过文件事件无阻塞地accept客户端连接,并返回一个套接字描述符 fd 。

2.   服务器fd创建一个对应的redis.h/redisClient结构实例,并将该实例加入到服务器的已连接客户端的链表中。

3.   服务器在事件处理器为该fd关联读文件事件。

完成这三步之后,服务器就可以等待客户端发来命令请求了。

Redis 以多路复用的方式来处理多个客户端,为了让多个客户端之间独立分开、不互相干扰,服务器为每个已连接客户端维持一个redisClient 结构,从而单独保存该客户端的状态信息。

redisClient结构主要包含以下信息:

  套接字描述符。

  客户端正在使用的数据库指针和数据库号码。

  客户端的查询缓冲(query buffer)和回复缓存(reply buffer)。

  一个指向命令函数的指针,以及字符串形式的命令、命令参数和命令个数,这些属性会在命令执行时使用。

  客户端状态:记录了客户端是否处于SLAVE、MONITOR 或者事务状态。

  实现事务功能(比如MULTI和WATCH)所需的数据结构。

  实现阻塞功能(比如BLPOP和BRPOPLPUSH)所需的数据结构。

  实现订阅与发布功能(比如PUBLISH和SUBSCRIBE)所需的数据结构。

  统计数据和选项:客户端创建的时间,客户端和服务器最后交互的时间,缓存的大小,等等。

为了简洁起见,上面列出的客户端结构信息不包含复制(replication)的相关属性。

2.4 命令的请求、处理和结果返回
当客户端连上服务器之后,客户端就可以向服务器发送命令请求了。

从客户端发送命令请求,到命令被服务器处理、并将结果返回客户端,整个过程有以下步骤:

1.    客户端通过套接字向服务器传送命令协议数据。

2.    服务器通过读事件来处理传入数据,并将数据保存在客户端对应redisClient结构的查询缓存中。

3.    根据客户端查询缓存中的内容,程序从命令表中查找相应命令的实现函数。

4.    程序执行命令的实现函数,修改服务器的全局状态server变量,并将命令的执行结果保存到客户端redisClient结构的回复缓存中,然后为该客户端的fd关联写事件。

5.    当客户端fd的写事件就绪时,将回复缓存中的命令结果传回给客户端。至此,命令执行完毕。

3      命令请求实例:SET
为了更直观地理解命令执行的整个过程,我们用一个实际执行 SET 命令的例子来讲解命令执行的过程。

假设现在客户端 C1 是连接到服务器 S 的一个客户端,当用户执行命令SET YEAR 2014 时,客户端调用写入函数,将协议内容*3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2014\r\n" 写入连接到服务器的套接字中。如果你打开了AOF服务的话,你会发现这些协议的内容都会写入到这个文件中。。

当 S 的文件事件处理器执行时,它会察觉到 C1 所对应的读事件已经就绪,于是它将协议文本读入,并保存在查询缓存。

通过对查询缓存进行分析(parse),服务器在命令表中查找SET字符串所对应的命令实现函数,最终定位到t_string.c/setCommand函数,另外,两个命令参数YEAR和2014也会以字符串的形式保存在客户端结构中。

接着,程序将客户端、要执行的命令、命令参数等送入命令执行器:执行器调用setCommand函数,将数据库中YEAR键的值修改为2014,然后将命令的执行结果保存在客户端的回复缓存中,并为客户端fd关联写事件,用于将结果回写给客户端。

因为YEAR键的修改,其他和数据库命名空间相关程序,比如 AOF、REPLICATION 还有事务安全性检查(是否修改了被 WATCH 监视的键?)也会被触发,当这些后续程序也执行完毕之后,命令执行器退出,服务器其他程序(比如时间事件处理器)继续运行。

当 C1 对应的写事件就绪时,程序就会将保存在客户端结构回复缓存中的数据回写给客户端,当客户端接收到数据之后,它就将结果打印出来,显示给用户看。

以上就是SET YEAR 2014命令执行的整个过程。



小结:

服务器经过初始化之后,才能开始接受命令。

服务器初始化可以分为六个步骤:

1.   初始化服务器全局状态。

2.   载入配置文件。

3.   创建 daemon 进程。

4.   初始化服务器功能模块。

5.   载入数据。

6.   开始事件循环。

服务器为每个已连接的客户端维持一个客户端结构,这个结构保存了这个客户端的所有状态信息。

客户端向服务器发送命令,服务器接受命令然后将命令传给命令执行器,执行器执行给定命令的实现函数,执行完成之后,将结果保存在缓存,最后回传给客户端。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics