1 发布与订阅
Redis提供了发布订阅功能,可以用于消息的传输。Redis的发布订阅机制包括三个部分,publisher,subscriber和Channel发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
1.1 频道/模式的订阅与退订命令
subscribe:订阅 subscribe channel1 channel2 ..publish:发布消息 publish channel messageunsubscribe:退订 channelpsubscribe :模式匹配 psubscribe +模式punsubscribe 退订模式
1.2 发布订阅的机制
订阅某个频道或模式:
客户端(client):
属性为pubsub_channels,该属性表明了该客户端订阅的所有频道
属性为pubsub_patterns,该属性表示该客户端订阅的所有模式
服务器端(RedisServer):
属性为pubsub_channels,该服务器端中的所有频道以及订阅了这个频道的客户端
属性为pubsub_patterns,该服务器端中的所有模式和订阅了这些模式的客户端
typedef struct redisClient {
...
dict *pubsub_channels; //该client订阅的channels,以channel为key用dict的方式组织
list *pubsub_patterns; //该client订阅的pattern,以list的方式组织
...
} redisClient;
struct redisServer {
...
dict *pubsub_channels; //redis server进程中维护的channel dict,它以channel 为key,订阅channel的client list为value
list *pubsub_patterns; //redis server进程中维护的pattern list 列表结构
int notify_keyspace_events;
...
};
当客户端向某个频道发送消息时,Redis首先在redisServer中的pubsub_channels中找出键为该频道的结点,遍历该结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端。然后,遍历结构体redisServer中的pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。在Redis哨兵模式中,哨兵通过发布与订阅的方式与Redis主服务器和Redis从服务器进行通信。Redisson是一个分布式锁框架,在Redisson分布式锁释放的时候,是使用发布与订阅的方式通知的。
2 事务
指作为单个逻辑工作单元执行的一系列操作
2.1 Redis事务
-
Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
-
Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
-
Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
-
Redis不支持回滚操作
2.1 事务命令
multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列 exec:执行命令队列 discard:清除命令队列 watch:监视key unwatch:清除监视key
2.2 事务机制
2.2.1 事务执行
- 事务开始
在RedisClient中,有属性flflags,用来表示是否在事务中flflags=REDIS_MULTI
2. 命令入队
RedisClient将命令存放在事务队列中(EXEC,DISCARD,WATCH,MULTI除外)
3. 事务队列
multiCmd *commands 用于存放命令
4. 执行事务
RedisClient向服务器端发送exec命令,RedisServer会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。如果某条命令在入队过程中发生错误,redisClient将flflags置为REDIS_DIRTY_EXEC,EXEC命令将会失败返回
typedef struct redisClient{
// flags
int flags //状态
// 事务状态
multiState mstate;
// .....
}redisClient;
// 事务状态
typedef struct multiState{
// 事务队列,FIFO顺序 // 是一个数组,先入队的命令在前,后入队在后
multiCmd *commands; // 已入队命令数
int count;
}multiState;
// 事务队列
typedef struct multiCmd{
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
}multiCmd;
2.2.2 watch的执行
使用WATCH命令监视数据库键,redisDb有一个watched_keys字典,key是某个被监视的数据的key,值是一个链表.记录了所有监视这个数据的客户端。监视机制的触发,当修改数据后,监视这个数据的客户端的flflags置为REDIS_DIRTY_CAS事务执行RedisClient向服务器端发送exec命令,服务器判断RedisClient的flflags,如果为REDIS_DIRTY_CAS,则清空事务队列。
typedef struct redisDb{ // ..... // 正在被WcATCH命令监视的键
dict *watched_keys; // .....
}redisDb;
2.2.3 Redis的弱事务性
- Redis语法错误
整个事务的命令在队列里都清除
flflags=multi_dirty
- Redis运行错误
在队列里正确的命令可以执行 (弱事务性)
弱事务性 :
-
在队列里正确的命令可以执行 (非原子操作)
-
不支持回滚
- Redis不支持事务回滚(为什么呢)
-
大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
-
Redis为了性能方面就忽略了事务回滚。 (回滚记录历史版本)
3 慢查询日志
3.1 慢查询设置
在redis.conf中可以配置和慢查询日志相关的选项:
#执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录
slowlog-log-slower-than 10000
#slowlog-max-len 存储慢查询日志条数
slowlog-max-len 128
Redis使用列表存储慢查询日志,采用队列方式(FIFO)
confifig set的方式可以临时设置,redis重启后就无效
confifig set slowlog-log-slower-than 微秒
confifig set slowlog-max-len 条数
slowlog get [n] 查看日志
3.2 慢查询记录的保存
在redisServer中保存和慢查询日志相关的信息
struct redisServer { // ...
// 下一条慢查询日志的 ID
long long slowlog_entry_id;
// 保存了所有慢查询日志的链表 FIFO
list *slowlog;
// 服务器配置 slowlog-log-slower-than 选项的值
long long slowlog_log_slower_than; // 服务器配置 slowlog-max-len 选项的值
unsigned long slowlog_max_len;// ...
};
lowlog 链表保存了服务器中的所有慢查询日志, 链表中的每个节点都保存了一个 slowlogEntry 结构, 每个 slowlogEntry 结构代表一条慢查询日志。
typedef struct slowlogEntry {
// 唯一标识符
long long id;
// 命令执行时的时间,格式为 UNIX 时间戳
time_t time;
// 执行命令消耗的时间,以微秒为单位
long long duration;
// 命令与命令参数
robj **argv;
// 命令与命令参数的数量
int argc;
} slowlogEntry;
3.3 慢查询日志的阅览&删除
初始化日志列表
void slowlogInit(void) {
server.slowlog = listCreate(); /* 创建一个list列表 */
server.slowlog_entry_id = 0; /* 日志ID从0开始 */
listSetFreeMethod(server.slowlog,slowlogFreeEntry); /* 指定慢查询日志list空间 的释放方法 */
}
获得慢查询日志记录 slowlog get [n]
def SLOWLOG_GET(number=None):
# 用户没有给定 number 参数
# 那么打印服务器包含的全部慢查询日志
if number is None: number = SLOWLOG_LEN()
# 遍历服务器中的慢查询日志
for log in redisServer.slowlog:
if number <= 0:
# 打印的日志数量已经足够,跳出循环
break else:
# 继续打印,将计数器的值减一
number -= 1
# 打印日志
printLog(log)
查看日志数量的 slowlog len
def SLOWLOG_LEN():
# slowlog 链表的长度就是慢查询日志的条目数量
return len(redisServer.slowlog)
清除日志 slowlog reset
def SLOWLOG_RESET():
# 遍历服务器中的所有慢查询日志
for log in redisServer.slowlog:
# 删除日志
deleteLog(log)
3.4 添加日志实现
在每次执行命令的之前和之后, 程序都会记录微秒格式的当前 UNIX 时间戳, 这两个时间戳之间的差就是服务器执行命令所耗费的时长, 服务器会将这个时长作为参数之一传给slowlogPushEntryIfNeeded 函数, 而 slowlogPushEntryIfNeeded 函数则负责检查是否需要为这次执行的命令创建慢查询日志
// 记录执行命令前的时间
before = unixtime_now_in_us()
//执行命令
execute_command(argv, argc, client)
//记录执行命令后的时间
after = unixtime_now_in_us()
// 检查是否需要创建新的慢查询日志
slowlogPushEntryIfNeeded(argv, argc, before-after)
void slowlogPushEntryIfNeeded(robj **argv, int argc, long long duration) {
if (server.slowlog_log_slower_than < 0) return; /* Slowlog disabled */ /* 负 数表示禁用 */
if (duration >= server.slowlog_log_slower_than) /* 如果执行时间 > 指定阈值*/
listAddNodeHead(server.slowlog,slowlogCreateEntry(argv,argc,duration)); /* 创建一个slowlogEntry对象,添加到列表首部*/
while (listLength(server.slowlog) > server.slowlog_max_len) /* 如果列表长度 > 指定长度 */
listDelNode(server.slowlog,listLast(server.slowlog)); /* 移除列表尾部元素 */}
slowlogPushEntryIfNeeded 函数的作用有两个:
-
检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置的时间, 如果是的话, 就为命令创建一个新的日志, 并将新日志添加到 slowlog 链表的表头。
-
检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度, 如果是的话, 那么将多出来的日志从 slowlog 链表中删除掉。
3.5 慢查询定位&处理
使用slowlog get 可以获得执行较慢的redis命令,针对该命令可以进行优化:
1、尽量使用短的key,对于value有些也可精简,能使用int就int。
2、避免使用keys *、hgetall等全量操作。
3、减少大key的存取,打散为小key
4、将rdb改为aof模式 ,rdb fork 子进程 主进程阻塞 redis大幅下降。关闭持久化(适合于数据量较小)。 改aof 命令式
5、想要一次添加多条数据的时候可以使用管道
6、尽可能地使用哈希存储
7、尽量限制下redis使用的内存大小,这样可以避免redis使用swap分区或者出现OOM错误
内存与硬盘的swap
4 监控器
Redis客户端通过执行MONITOR命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理的命令请求的相关信息。此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。
客户端1:
127.0.0.1:6379> monitor
OK
1589706136.030138 [0 127.0.0.1:42907] "COMMAND"
1589706145.763523 [0 127.0.0.1:42907] "set" "name:10" "zhaoyun"
1589706163.756312 [0 127.0.0.1:42907] "get" "name:10"
客户端2:
127.0.0.1:6379>
127.0.0.1:6379> set name:10 zhaoyun
OK
127.0.0.1:6379> get name:10
"zhaoyun"
4.1 实现监视器
redisServer 维护一个 monitors 的链表,记录自己的监视器,每次收到 MONITOR 命令之后,将客户端追加到链表尾。
void monitorCommand(redisClient *c) {
/* ignore MONITOR if already slave or in monitor mode */
if (c->flags & REDIS_SLAVE)
return; c->flags |= (REDIS_SLAVE|REDIS_MONITOR);
listAddNodeTail(server.monitors,c);
addReply(c,shared.ok); //回复OK
}
4.2 向监视器发送命令信息
call 主要调用了 replicationFeedMonitors ,这个函数的作用就是将命令打包为协议,发送给监视器。
// call() 函数是执行命令的核心函数,这里只看监视器部分 /*src/redis.c/call*/ /* Call() is the core of Redis execution of a command */
void call(redisClient *c, int flags) {
long long dirty,
start = ustime(), duration;
int client_old_flags = c->flags; /* Sent the command to clients in MONITOR mode, only if the commands are * not generated from reading an AOF. */
if (listLength(server.monitors) && !server.loading && !(c->cmd->flags & REDIS_CMD_SKIP_MONITOR)) {
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
......
}
4.3 Redis监控平台
grafana、prometheus以及redis_exporter。
Grafana 是一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形化选项,可以混合多种风格,支持多个数据源特点。
Prometheus是一个开源的服务监控系统,它通过HTTP协议从远程的机器收集数据并存储在本地的时序数据库上。
redis_exporter为Prometheus提供了redis指标的导出,配合Prometheus以及grafana进行可视化及监控。