Redis设计与实现总结——独立功能的实现

发布与订阅

通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当其他客户端向被订阅的频道发送消息时,频道的所有订阅者都会收到这条消息。
除了订阅频道之外,客户端还可以通过执行PSUBSCRIBE命令订阅一个或多个模式,从而成为这些模式的订阅者:每当有其他客户端祥某个频道发送消息时,消息不仅会被发送给这个频道所有订阅者,它还会被发送给所有与这个频道匹配的模式的订阅者。
Redis将所有频道的订阅管系都保存在服务器状态的pubsub_channels字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端。每当执行订阅命令时服务器都会将客户端与被订阅的频道着pubsub_channels字典中进行关联。如果执行退订命令,那么就会从pubsub_channels中删除这个客户端。
模式的订阅则是保存在服务器pubsub_patterns这个属性中,其操作过程与上面相同。
发送消息是就会遍历频道的pubsub_channelspubsub_patterns的客户端,将消息发送给订阅了这些频道和模式的客户端。

事务

Redis通过MULTI,EXEC,WATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性,按顺序地执行多个命令的机制,并且在事务执行期间(当接收到EXEC命令后才开始真正执行, 之前只是命令输入),服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
MULTI命令标识事务的开始,除了EXEC,DISCARD,WATCH,MULTI四个命令外的其他命令都会进入事务的队列中,当接收到EXEC命令时开始执行事务队列中的命令。
WATCH命令是一个乐观锁(optimistic locking), 它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的回复。 (注意WATCH命令执行的顺序是在MULTI之前)。
WATCH命令执行的过程是:

  1. 将监控的键保存到watched_keys字典中,字典的值是所有监视相应数据库键的客户端。
  2. 所有对数据库进行修改的命令都会对watched_keys进行检查,如果键被修改了,就会把客户端的REDIS_DIRTY_CAS标识打开。
  3. 当接收到EXEC执行命令时,如果判断客户端的REDIS_DIRTY_CAS被打开了,标识客户端提交的事务已经不再安全,服务器拒绝客户端提交的事务。

事务的ACID性质: Redis中,事务总是具有原子性(Atomicity), 一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定持久化模式下时,事务也具有耐久性(Durability)

  • 事务的原子性指的是,数据库将事务中的多个操作当做一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。但是Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,知道将事务队列中的所有命令都执行完毕为止。
  • 事务具有一致性指的是,如果数据库在执行事务之前一致的,那么事务在执行之后,无论事务是否执行成功,数据库也应该仍然是一致的。一致指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。
  • 事务的隔离性指的是,即时数据库中有多个事务并发地执行,各个事务之间也不会相互影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。因为Redis是使用单线程的方式执行事务,并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis中的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。
  • 事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质里面了,即使服务器在事务执行完毕后停机,,执行事务所得的结果也不会丢失。Redis有RDBAOF两种持久化方案,但是要持久化方案要和性能进行兼顾。

Lua脚本

Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。使用EVAL命令可以直接对输入的脚本进行求值,而EVALSHA命令则可以根据脚本的SHA1校验和来对脚本进行求值。
为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境,并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足Redis服务器的需要。
Redis服务器创建并修改Lua环境的整个过程由以下步骤组成:

  1. 创建一个基础的 Lua环境(通过调用lua_open函数)
  2. 载入函数库(基础库,表格库,字符串库等), 让Lua脚本可以使用这些函数库来进行数据操作。
  3. 创建全局表格Redis,这个表格包含了对Redis进行操作的函数,比如用于在 Lua脚本中执行Redis命令的redis.call函数
  4. 使用Redis自制的随机函数来替换Lua原有的代有副作用的随机函数,从而避免在脚本中引入副作用。(关于副作用,纯函数的概念参考:wiki
  5. 创建排序辅助函数,Lua环境使用这个辅助函数来对一部分Redis命令的结果(比如集合)进行排序,从而消除这些命令的不确定性。
  6. 创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息。
  7. 对Lua环境中的全局环境进行保护,防止用户在执行Lua脚本过程中,将额外的全局变量添加到Lua环境中。
  8. 将完成修改的Lua环境保存到服务器状态的Lua属性中,等待执行服务器传来的Lua脚本。

除了创建并修改Lua环境之外,Redis服务器还创建了两个用于与 Lua环境进行协作的组件,它们分别是负责执行Lua脚本中的Redis命令的伪客户端,以及用于保存Lua脚本的lua_scripts字典。

  • 伪客户端: 执行Redis命令必须有响应的客户端状态,所以为了执行Lua脚本中包含的Redis命令,Redis服务器专门为Lua环境创建了一个伪客户端,并由这个伪客户端负责处理Lua脚本中包含的所有Redis命令。下图是Lua脚本执行Redis命令时的通信步骤:
    redis_lua命令执行步骤
  • lua_scirpts字典: 这个字典的键为某个Lua脚本的SHA1校验和,而字典的值则是SHA1校验和对应的Lua脚本。
    EVAL命令的执行过程可以分为以下三个步骤:
  1. 根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。
  2. 将客户端给定的脚本保存到lua_scripts字典中,等待将来进一步使用。
  3. 执行刚刚在Lua环境中定义的函数,以此来执行客户端给定的Lua脚本。

Redis还有四个有关Lua脚本的命令:SCRIPT FLUSH, SCRIPT EXISTS, SCRIPT LOADSCRIPT KILL命令。

排序

Redis的SORT命令可以对列表建,集合键或者有序集合键的值进行排序。
SORT命令的实现原理是(以SORT numbers为例):

  1. 创建一个和要排序的对象numbers长度相同的数组,该数组的每个项都是一个redis.h/redisSortObject结构。
  2. 遍历数组,将各个数组项的obj指针分别指向numbers列表的各个项,构成obj指针和列表项之间一对一关系
  3. 遍历数组,将各个obj指针所指向的列表项转换成一个double类型的浮点数,并将这个浮点数保存在相应数组项的u.score属性里面
  4. 根据数组项u.score属性的值,对数组进行数字值排序(快速排序算法),排序后的数组项按u.score属性的值从小到大排列
  5. 遍历数组,将各个数组项的obj指针所指向的列表项作为排序结果返回给客户端。

其他的排序方式,比如按照字母顺序排列,降序排列,通过外部键进行排序等原理都差不多,变化的是排列的顺序,排列的依据u.score不一样。
更多SORT命令的具体使用和参数可以参考文档:Redis SORT命令

二进制位数组

Redis提供了SETBIT,GETBIT, BITCOUNT, BITOP四个命令用于处理二进制位数组(bit array, 又称为”位数组”)
Redis使用字符串对象来表示位数组,因为字符串对象使用的SDS数据结构是二进制安全的,所以程序可以直接使用SDS结构来保存位数组,并使用SDS结构的操作函数来处理位数组。
具体使用方法参考官方文档。

慢查询日志

Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。
服务器有两个和慢查询有关的选项:

  • slowlog-log-slower-than选项执行执行时间超过多少微秒的命令请求会被记录到日志上。(可以通过CONFIG SET slowlog-log-slower-than N设置)
  • slowlog-max-len选项执行服务器最多保存多少条慢查询日志。(可以通过CONFIG SET slowlog-max-len N设置)

使用SLOWLOG GET命令可以查看服务器所保存的慢查询日志, 使用SLOWLOG LEN可以查看当前日志的数量。

监视器

通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印服务器当前处理的命令请求的相关信息。当一个客户端使用MONITOR向服务器发送命令时,这个客户端的REDIS_MONITOR标识会被打开,并且客户端本身会被服务器添加到monitors链表的表尾。当服务器每次接收到请求时(处理命令之前), 都会调用replicationFeedMonitors函数,由这个函数将被处理的命令请求的相关信息发送给各个监视器。