Redis(四). 事务,发布订阅,Lua,慢日志

Redis(四). 事务,发布订阅,Lua,慢日志

1. 事务

1.1 简介

Redis 通过 MULTIDISCARDEXECWATCH 四个命令来实现事务功能

MULTI 开始一个事务,然后将多个命令入队到事务中,最后由 EXEC 命令触发事务,一并执行事务中的所有命令;一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务。
  2. 命令入队。
  3. 执行事务。

1.2 开启事务

REDIS_MULTI

127.0.0.1:6379> multi
OK
## 此时客户端开启了事务

1.3 命令入队

127.0.0.1:6379> set mykey aaa
QUEUED
127.0.0.1:6379> set mykey1  bbb
QUEUED

Redis(四). 事务,发布订阅,Lua,慢日志_第1张图片

进入一个事务队列的数组,数组元素的字段有

事务队列是一个数组,每个数组项是都包含三个属性:

  1. 要执行的命令(cmd)。
  2. 命令的参数(argv)。
  3. 参数的个数(argc)。

1.4 执行事务

命令EXEC 触发执行事务队列的命令 FIFO 方式执行在队列中的命令;结果会以 FIFO 的顺序保存到一个回复队列中

1.5 事务和非事务

区别:

1.非事务命令是以单个命令为执行单位,前一个命令和后一个命令客户端可以不是同一个,事务情况下,执行单位是一个事务内部的命令,除非执行完成,否则不会中断执行,也不会执行其他客户端的命令;

2.非事务状态下命令的响应立刻返回,而事务状态下所有都执行完成并且放入回复队列,作为EXEC 的响应一起放回;

1.6 事务状态下的 DISCARD

DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,然后将客户端从事务状态调整回非事务状态,最后返回字符串 OK 给客户端,说明事务已被取消

1.7 事务状态下的 MULTI

不支持子事务

响应是一个错误,然后继续等待其他命令的入队。MULTI 命令的发送不会造成整个事务失败,也不会修改事务队列中已有的数据

1.8 事务状态下的WATCH

只能在客户端进入事务状态之前执行,在事务状态下发送 WATCH 命令会引发一个错误,但它不会造成整个事务失败,也不会修改事务队列中已有的数据

WATCH 命令用于在事务开始之前监视任意数量的键:当调用 EXEC 命令执行事务时,如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行,直接返回失败。

Redis(四). 事务,发布订阅,Lua,慢日志_第2张图片

时间 客户端1 客户端2
T1 watch mykey
T2 multi
T3 set mykey qwer
T4 set mykey bbb
T5 exec 执行失败

类似于CAS 只有数据是原来的 我才set (修改)

1.9 WATCH 实现

所有开启事务的客户端所用到的key 会创建一个watched_keys 类似于

Redis(四). 事务,发布订阅,Lua,慢日志_第3张图片

比如客户端client5 修改 的key1 被其他的客户端修改 ,对应的客户端 2 ,5 ,1,的REDIS_DIRTY_CAS 参数就会被打开

  • 如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败

  • 如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务

1.10 事务ACID

Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)

原子性(Atomicity)

要不全成功,要么全失败;单个命令是原子的,但是在有事务的情况下是非原子的操作;事务失败的情况下,不会重试和回滚;

一致性(Consistency)

事务开始之前和事务结束以后,数据库的完整性没有被破坏

执行前:不正确入队命令的事务不会被执行,也不会影响数据库的一致性。

执行中:不会引起事务中断或整个失败,不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响

执行中被KILL:

  • 内存模式下 没有设置 持久化 就重启之后的数据库总是空白的,所以数据总是一致的。
  • RDB 模式:事务执行之后,保存 RDB 的工作才有可能开始,可能保存的是最近一份的快照,数据可能不是最新的
  • AOF 模式:事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,但不一定是最新的;写入到 AOF 文件,并且 AOF 文件被成功保存,事务不完整,Redis 报错退出,需要工具溢出事务命令,恢复,而且数据也是最新的(直到事务执行之前为止)

隔离性(Isolation)

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的

持久性(Durability)

因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定;

  • 内存模式下,事务肯定是不持久的
  • RDB 模式下 ,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败 ,不持久的
  • AOF 的“总是 SYNC ”模式下,但是不是主线程执行的,也是不持久的 ,其他的差不多都是这样

1.11 总结

  • 事务执行 一次性,有序执行
  • 事务不对被中断,职务执行完成,才结束
  • 事务内部命令FIFO 结果也是FIFO
  • Redis 的事务保证了 ACID 中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)

2. 订阅发布

Redis 通过 PUBLISHSUBSCRIBE 等命令实现了订阅与发布模式;

2.1 频道的订阅与信息发送

Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道,每当有新信息发送到被订阅的频道时,信息就会被发送给所有订阅指定频道的客户端

Redis(四). 事务,发布订阅,Lua,慢日志_第4张图片

这个消息就会被发送给订阅它的三个客户端

示例:

Redis(四). 事务,发布订阅,Lua,慢日志_第5张图片

2.2 实现机制

pubsub_channels 属性是一个字典,这个字典就用于保存订阅频道的信息

如图 :pubsub_channels 示例中,client2 、client5 和 client1 就订阅了 channel1

struct redisServer {
    ......
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */
}

Redis(四). 事务,发布订阅,Lua,慢日志_第6张图片

当客户端调用 SUBSCRIBE 命令时,程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。

2.3 发送信息到频道

用 PUBLISH channel message 命令,程序首先根据 channel 定位到字典的键,然后将信息发送给字典值链表中的所有客户端

2.4 订阅模式

struct redisServer {
    ......
dict *pubsub_patterns;  /* A dict of pubsub_patterns */
}

当使用 PUBLISH 命令发送信息到某个频道时,不仅所有订阅该频道的客户端会收到信息,如果有某个/某些模式和这个频道匹配的话,那么所有订阅这个/这些频道的客户端也同样会收到信息

Redis(四). 事务,发布订阅,Lua,慢日志_第7张图片

Redis(四). 事务,发布订阅,Lua,慢日志_第8张图片

因为 PUBLISH 除了将 message 发送到所有订阅 channel 的客户端之外,它还会将 channel 和 pubsub_patterns 中的模式进行对比,如果 channel 和某个模式匹配的话,那么也将 message 发送到订阅那个模式的客户端

2.5 总结

  • 订阅信息由服务器进程维持的 redisServer.pubsub_channels 字典保存

  • 订阅模式的信息由服务器进程维持的 redisServer.pubsub_patterns 链表保存

3.Lua 脚本

3.1 脚本的安全性

redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK
//get_random_number 随机数

假如 EVAL 的代码被复制到了附属节点 SLAVE ,因为 get_random_number() 的随机性质,它有很大可能会生成一个不同的值;它破坏了服务器和附属节点数据之间的一致性

Redis 对 Lua 环境所能执行的脚本做了一个严格的限制——所有脚本都必须是无副作用的纯函数(pure function);

经过这一系列的调整之后,Redis 可以保证被执行的脚本:

无副作用。没有有害的随机性。对于同样的输入参数和数据集,总是产生相同的写入命令;

在脚本环境的初始化工作完成以后,Redis 就可以通过 EVAL 命令或 EVALSHA 命令执行 Lua脚本了

3.2 EVAL 命令的实现

EVAL 命令的执行可以分为以下步骤:

  1. 为输入脚本定义一个 Lua 函数。
  2. 执行这个 Lua 函数。

4. 慢查询日志

慢查询日志是 Redis 提供的一个用于观察系统性能的功能

4.1 相关数据结构

每条慢查询日志的保存格式

/* This structure defines an entry inside the slow log list */
typedef struct slowlogEntry {
    robj **argv;  /* 命令参数 */
    int argc;     /* 命令参数数量 */
    long long id;       /* 唯一标识符 Unique entry identifier. */
    long long duration; /* / 执行命令消耗的时间,以纳秒 Time spent by the query, in microseconds. */
    time_t time;        /* 命令执行时的时间 Unix time at which the query was executed. */
    sds cname;          /* Client name. */
    sds peerid;         /* Client network address. */
} slowlogEntry;
struct redisServer {
    .....
    list *slowlog;                  /* 保存慢查询日志的链表    SLOWLOG list of commands */
    long long slowlog_entry_id;     /* 慢查询日志的当前 id 值 SLOWLOG current entry ID */
    long long slowlog_log_slower_than; /* 慢查询时间限制 SLOWLOG time limit (to get logged) */
    unsigned long slowlog_max_len;     /* 慢查询日志的最大条目数量 SLOWLOG max number of items logged */
.....
}

slowlog 属性是一个链表,链表里的每个节点保存了一个慢查询日志结构,所有日志按添加时间从新到旧排序,新的日志在链表的左端,旧的日志在链表的右端。

slowlog_entry_id 在创建每条新的慢查询日志时增一,用于产生慢查询日志的 ID (这个 ID在执行 SLOWLOG RESET 之后会被重置)。

slowlog_log_slower_than 是用户指定的命令执行时间上限,执行时间大于等于这个值的命令会被慢查询日志记录。

slowlog_max_len 慢查询日志的最大数量,当日志数量等于这个值时,添加一条新日志会造成最旧的一条日志被删除。

Redis(四). 事务,发布订阅,Lua,慢日志_第9张图片

4.2 慢查询日志的记录

在每次执行命令之前,Redis 都会用一个参数记录命令执行前的时间,在命令执行完之后,再计算一次当前时间,然后将两个时间值相减,得出执行命令所耗费的时间值 duration ,并将duration 传给 slowlogPushEntryIfNeed 函数。如果 duration 超过服务器设置的执行时间上限 server.slowlog_log_slower_than 的话,slowlogPushEntryIfNeed 就会创建一条新的慢查询日志,并将它加入到慢查询日志链表里

4.3 查看

针对慢查询日志有三种操作,分别是查看、清空和获取日志数量;
slowlog get [n],获取慢查询日志列表,可指定返回条数
slowlog reset,清空慢查询日志列表
slowlog len,获取慢查询日志列表当前的长度

4.4 总结

  • Redis 用一个链表以 FIFO 的顺序保存着所有慢查询日志。
  • logPushEntryIfNeed 函数。如果 duration 超过服务器设置的执行时间上限 server.slowlog_log_slower_than 的话,slowlogPushEntryIfNeed 就会创建一条新的慢查询日志,并将它加入到慢查询日志链表里

你可能感兴趣的:(Redis,redis,redis,事务,redis,发布订阅)