redis--独立功能

发布与订阅

Redis的发布与订阅由PUBLISH, SUBSCRIBE, PSUBSCRIBE等命令组成
订阅某频道后,
有人向频道发消息,
频道的所有订阅者均可收到此消息

模式匹配

频道的订阅与退订

Redis将所有频道的订阅关系都保存在
服务器状态的pubsub_channels字典里.
字典的键是某个被订阅的频道,
键的值是一个链表,链表里记录了所有订阅这个频道的客户端.
struct redisServer
{
    
    
	...
	dict *pubsub_channels;
	...
};
- 订阅频道
每当客户端执行SUBSCRIBE订阅某个或某些频道时,
服务器会将客户端与被订阅的频道在pubsub_channels字典中进行关联
- 退订频道

模式的订阅与退订

服务器也将所有模式的订阅关系都保存在服务器状态的pubsub_patterns
struct redisServer
{
    
    
	...
	list *pubsub_patterns;
	...
};

typedef struct pubsubPattern
{
    
    
	redisClient *client;
	robj *pattern;
}pubsubPattern;
- 订阅模式
- 退订模式

发送消息

当一个Redis客户端执行PUBLISH <channel> <message>
将消息message发给频道channel时,
服务器需执行以下两个动作
a.将消息message发给channel频道的所有订阅者
b.发给匹配模式的订阅者

查看订阅信息

- PUBSUB CHANNELS
PUBSUB CHANNELS [pattern]子命令用于返回服务器当前被订阅的频道
- PUBSUB NUMSUB
PUBSUB NUMSUB [channel-1 ... channel-n]
返回每个指定频道的订阅者数量
- PUBSUB NUMPAT
返回服务器当前被订阅模式的数量

事务

Redis通过MULTI,EXEC,WATCH等命令来实现事务功能
事务提供了一种将多个命令请求打包,
然后一次性,按顺序地执行多个命令的机制,
且在事务执行期间,
服务器不会中断事务而改去执行其他客户端的命令请求,
他会将事务中的所有命令都执行完毕,
然后才去处理其他客户端的命令请求.
redis>MULTI
redis>SET "name" "Practical Common Lisp"
redis>GET "name"
redis>SET "author" "Peter Seibel"
redis>GET "author"
redis>EXEC
MULTI和EXEC之间的部分构成的事务的内容

事务的实现

a.事务开始
b.命令入队
c.事务执行
- 事务开始
MULTI
让客户端状态的flags属性中打开REDIS_MULTI标识
- 命令入队
一个客户端处于非事务状态时,
客户端发送的命令会立即被服务器执行

当一个客户端切换到事务状态之后,
服务器会根据这个客户端发来的不同命令执行不同的操作
a.如客户端发送的命令为EXEC, DISCARD, WATCH, MULTI
四个其中一个,则服务器立即执行.
b.其它命令,将命令放入一个事务队列,
向客户端返回QUEUED回复.

- 事务队列
每个Redis客户端有自己的事务状态,
此事务状态保存在客户端状态的mstate属性里面
typedef struct redisClient
{
    
    
	...
	multiState mstate;
	...
} redisClient;

typedef struct multiState
{
    
    
	multiCmd *commands;
	int count;
} multiState;

typedef struct multiCmd
{
    
    
	robj **argv;
	int argc;
	struct redisCommand *cmd;
}multiCmd;
- 执行事务
当一个处于事务状态的客户端向服务器发EXEC命令时,
此EXEC命令将立即被服务器执行.
服务器会遍历此客户端的事务队列,
执行队列中保存的所有命令,
最后将执行命令所得的结果全部返回给客户端.

WATCH命令

WATCH命令是一个乐观锁,
它可以在EXEC命令执行前,
监视任意数量的数据库键,
并在EXEC命令执行时,
检查被监视的键是否至少有一个已经被修改过,
如是,服务器将拒绝执行事务.返回空回复.
redis>WATCH "name"
redis>MULTI
redis>SET "name" "peter"
redis>EXEC
- 使用WATCH命令监视数据库键
每个Redis数据库都保存着一个watched_keys字典,
这个字典的键是某个被WATCH命令监视的数据库键,
字典的值则是一个链表,
链表记录了所有监视相应数据库键的客户端
typedef struct redisDb
{
    
    
	...
	// 键为监视键值
	// 值是节点指针.[单链表结构,由所有监视此键的客户端组成]
	dict *watched_keys;
	...
}redisDb;
- 监视机制的触发
所有对数据库进行修改的命令,
在执行后都会对watched_keys字典进行检查,
查看是否有客户端正在监视刚刚被命令修改过的数据库键.
如有,则会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,
标识该客户端的事务安全性已经被破坏

- 判断事务是否安全
当服务器接收到一个客户端发来的EXEC命令时,
服务器会根据此客户端
是否打开REDIS_DIRTY_CAS标识来决定是否执行事务.
a.如客户端的REDIS_DIRTY_CAS标识已经被打开,
则说明客户端所监视的键当中,
至少有一个键已经被修改过,
服务器拒绝执行客户端提交的事务.
b.如客户端的REDIS_DIRTY_CAS标识没打开,
服务器执行提交的事务.

事务的ACID性质

在传统的关系式数据库中,
常用ACID性质来检验事务功能的可靠性和安全性
在Redis中,
事务总是有原子性,一致性,隔离性,
且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性.
- 原子性
数据库将事务中的多个操作当做一个整体来执行,
服务器要么就执行事务中的所有操作,
要么一个也不执行.

Redis事务和传统的关系型数据库事务最大区别在于,
Redis不支持事务回滚机制,
即使事务队列中的某个命令在执行期间出现了错误,
整个事务也会继续执行下去,
直到将事务队列中的所有命令执行完毕为止.

- 一致性
数据库在执行事务之前是一致的,
在事务执行后,
无论事务是否执行成功,
数据库也应该是一致的

一致指的是数据符合数据库本身的定义和要求,不含非法/无效/...数据.
a.入队错误
向事务入队一个不存在的命令,
所有事务EXEC时,所有内容被拒绝执行
b.执行错误
执行过程中发生的错误
会继续执行事务中余下的其他命令

事务执行的过程中,
出错的命令会被服务器识别出来,
并进行相应的错误处理,
故这些出错命令不会对数据库做任何修改,
也不会对事务的一致性产生任何影响

- 服务器停机
如Redis服务器在执行事务的过程中停机,
则根据服务器所使用的持久化模式,可能有以下:
a.如服务器运行在无持久化内存模式下,
则重启之后的数据库将是空白的,
因此数据总是一致的
b.如服务器运行在RDB模式下,
则在事务中途停止不会一致不一致性,
因为服务器可根据RDB恢复数据,
从而将数据库还原到一个一致的状态.
c.如服务器运行在AOF模式下,
则在事务中途停机不会导致不一致性,
因为服务器可根据现有的AOF文件来恢复数据,
从而将数据库还原到一个一致状态.

- 隔离性
数据库中多个事务并发执行,各个事务间不会互相影响,
并且在并发状态下执行的事务和串性执行的事务产生的结果完全相同

Redis使用单线程方式来执行事务[及事务队列中的命令],
且保证,在执行事务期间不会对事务进行中断.
故,Redis的事务总是以串行方式执行的.
具备隔离性.

- 耐久性
当一个事务执行完毕时,
执行这个事务所得的结果已经被保存到永久性存储介质里了.
即使服务器在事务执行完毕后停机,
执行事务所得的结果也不会丢失.

Redis的事务没有额外的持久化,
事务的持久性由Redis所使用的持久化模式决定
a.当服务器在无持久化模式下运作时,
事务无持久性,
一旦服务器停机,
包括事务数据在内的所有服务器数据都将丢失
b.当服务器在RDB持久化模式下运作时,
服务器只会在特定的保存条件被满足时,
才执行BGSAVE命令,对数据库进行保存,
且异步执行的BGSAVE不能保证事务数据被第一时间保存到硬盘,
故RDB持久化模式下的事务也不具有耐久性.
c.当服务器运行在AOF持久化模式下,
且appendfsync选项的值为always时,
程序总会在执行命令之后调同步函数,
将命令数据真正地保存到硬盘里,
故,此配置下的事务具有耐久性.
d.当服务器运行在AOF持久化模式下,
且appendfsync选项的值为everysec时,
每秒同步一次命令到硬盘,
故,不具备耐久性
e.当服务器运行在AOF持久化模式下,
且appendfsync选项为no时,
程序会交由操作系统来决定何时将命令数据同步到硬盘.
事务数据可能在等待同步过程丢失,此时不具有耐久性

不论Redis在什么模式下运作,
在一个事务的最后加上SAVE命令总是可保证事务的耐久性[效率低]

Lua脚本

通过在服务器中嵌入Lua环境,
Redis客户端可使用Lua脚本,
直接在服务器原子地执行多个Redis命令.

创建并修改Lua环境

Redis在服务器内嵌了一个Lua环境
并对此Lua环境进行了一系列修改,
从而确保此Lua环境可满足Redis服务器的需要.
a.创建一个基础的Lua环境,之后针对此环境修改
b.载入多个函数库到Lua环境,
让Lua脚本可使用这些函数库来进行数据操作
c.创建全局表格redis
此表格含对Redis进行操作的函数,
如用于在Lua脚本中执行Redis命令的redis.call
d.使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数
避免在脚本中引入副作用
e.创建排序辅助函数
f.创建redis.pcall函数的错误报告辅助函数
此函数可提供更详细的出错信息
g.对Lua环境的全局环境进行保护,
防止用户在执行Lua脚本过程中,
将额外的全局变量添加到Lua环境中
h.将完成修改的Lua环境保存到服务器状态的Lua属性中,
等待执行服务器传来的Lua脚本

- 创建Lua环境
// 创建基本Lua环境
lua_open
- 载入函数库
将以下函数库载入到Lua环境里面
a.基础库
含Lua的核心函数,为防止用户从外部文件引入不安全代码,
库中loadfile禁用
b.表格库
包含用于处理表格的通用函数
c.字符串库
包含用于处理字符串的通用函数
d.数学库
数学处理
e.调试库
调试支持
f.Lua CJSON库
用于处理UTF-8编码的JSON格式
g.Struct库
用于在Lua值和C结构之间进行转换
h.Lua cmsgpack库
用于处理MessagePack格式的数据

- 创建redis全局表格
服务器将在Lua环境中创建一个redis表格,
并将它设为全局变量.
此redis表格含
a.用于执行Redis命令的redis.call和redis.pcall
借助上述两个函数,可在Lua脚本执行Redis命令
b.用于记录Redis日志的redis.log函数,
及相应的日志级别常量
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
c.用于计算SHA1校验和的redis.sha1hex函数
d.用于返回错误信息的redis.error_reply和redis.status_reply

- 使用Redis自制的随机函数来替换Lua原有的随机函数
Redis要求所有传入服务器的Lua脚本,
及Lua环境中的所有函数,
需为无副作用的纯函数.
Redis使用自制的函数替换了math库中原有的
math.random函数和math.randomseed
替换后的两个函数有以下特征:
a.对相同的seed,math.random总产生相同的随机数序列
b.除非在脚本中用math.randomseed显式地修改seed,
否则,每次运行脚本时,
Lua环境用固定的math.randomseed(0)来初始化seed
	
- 创建排序辅助函数
- 创建redis.pcall函数的错误报告辅助函数
- 保护Lua的全局环境
- 将Lua环境保存到服务器状态的Lua属性里面

Lua环境协作组件

负责执行Lua脚本中Redis命令的伪客户端
用于保存Lua脚本的lua_scripts字典
- 伪客户端
为了执行Lua脚本中包含的Redis命令,
Redis服务器专门为Lua环境创建了一个伪客户端,
由此伪客户端负责处理Lua脚本中包含的所有Redis命令

Lua脚本用redis.call或redis.pcall执行一个Redis命令,
需完成以下步骤
a.Lua环境将redis.call或redis.pcall想执行的命令传给伪客户端
b.伪客户端将脚本想要执行的命令传给命令执行器
c.命令执行器执行伪客户端传给它的命令,
将命令的执行结果返回给伪客户端
d.伪客户端接收命令执行器返回的命令结果,
将此命令结果返回给Lua环境
e.Lua环境在接收到命令结果后,
将该结果返回给redis.call或redis.pcall
f.收到结果的redis.call或redis.pcall将命令结果作为函数返回值
返回给脚本中的调用者

- lua_scripts字典
除了伪客户端外,
Redis服务器为Lua环境创建的另一个协作组件是lua_scripts字典.
键为某个Lua脚本的SHA1校验和
值为SHA1校验和对应的Lua脚本
struct redisServer
{
    
    
	...
	dict *lua_scripts;
	...
};

EVAL命令的实现

EVAL命令的执行过程可分为以下三个步骤
a.根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数
b.将客户端给定的脚本保存到lua_scripts字典
等待将来进一步使用
c.执行刚刚在Lua环境中定义的函数,来执行客户端给定的Lua脚本

- 定义脚本函数
当客户端向服务器发EVAL命令,
要求执行某个Lua脚本时,
服务器首先要做的是在Lua环境中,
为传入的脚本定义一个与这个脚本对应的Lua函数
Lua函数名字由f_加脚本SHA1校验和组成
function f_xxx
	xxx
end
- 将脚本保存到lua_scripts字典
- 执行脚本函数
在为脚本定义函数,
且将脚本保存到lua_scripts字典后,
服务器还需进行一些设置钩子,传入参数之类的准备工作.

准备和执行脚本过程如下:
a.将EVAL命令中传入的键名参数和脚本参数保存到
KEYS数组和ARGV数组
将此两个数组作为全局变量传入到Lua环境里
b.为Lua环境装载超时处理钩子
此钩子可在脚本出现超时运行时,
让客户端通过SCRIPT KILL停止脚本,
或通过SHUTDOWN直接关闭服务器
c.执行脚本函数
d.移除之前装载的超时钩子
e.将执行脚本函数所得的结构保存到客户端状态的输出缓冲区里
等待服务器将结果返回给客户端
f.对Lua环境执行垃圾回收

EVALSHA命令的实现

每个被EVAL命令成功执行过的Lua脚本,
在Lua环境里有一个与此脚本相对应的Lua函数

脚本管理命令

- SCRIPT FLUSH
SCRIPT FLUSH命令用于清除服务器中所有和Lua脚本有关的信息,
此命令会释放并重建lua_scripts字典
关闭现有的Lua环境并重新创建一个新的Lua环境
- SCRIPT EXISTS
根据输入的SHA1校验和,检查校验和和对应的脚本释放存在于服务器中
- SCRIPT LOAD
- SCRIPT KILL

脚本复制

- 复制EVAL命令/SCRIPT FLUSH命令/SCRIPT LOAD命令

排序

a.排序
使用ALPHA/ASC/DESC/BY,对输入键进行排序,
得到一个排序结果集
b.限制排序结果集长度
LIMIT
c.获取外部键
GET
d.保存排序结果集
STORE
e.向客户端返回排序结果集

二进制位数组

Redis提供了SETBIT/GETBIT/BITCOUNT/BITOP四个命令用于
处理二进制位数组.

位数组的表示

Redis使用字符串对象来表示位数组,
因为字符串对象使用的SDS数据结构是二进制安全的

用SDS表示的,一字节长的位数组例子
a.redisObject.type为REDIS_STRING
b.sdshdr.len值为1
c.buf数组中的buf[0]字节保存了一个字节长的位数组
d.buf数组中buf[1]字节保存了SDS程序自动追加到值的末尾的空字'\0'

buf数组使用逆序来保存位数组,
 位数组
 1111 0000 1100 0011 1010 0101
 在buf数组中会被保存为
 1010 0101 1100 0011 0000 1111

慢查询日志

struct redisServer
{
    
    
	...
	long long slowlog_entry_id;
	list *slowlog;
	long long slowlog_log_slower_than;
	unsigned long slowlog_max_len;
	...
};

猜你喜欢

转载自blog.csdn.net/x13262608581/article/details/115196859