redis的发布和订阅功能由PUBLISH、 SUBSCRIBE、PSUBSCRIBE等命令组成。
(一)频道的订阅与退订
服务器状态redisServer的pubsub_channels字典里面存储着频道的订阅关系,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里记录了所有订阅这个频道的客户端。
1、订阅频道
每当客户端执行SUBSCRIBE命令订阅某个或某些频道的时候,服务器就会将客户端与被订阅的频道在pubsub_channels字典进行关联。
2、退订频道
UNSUBSCRIBE命令与SUBSCRIBE命令正好相反,当一个客户端退订某个或者某些频道的时候,服务器将从pubsub_channels字典解除客户端与被退订频道之间的关联。
(二)模式的订阅与退订
服务器状态将所有模式订阅关系都保存在pubsub_patterns属性里面,pubsub_patterns属是一个链表,链表的每个节点都包含着一个pubsubPattern结构,这个结构的pattern属性记录了被订阅的模式,而client属性则记录了订阅模式的客户端。
1、订阅模式
每当客户端执行一个PSUBSCRIBE命令时,服务器新建一个pubsubPattern结构体,将pattern属性置为被订阅的模式,而client属性则置为订阅模式的客户端,然后将pubsubPattern结构添加到pubsub_patterns的链表尾。
2、退订模式
PUNSUBSCRIBE和PSUBSCRIBE正好相反,当一个客户端退订某个或者某些模式的时候,服务器在pubsub_patterns链表中查找并删除那些pattern属性为被订阅的模式,而client属性为订阅模式的客户端的pubsubPattern结构。、
(三)发送消息
当一个redis客户端执行PUBLISH命令将消息发送给频道channel的时候,服务器会将消息发送给channel频道的所有订阅者,同时如果有一个或多个模式的pattern与频道channel相匹配,那么消息message也会发送给pattern模式的订阅者。
1、将消息发送给频道订阅者
2、将消息发送给模式订阅者
(四)查看订阅信息
客户端可以通过命令PUBSUB查看频道或者模式的相关信息,比如某个频道目前有多少订阅者,又或者某个模式目前有多少订阅者等。
1、PUBSUB CHANNELS (pattern)
2、PUBSUB NUMSUB channel…(返回任意多个频道的订阅者数量)
3、PUBSUB NUMPAT(返回被订阅模式的数量)
redis通过MUTI、EXEC、WATCH等命令来实现事务(transaction)功能,事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后再去处理客户端的命令请求。
(一)事务的实现
事务从开始到结束通常会经历以下三个阶段:事务开始、命令入队、事务执行。
1、事务开始
MULTI命令的执行标志着事务的开始:MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,通过客户端状态的flags属性中打开REDIS_MULTI标识来完成。
2、命令入队
当一个客户端在事务状态的时候,客户端发送的命令为EXEC、DISCARD、WATCH、MULTI四个命令的其中一个,那么服务器立即执行这个命令;如果发送的命令时除此之外的命令,那么服务器不会立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。
3、事务队列
每个客户端状态都有自己的事务状态,这个事务状态保存在客户端状态的mstate(multiState mstate)属性里面,事务状态multiState结构包含一个事务队列(multiCmd * commands,FIFO顺序),以及一个已入队命令的计数器;multiCmd结构包含命令实现函数的指针(redisCommand * )、命令的参数以及参数的个数。
4、执行事务
当一个事务状态的客户端发送EXEC命令时,这个EXEC命令将立即被服务器执行,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行结果全部返回给客户端。
(二)WATCH命令的实现
WATCH命令是个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复(nil)。
1、使用WATCH命令监视数据库键
每个redisshjk都保存着一个watch_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值是一个链表,链表记录了所有监视相应数据库键的客户端。
2、监视机制的触发
所有执行数据库修改的命令之后,都会调用multi.c/touchWatchKey函数对watch_keys进行检查,查看是否有客户端在监视刚刚被命令修改过的键,如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,标识该客户端的事务安全性已经被破坏。
3、判断事务是否安全
当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务。
4、一个完整的WATCH事务执行过程
WATCH命令,更新数据库watch_keys字典,MULTI命令,另一客户端修改watch的数据库键,当前客户端的REDIS_DIRTY_CAS标识被打开,EXEC命令,服务器检查REDIS_DIRTY_CAS标识被打开拒绝执行事务。
(三)事务的ACID性质
传统的关系型数据库中,常常用ACID性质来检验事务功能的可靠性和安全性。
在redis中,事务总是具有原子性(atomicity)、一致性(consistency)和隔离性(isolation),并且当redis运行在某种特定的持久化模式下时,事务也具有耐久性(durability)。
1、原子性
事务具有原子性指的是,数据库将事务中的多个操作当做一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作都不执行。
redis事务和传统的关系型数据库事务的最大区别在于,redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务中的所有命令执行完毕为止。这种错误通常出现在开发环境中,而很少会在实际的生产环境中出现,并且事务的回滚功能复杂,影响redis简单高效的主旨,所以开发者认为没有必要为redis开发事务回滚功能。
2、一致性
事务一致性指的是如果数据库在执行事务之前是一致的,那么执行事务之后,无论事务执行是否成功,数据库也应该仍然是一致的,一致指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。
redis通过谨慎的错误检测和简单的设计来保证事务的一致性,从而确保事务的一致性。
1)入队错误
一个事务在入队命令的过程中,出现了命令不存在或者命令格式不正确等情况,那么redis将拒绝执行这个事务,因此redis事务的一致性不会被带入有入队错误的事务影响。
2)执行错误
事务执行的过程中也可能发生错误,出错的命令会被服务器识别出来,并进行相应的错误处理,所以这些出错命令不会对数据库做任何修改,也不会对事务的一致性产生任何影响。
3)服务器停机
如果在事务执行过程中redis服务器停机,那么根据服务器的持久化模式可能出现以下情况:如果服务器无持久化,那么服务器重启数据库将是空白的,因此数据时一致的;如果服务器运行在RDB或者AOF模式下,那么事务在中途停机不会导致不一致性,因为重启之后会根据RDB和AOF文件还原到一个一致的状态,找不到对应文件的话数据库将是空白的。
3、隔离性
事务的隔离是指即使数据库中有多个事务并发地执行,各个事务之间也不会相互影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
因为redis使用单线程的方式来执行事务,并且服务器保证,在执行事务期间不会对事务进行中断,因此redis事务总是以串行的方式运行的,并且事务也总是具有隔离性的。
4、耐久性
事务的耐久性是指事务的执行结果永远保存到永久性存储介质里面,即使服务器在事务执行完毕之后停机,执行事务所得结果也不会丢失。redis的事务只是简单地用队列包裹了一组redis命令,redis没有为事务提供额外的持久化功能,因此redis事务的耐久性是由redis所使用的持久化模式决定的。
redis引入了对lua脚本的支持,通过在服务器中嵌入Lua环境,redis客户端可以使用lua脚本,直接在服务端原子地执行多个redis命令。EVAL、EVALSHA、EVALSHA、SCRIPT EXISTS、SCRIPT FLUSH、SCRIPT KILL、SCRIPT LOAD。
(一)创建并修改Lua环境
redis服务器启动的时候创建一个基础的lua环境;载入多个函数库到lua环境里面,让lua脚本可以使用这些函数库来进行数据操作;创建全局表格redis,这个表格包含了对redis进行操作的函数;使用redis自制的随机函数来替换lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用;创建排序辅助函数,Lua环境使用这个辅助函数来对一部分redis命令的结果进行排序,从而消除这些命令的不确定性;创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息;对lua环境中的全局环境进行保护,防止用户在执行Lua脚本的过程中,将额外的全局变量添加到Lua环境中;将完成修改的Lua环境保存到服务器状态的lua属性中,等待执行服务器传来的lua脚本。
1、创建Lua环境
2、载入函数库
3、创建redis全局表格
4、使用redis自制的随机函数来替换Lua原有的随机函数
为了保证相同的脚本在不同的机器上产生相同的结果,redis要求传入的脚本和Lua环境中的所有函数都是无副作用的纯函数,Lua环境的math函数库用于生成随机数的math.random函数和math.randomseed函数都是带有副作用的,因此redis用自制的函数替换了math库中原有的随机函数。
5、创建排序辅助函数
另一个可能产生不一致数据的地方是那些带有不确定性质的命令,包括:SINTER、SUNION、SDIFF、SMEMBERS、HKEYS、HVALS、KEYS。服务器会为Lua环境创建一个排序辅助函数redis_compare_helper,当Lua脚本执行完一个带有不确定性的命令之后,程序会使用redis_compare_helper作为对比函数,自动调用table.sort函数对此命令的返回值做一次排序,以此来保证相同数据集总是产生相同的输出。
6、创建redis.pcall函数的错误报告辅助函数
7、保护Lua的全局环境
禁止脚本创建全局变量,获取一个不存在全局变量会引发错误,但是并未禁止用户修改已存在的去全局变量,所以执行Lua脚本时要小心,以免错误的修改了已存在的全局变量。
8、将Lua环境保存到服务器状态的Lua属性里面
修改工作结束,redis服务器会将Lua环境和服务器状态的lua属性关联起来,在任何特定的时间里,最多只有一个脚本被放到Lua环境中运行,因为redis时串行的。
(二)Lua环境协作组件
除了创建并修改Lua环境之外,redis服务器还创建了两个用于与Lua环境进行协作的组件,它们分别是复制执行Lua脚本中的redis命令的伪客户端,以及用于保护Lua脚本的lua_script字典。
1、伪客户端
2、lua_script字典
服务器状态的这个字典的键是某个Lua脚本的SHA1校验和(checksum),而字典的值是SHA1校验和对应的Lua脚本。一个作用是用于实现SCRIPT EXSITS命令,另一个是实现脚本的复制功能。
(三)EVAL命令的实现
步骤:根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数;将客户端给定的脚本保存到lua_script字典,等待将来进一步使用;执行刚刚在lua环境中定义的函数,以此来执行客户端给定的lua脚本。
1、定义脚本函数
服务器在Lua环境中,为传入的脚本定义一个与这个脚本相对应的Lua函数,其中函数名字是f_前缀加上SHA1校验和(四十个字符长)组成,而函数的体(body)则是脚本本身。
好处:执行脚本简单,直接调用函数即可;通过函数的局部性来让Lua环境保持清洁,减少了垃圾的回收工作量,并且避免了使用全局变量;如果某个脚本的函数在lua环境中被定义至少一次,那么只要记得这个函数的SHA1校验和,直接调用这个脚本来执行,这是EVALSHA命令的实现原理。
2、将脚本保存到 lua_script字典
3、执行脚本函数
步骤如下:
1)将EVAL命令中传入的键名参数和脚本参数 分别保存到KEYS数组和ARGV数组,然后将这两个数组作为全局变量传入到Lua环境里面;
2)为Lua环境装载超时钩子,这个钩子可以在脚本出现超时运行情况时,让客户端通过SCRIPT KILL命令停止脚本,或者通过SHUTDOWN命令直接关闭服务器;
3)执行脚本函数;
4)移除之前装载的超时钩子;
5)将执行脚本函数得到的结果保存到客户端状态的输出缓冲区里面,等待服务器将结果返回给客户端;
6)对Lua环境执行垃圾回收操作。
(四)EVALSHA命令的实现
根据校验和去找到函数进行执行,从而达到执行脚本的目的。
(五)脚本管理命令的实现
redis与脚本有关的命令还有四个,SCRIPT EXISTS、SCRIPT FLUSH、SCRIPT KILL、SCRIPT LOAD。
1、SCRIPT FLUSH
SCRIPT FLUSH命令用于清除服务器中所有和Lua脚本有关的信息,这个命令会先释放并重建lua_script字典,关闭现有的Lua环境并重建一个新的Lua环境。
2、SCRIPT EXISTS
SCRIPT EXISTS命令根据输入的SHA1校验和,检查校验和对应的脚本是否存在于服务器中,具体是通过检查给定的校验和是否存在于lua_script字典来实现的。允许一次传入多个SHA1校验和。
3、SCRIPT LOAD
SCRIPT LOAD命令和EVAL命令执行脚本时所做的前两步完全一样:创建脚本函数,保存脚本到lua_script字典里面。
4、SCRIPT KILL
如果服务器设置了lua-time-limit配置选项,那么在每次执行lua脚本之前,服务器都会在Lua环境里面设置一个超时处理钩子(hook)。服务器会定期调用钩子检查脚本是否超时运行,如果是的话就查看是都有SCRIPT KILL(脚本未执行写入操作)或者SHUTDOWN NOSAVE(脚本执行写入操作之后,就只能调用此命令)命令达到,有则执行,没有则继续执行脚本。
(六)脚本复制
与其他普通redis命令一样,当服务器运行在复制模式之下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包括:EVAL、EVALSHA、SCRIPT FLUSH、SCRIPT LOAD命令。
1、复制EVAL、SCRIPT FLUSH、SCRIPT LOAD命令
这三个命令和其他普通的redis命令一样,主服务器执行完之后,被执行的命令会传播给所有从服务器。
2、复制EVALSHA命令
EVALSHA命令比较特殊,有的从服务可能是新的,没有载入脚本,这个时候如果复制EVALSHA命令来执行则会出错,为了防止这种情况,redis要求主服务器在传播EVALSHA命令的时候,必须确保EVALSHA命令要执行的脚本已经被从服务器载入过,如果不能确保这一点,主服务器会将EVALSHA命令装换成一个等价的EVAL命令,然后传播EVAL命令来代替EVALSHA命令。转换命令的时候,需要用到lua_script和repl_scriptcache_dict两个字典。
1)判断传播EVALSHA命令是否安全的方法
主服务器的服务器状态中的repl_scriptcache_dict字典记录了自己已经将哪些脚本传播给了所有服务器。字典的键是脚本的校验和,而字典的值则全部是NULL。
2)清空repl_scriptcache_dict字典
每当主服务器添加一个新的从服务器时,主服务器都会清空自己的repl_scriptcache_dict字典,这是因为随着新服务器的出现,repl_scriptcache_dict里面记录的脚本已经不在被所有从服务器载入过。
3)EVALSHA命令转换成EVAL命令的方法
通过EVALSHA命令指定的校验和,以及lua_script字典保存的脚本,服务器总可以将一个EVALSHA命令转换成EVAL命令。传播完EVAL命令之后,会将校验和添加到repl_scriptcache_dict字典,下次主服务器就可以传播EVALSHA命令而不用转换了。
4)传播EVALSHA命令的方法
redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。ASC、DESC、ALPHA、LIMIT、STORE、BY、GET选项。同时使用多个选项时,各个不同的选项的执行顺序,以及选项的执行顺序对排序结果所产生的影响。
(一)SORT < key> 命令的实现
详细步骤:创建一个和key列表长度相同的数组,该数组的每个选项是一个redis.h/redisSortObject结构;遍历数组,将各个数组项的obj指针指向key列表的各个项,构成一一对应关系;遍历数组,将obj所指的列表项转换成为一个double类型的浮点数,并将这个浮点数保存在u.score属性里面;根据u.score属性进行排序;遍历数组,将各个数组项的obj指针所指向的列表项作为排序结果返回给客户端,从数组索引0开始。
(二) ALPHA选项的实现
通过ALPHA选项,SORT命令可以对包含字符串值的键进行排序。
(三)ASC和DESC选项的实现
默认是升序排列,降序和升序都采用快速排序,降序数组和升序数组相反。
(四)BY选项的实现
SORT < key> BY < pattern>
(五)带有ALPHA选项的BY选项
SORT < key> BY < pattern> ALPHA
(六)LIMIT选项
LIMIT选项可以让SORT命令只返回其中一部分已排序的元素。
LIMIT < offset> < count>:offset参数表示要跳过的已排序元素的数量;count参数表示跳过给定数量的已排序元素之后要返回的已排序元素的个数。
(七)GET选项的实现
SORT命令总是返回被排序键本身所包含的元素,GET选项是根据排序的元素,返回与其模式匹配的其他某些键的值。
一个SORT命令可以带有多个GET选项。
(八)STORE选项的实现
默认情况下,SORT命令只向客户端返回排序结果,而不保存排序结果,通过STORE选项,我们可以将结果保存在指定的键里面,并在有需要的时候重用这个排序结果。
(九)多个选项的执行顺序
一个SORT命令请求通常会用到多个选项,而这些选项的执行顺序是有先后之分的。
1、选项的执行顺序
一个SORT命令的执行步骤:
1)排序:根据ALPHA、ASC或DESC、BY这几个选项进行排序
2)限制排序结果集的长度:LIMIT选项
3)获取外部键:GET选项
4)保存排序结果:STORE选项
2、选项的摆放顺序
如果有多个GET选项时,那么在调整选项的位置时,必须保证多个GET选项的摆放顺序不变,这才可以让排序结果集保持不变。
redis提供了SETBIT、GETBIT、BITCOUNT、BITOP(AND、OR、XOR、NOT)四个命令来处理二进制位数组。
(一)、位数组的表示
redis使用字符串对象来表示位数组,因为字符串对象使用的SDS数据机构是二进制安全的,所以程序直接使用SDS结构来保存位数组,并使用SDS结构的操作函数来处理位数组。
(二)、GETBIT命令的实现
GETBIT < bitarray> < offset>
首先根据offset/8,得到要获取的字节位置,再用offset%8+1,得到二进制位所在的位置,最后获取位数组在该位的值。
(三)、SETBIT命令的实现
1、SETBIT命令的执行示例:
同上
2、带扩展操作的SETBIT命令示例
(四)、BITCOUNT命令
BITCOUNT命令看上去的工作并不复杂,但实际上要实现高效的计算,需要用到一些精巧的算法。下面给出几种可能的实现算法,并最终给出BITCOUNT命令的实现原理。
1、二进制位统计算法:遍历法
遍历数组的每个二进制位,不高效。
2、二进制位统计算法:查表算法
建立位数组对应的表,效率就会成倍提升;但是并没有那么简单,因为查找表的实际效果会受到内存和缓存两方面因素的限制:空间换时间,节约的时间越多,内存花费就会越大,创建的表格越大,CPU缓存所能保存的内容相比整个表格的比例就越少,查表时出现缓存不命中(cache miss)的情况会越高,缓存的换入和换出就会越频繁,最终影响查表法的实际效率。因此,为了高效的BITCOUNT命令,我们需要一种不会带来内存压力、并且可以在一次检查中统计多个二进制位的算法。
3、二进制位统计算法:variable-precision SWAR算法
统计非0二进制位的数量,在数学上被称为计算汉明重量(hamming weight )。
通过位运算和移位,先统计每两个二进制为一组进行分组,各组的十进制就是该组的汉明重量;然后依次统计每四个、八个、为一组的汉明重量;然后程序*0x01010101将汉明重量汇聚到二进制位的最高八位,最后再移位将汉明重量移动到低八位,最终得出的值就是汉明重量。
swar函数是一个常数复杂度的操作,因此可以在循环里面多次调用,这样效率就会成倍提升,但是也是有极限的,一旦循环中处理的位数组的大小超过了缓存的大小,这种优化的效果就会降低并最终消失。
4、二进制位统计算法:redis的实现
两种算法:键长为8位的表,表中记录了从0000 0000到1111 1111在内的所有二进制位的汉明重量;variable-precision SWAR算法采用每次循环载入128个二进制位,然后调用四次32位variable-precision SWAR算法来计算128位二进制位的汉明重量。
5、BITOP命令的实现
AND、OR、XOR三个命令可以接受多个位数组作为输入,NOT命令只能接受一个位数组输入。
redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。
服务器配置有两个和慢查询相关的选项:slowlog-log-slower-than选项指定了执行时间超过多少微秒的命令请求会被记录到日志上;slowlog-max-len选项指定服务器最多保存多少条慢查询日志(先进先出)。
CONFIG SET slowlog-log-slower-than num;
CONFIG SET slowlog-max-len num;
SLOWLOG GET命令查看服务器保存的慢查询日志。
1、慢查询记录的保存
服务器状态中包含了几个和慢查询日志功能相关的属性:下一条慢查询日志的ID、所有慢查询日志的链表(slowlogEntry结构)、服务器配置的slowlog-log-slower-than和slowlog-max-len选项的值。
slowlogEntry结构:唯一标识符、命令执行时的时间戳、执行命令消耗的时间(微秒为单位)、命令与命令参数、命令与命令参数的数量。
2、慢查询日志的阅览和删除
SLOWLOG GET;SLOWLOG LEN;SLOWLOG RESET
3、添加新日志
每次执行命令的前后,程序会记录两个时间戳, 服务器把这两个时间之差传给slowlogPushEntryIfNeeded函数,函数会负责检查是否需要为这次执行的命令创建慢查询日志。慢查询功能未开启则直接返回,slowlogCreateEntry函数创建一个新的慢查询日志,并将下一条慢查询日志的ID增1。
通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接受并打印服务器当前处理的命令请求的相关信息,每当一个客户端执行一条命令之后,服务器除了执行这条命令,还会将这条命令请求的信息发送给所有的监视器。
1、成为监视器
客户端标识REDIS_MONITOR打开,将客户端添加到服务器状态的monitors链表的末尾。
2、向监视器发送命令信息
服务器在每次处理命令请求之前,都会调用replicationFeedMonitors函数,由这个函数将被处理的请求的相关信息发送给所有监视器。