Redis 通过 MULTI 、DISCARD 、EXEC 和 WATCH 四个命令来实现事务功能
MULTI 开始一个事务,然后将多个命令入队到事务中,最后由 EXEC 命令触发事务,一并执行事务中的所有命令;一个事务从开始到执行会经历以下三个阶段:
REDIS_MULTI
127.0.0.1:6379> multi
OK
## 此时客户端开启了事务
127.0.0.1:6379> set mykey aaa
QUEUED
127.0.0.1:6379> set mykey1 bbb
QUEUED
进入一个事务队列的数组,数组元素的字段有
事务队列是一个数组,每个数组项是都包含三个属性:
命令EXEC 触发执行事务队列的命令 FIFO 方式执行在队列中的命令;结果会以 FIFO 的顺序保存到一个回复队列中
区别:
1.非事务命令是以单个命令为执行单位,前一个命令和后一个命令客户端可以不是同一个,事务情况下,执行单位是一个事务内部的命令,除非执行完成,否则不会中断执行,也不会执行其他客户端的命令;
2.非事务状态下命令的响应立刻返回,而事务状态下所有都执行完成并且放入回复队列,作为EXEC 的响应一起放回;
DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,然后将客户端从事务状态调整回非事务状态,最后返回字符串 OK 给客户端,说明事务已被取消
不支持子事务
响应是一个错误,然后继续等待其他命令的入队。MULTI 命令的发送不会造成整个事务失败,也不会修改事务队列中已有的数据
只能在客户端进入事务状态之前执行,在事务状态下发送 WATCH 命令会引发一个错误,但它不会造成整个事务失败,也不会修改事务队列中已有的数据
WATCH 命令用于在事务开始之前监视任意数量的键:当调用 EXEC 命令执行事务时,如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行,直接返回失败。
时间 | 客户端1 | 客户端2 |
---|---|---|
T1 | watch mykey | |
T2 | multi | |
T3 | set mykey qwer | |
T4 | set mykey bbb | |
T5 | exec 执行失败 |
类似于CAS 只有数据是原来的 我才set (修改)
所有开启事务的客户端所用到的key 会创建一个watched_keys 类似于
比如客户端client5 修改 的key1 被其他的客户端修改 ,对应的客户端 2 ,5 ,1,的REDIS_DIRTY_CAS 参数就会被打开
如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败
如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务
Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)
原子性(Atomicity)
要不全成功,要么全失败;单个命令是原子的,但是在有事务的情况下是非原子的操作;事务失败的情况下,不会重试和回滚;
一致性(Consistency)
事务开始之前和事务结束以后,数据库的完整性没有被破坏
执行前:不正确入队命令的事务不会被执行,也不会影响数据库的一致性。
执行中:不会引起事务中断或整个失败,不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响
执行中被KILL:
隔离性(Isolation)
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的
持久性(Durability)
因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定;
Redis 通过 PUBLISH 、SUBSCRIBE 等命令实现了订阅与发布模式;
Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道,每当有新信息发送到被订阅的频道时,信息就会被发送给所有订阅指定频道的客户端
这个消息就会被发送给订阅它的三个客户端
示例:
pubsub_channels 属性是一个字典,这个字典就用于保存订阅频道的信息
如图 :pubsub_channels 示例中,client2 、client5 和 client1 就订阅了 channel1
struct redisServer {
......
dict *pubsub_channels; /* Map channels to list of subscribed clients */
}
当客户端调用 SUBSCRIBE 命令时,程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。
用 PUBLISH channel message 命令,程序首先根据 channel 定位到字典的键,然后将信息发送给字典值链表中的所有客户端
struct redisServer {
......
dict *pubsub_patterns; /* A dict of pubsub_patterns */
}
当使用 PUBLISH 命令发送信息到某个频道时,不仅所有订阅该频道的客户端会收到信息,如果有某个/某些模式和这个频道匹配的话,那么所有订阅这个/这些频道的客户端也同样会收到信息
因为 PUBLISH 除了将 message 发送到所有订阅 channel 的客户端之外,它还会将 channel 和 pubsub_patterns 中的模式进行对比,如果 channel 和某个模式匹配的话,那么也将 message 发送到订阅那个模式的客户端
订阅信息由服务器进程维持的 redisServer.pubsub_channels 字典保存
订阅模式的信息由服务器进程维持的 redisServer.pubsub_patterns 链表保存
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脚本了
EVAL 命令的执行可以分为以下步骤:
慢查询日志是 Redis 提供的一个用于观察系统性能的功能
每条慢查询日志的保存格式
/* 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 都会用一个参数记录命令执行前的时间,在命令执行完之后,再计算一次当前时间,然后将两个时间值相减,得出执行命令所耗费的时间值 duration ,并将duration 传给 slowlogPushEntryIfNeed 函数。如果 duration 超过服务器设置的执行时间上限 server.slowlog_log_slower_than 的话,slowlogPushEntryIfNeed 就会创建一条新的慢查询日志,并将它加入到慢查询日志链表里
针对慢查询日志有三种操作,分别是查看、清空和获取日志数量;
slowlog get [n],获取慢查询日志列表,可指定返回条数
slowlog reset,清空慢查询日志列表
slowlog len,获取慢查询日志列表当前的长度