Redis 缓存过期、淘汰策略 & 持久化 & 扩展功能

缓存过期、淘汰策略

下面主要研究 redis.conf 中的配置项 maxmemory 最大占用内存

用过 Redis 都知道 redis 的性能高,官方性能数据,读 110000次/s,写 81000次/s,但是性能再高也架不住长期使用且只写入(key会不断增加)、不淘汰(删除)

不设置的场景

  • Redis 的 key 是固定的,不会增加新的 key
  • 作为 DB 使用,保证数据的完整性,不能淘汰,可以做集群,横向扩展
  • 缓存淘汰策略:禁止驱逐 (默认)

设置的场景

Redis 作为缓存使用,物理内存在长期只写入的情况下很有可能会满,内存与硬盘交换(swap) 虚拟内存 ,频繁 IO 性能急剧下降,甚至崩溃,为了防止 redis 占用过多的内存对其他的应用程序造成影响,可以在 redis.conf 文件中通过设置 maxmemory 选项对 redis 所能够使用的最大内存做限制,并通过 maxmemory_policy 对 redis 占用内存超过 maxmemory 之后的行为(数据淘汰策略)做定制。

maxmemory 设置多少?与业务有关

1个 Redis 实例,保证系统运行 1G ,剩下的就都可以设置, Redis 可占物理内存的 3/4
如果是 slaver 节点需留出一定的内存在 redis.conf 中

maxmemory 1024mb
获得 maxmemory 数
CONFIG GET maxmemory

设置 maxmemory 后,当 maxmemory 趋近设置的值时,通过缓存淘汰策略,从内存中删除对象

Expire 数据结构

在 Redis 中可以使用 expire 命令设置一个键的存活时间 (ttl: time to live),过了这段时间,该键就会自动被删除。

expire 使用

expire 命令的使用方法如下: expire key ttl(单位秒)

127.0.0.1:6379> expire name 2 # 2秒失效 
(integer) 1 
127.0.0.1:6379> get name 
(nil) 
127.0.0.1:6379> set name zhangfei 
OK
127.0.0.1:6379> ttl name # 永久有效 
(integer) -1 
127.0.0.1:6379> expire name 30 # 30秒失效 
(integer) 1 
127.0.0.1:6379> ttl name # 还有24秒失效
(integer) 24 
127.0.0.1:6379> ttl name # 失效 
(integer) -2

expire 原理

typedef struct redisDb { 
	dict *dict; # key Value 
	dict *expires; # key ttl
	ttl dict *blocking_keys; 
	dict *ready_keys; 
	dict *watched_keys; 
	int id; 
} redisDb;

上面的代码是 Redis 中关于数据库的结构体定义,这个结构体定义中除了 id 以外都是指向字典的指针,其中我们只看 dict 和 expires。
dict 用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对
expires 则用于维护一个 Redis 数据库中设置了失效时间的键(即key与失效时间的映射)。

当我们使用 expire 命令设置一个 key 的失效时间时,Redis 首先到 dict 这个字典表中查找要设置的 key 是否存在,如果存在就将这个 key 和失效时间添加到 expires 这个字典表。

当我们使用 setex 命令向系统插入数据时,Redis 首先将 Key 和 Value 添加到 dict 这个字典表中,然后将 Key 和失效时间添加到 expires 这个字典表中。

简单地总结来说就是,设置了失效时间的 key 和具体的失效时间全部都维护在 expires 这个字典表中。

删除策略

Redis 的数据删除有定时删除、惰性删除和主动删除三种方式。
Redis 目前采用 惰性删除 + 主动删除 的方式。

定时删除

在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
需要创建定时器,而且消耗CPU,一般不推荐使用。

惰性删除

在 key 被访问时如果发现它已经失效,那么就删除它。
调用 expireIfNeeded 函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删除它。

主动删除

在 redis.conf 文件中可以配置主动删除策略,默认是 no-enviction(不删除)

LRU

LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。
  4. 在Java中可以使用LinkHashMap(哈希链表)去实现LRU
    下面以用户信息的需求为例,来演示一下LRU算法的基本思路:
  5. 假设我们使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个用户是按照时间顺序依次从链表右端插入的。
    在这里插入图片描述
  6. 此时,业务方访问用户5,由于哈希链表中没有用户5的数据,我们从数据库中读取出来,插入到缓存当中。这时候,链表中最右端是最新访问到的用户5,最左端是最近最少访问的用户1。
    在这里插入图片描述
  7. 接下来,业务方访问用户2,哈希链表中存在用户2的数据,我们怎么做呢?我们把用户2从它的前驱节点和后继节点之间移除,重新插入到链表最右端。这时候,链表中最右端变成了最新访问到的用户 2,最左端仍然是最近最少访问的用户1。
    在这里插入图片描述
  8. 接下来,业务方请求修改用户4的信息。同样道理,我们把用户4从原来的位置移动到链表最右侧,并 把用户信息的值更新。这时候,链表中最右端是最新访问到的用户4,最左端仍然是最近最少访问的用 户1。
    在这里插入图片描述
  9. 业务访问用户6,用户6在缓存里没有,需要插入到哈希链表。假设这时候缓存容量已经达到上限,必须先删除最近最少访问的数据,那么位于哈希链表最左端的用户1就会被删除掉,然后再把用户6插入到最右端。
    在这里插入图片描述
Redis 的 LRU 数据淘汰机制

redis 源码中的内存管理策略都存在于 evict.c 文件中,其中最重要的一个函数就是 freeMemoryIfNeeded。该函数用于在 redis 占用的内存超过 maxmemory 之后真正释放掉 redis 中的某些键值对,将 redis 占用的内存控制在合理的范围之内。

redis 在占用的内存超过指定的 maxmemory 之后,通过 maxmemory_policy 确定 redis 是否释放内存以及如何释放内存。redis 提供了8种内存超过限制之后的响应措施,分别如下:

1.volatile-lru (least recently used):最近最少使用算法,从设置了过期时间的键中选择空转时间最长的键值对清除掉;
2.volatile-lfu (least frequently used):最近最不经常使用算法,从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉;
3.volatile-ttl:从设置了过期时间的键中选择过期时间最早的键值对清除;
4.volatile-random:从设置了过期时间的键中,随机选择键进行清除;
5.allkeys-lru:最近最少使用算法,从所有的键中选择空转时间最长的键值对清除;
6.allkeys-lfu:最近最不经常使用算法,从所有的键中选择某段时间之内使用频次最少的键值对清除;
7.allkeys-random:所有的键中,随机选择键进行删除;
8.noeviction:不做任何的清理工作,在redis的内存超过限制之后,所有的写入操作都会返回错误;但是读操作都能正常的进行;

缓存淘汰策略的选择
  • allkeys-lru : 在不确定时一般采用策略。 冷热数据交换
  • volatile-lru : 比 allkeys-lru 性能差
  • allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)
  • volatile-ttl:自己控制
  • 前缀为 volatile- 和 allkeys- 的区别在于二者选择要清除的键时的字典不同,volatile- 前缀的策略代表从 redisDb 中的 expire 字典中选择键进行清除;allkeys- 开头的策略代表从 dict 字典中选择键进行清除。Redis 底层数据结构

案例分享:字典库失效

key-Value 业务表,存 code 显示文字
将字典库缓存到 redis 并设置了 maxmemory,并设置缓存淘汰策略为 allkeys-lru 结果在执行缓存淘汰策略时删除了部分数据,造成字典库某些字段失效,缓存击穿,DB 压力剧增,甚至可能宕机。

分析:

字典库:Redis 做 DB 使用,要保证数据的完整性
maxmemory 设置较小,采用 allkeys-lru,会对没有经常访问的字典库随机淘汰当再次访问时会缓存击穿,请求会打到DB上。

解决方案:
  1. 不设置 maxmemory
  2. 使用 noenviction 策略,不做任何的清理工作

Redis 是作为 DB 使用的,要保证数据的完整性,所以不能删除数据。可以将原始数据源(XML)在系统启动时一次性加载到Redis中。
Redis 做 主从 + 哨兵 保证高可用。

Redis 持久化

Redis 是内存数据库,宕机后数据会消失,重启后需要快速恢复数据,故提供持久化机制
Redis 有两种持久化方式:RDB 和 AOF

注意:Redis 持久化不保证数据的完整性。

当 Redis 用作 DB 时,DB 数据要完整,所以一定要有一个完整的数据源(文件、mysql),在系统启动时,从这个完整的数据源中将数据load 到 Redis 中,一般数据量较小,不易改变,比如:字典库

  • 通过命令可以查看关于持久化的信息
127.0.0.1:6379> redis-cli info persistence
# Persistence loading:0 
rdb_changes_since_last_save:1 
rdb_bgsave_in_progress:0 
rdb_last_save_time:1589363051 
rdb_last_bgsave_status:ok 
rdb_last_bgsave_time_sec:-1 
rdb_current_bgsave_time_sec:-1 
rdb_last_cow_size:0 aof_enabled:1 
aof_rewrite_in_progress:0 
aof_rewrite_scheduled:0 
aof_last_rewrite_time_sec:-1 
aof_current_rewrite_time_sec:-1 
aof_last_bgrewrite_status:ok 
aof_last_write_status:ok 
aof_last_cow_size:0 aof_current_size:58 
aof_base_size:0 aof_pending_rewrite:0 
aof_buffer_length:0 
aof_rewrite_buffer_length:0 
aof_pending_bio_fsync:0 
aof_delayed_fsync:0

RDB

RDB(Redis DataBase),是 redis 默认的存储方式,RDB 方式是通过快照( snapshotting )完成的。
持久化的是当前这一刻的数据,不关注数据产生的过程

触发快照的方式

  1. 符合自定义配置的快照规则
  2. 执行 save 或者 bgsave 命令
  3. 执行 flushall 命令
  4. 执行主从复制操作 (第一次)
配置参数定期执行

在 redis.conf 中配置:save 多少秒内 数据变了多少

save "" # 不使用 RDB 存储 不能主从 
save 900 1 # 表示15分钟(900秒钟)内至少1个键被更改则进行快照。 
save 300 10 # 表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 # 表示1分钟内至少10000个键被更改则进行快照。

采用漏斗设计,为 Redis 提供性能保障

命令显式触发

在客户端输入 bgsave 命令

127.0.0.1:6379> bgsave 
Background saving started

RDB 执行流程(原理)

在这里插入图片描述

  1. Redis 父进程首先判断:当前是否在执行 save,或 bgsave / bgrewriteaof(AOF文件重写命令)的子进程,如果在执行则 bgsave 命令直接返回。
  2. 父进程执行 fork(调用 OS 函数复制主进程)操作创建子进程,这个过程中父进程是阻塞的,Redis 不能执行来自客户端的任何命令。
  3. 父进程 fork 后,bgsave 命令返回 “Background saving started” 信息并不再阻塞父进程,并可以响应其他命令。
  4. 子进程创建 RDB 文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。(RDB 始终完整)
  5. 子进程发送信号给父进程表示完成,父进程更新统计信息。
  6. 父进程 fork 子进程后,继续工作。

RDB 文件结构

在这里插入图片描述
RDB 文件结构由9个部分组成,分别如下:

  1. 头部 5字节 固定为 “REDIS” 字符串
  2. 4字节 “RDB” 版本号(不是 Redis 版本号),当前为9,填充后为 0009
  3. 辅助字段,以 key-value 的形式
    在这里插入图片描述
  4. 存储数据库号码
  5. 字典大小
  6. 过期 key
  7. 主要数据,以 key-value 的形式存储
  8. 结束标志
  9. 校验和,就是看文件是否损坏,或者是否被修改。

可以用 winhex 打开 dump.rdb 文件查看。
在这里插入图片描述

RDB 的优缺点

优点
  • RDB 是二进制压缩文件,占用空间小,便于传输(传给 slaver )
  • 主进程 fork 子进程,可以最大化 Redis 性能,主进程内数据不能太大,因为复制过程中主进程是阻塞的,会直接影响性能
缺点

不保证数据完整性,会丢失最后一次快照以后更改的所有数据

AOF

AOF(append only file)是 Redis 的另一种持久化方式。Redis 默认情况下是不开启的。开启 AOF 持久化后 Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据库状态的目的,当 Redis 重启后只要按顺序回放这些命令就会恢复到原始状态。

AOF会记录过程,RDB 只记录某一时刻的数据

AOF 持久化实现

配置 redis.conf

# 可以通过修改 redis.conf 配置文件中的 appendonly 参数开启 
appendonly yes 
# AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的。 
dir ./ 
# 默认的文件名是 appendonly.aof,可以通过 appendfilename 参数修改 
appendfilename appendonly.aof

AOF 原理

AOF 文件中存储的是 redis 的命令集,同步命令到 AOF 文件的整个过程可以分为三个阶段:

命令传播: Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中;
缓存追加: AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
文件写入和保存: AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话,fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。

命令传播

当一个 Redis 客户端需要执行命令时,它通过网络连接,将协议文本发送给 Redis 服务器。服务器在接到客户端的请求之后,它会根据协议文本的内容,选择适当的命令函数,并将各个参数从字符串文本转换为 Redis 字符串对象( StringObject )。每当命令函数成功执行之后,命令参数都会被传播到 AOF 程序。

缓存追加

当命令被传播到 AOF程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本。协议文本生成之后,它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。redisServer 结构维持着 Redis 服务器的状态,aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。

文件写入 和 保存

每当服务器常规任务函数被执行、或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用,这个函数执行以下两个工作:

  1. WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
  2. SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
AOF 保存模式

Redis 目前支持三种 AOF 保存模式,它们分别是:

  • AOF_FSYNC_NO:不保存。
  • AOF_FSYNC_EVERYSEC:每一秒钟保存一次。(默认)
  • AOF_FSYNC_ALWAYS:每执行一个命令保存一次。(不推荐)
AOF_FSYNC_NO 不保存

在这种模式下,每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行,但 SAVE 会被略过。
在这种模式下,SAVE 只会在以下任意一种情况中被执行:

  • Redis 被关闭
  • AOF 功能被关闭
  • 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)

这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。

AOF_FSYNC_EVERYSEC 每一秒钟保存一次

在这种模式中,SAVE 原则上每隔一秒钟就会执行一次,因为 SAVE 操作是由后台子线程(fork)调用的,所以它不会引起服务器主进程阻塞。

AOF_FSYNC_ALWAYS 每执行一个命令保存一次

在这种模式下,每次执行完一个命令之后,WRITE 和 SAVE 都会被执行。
另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。

AOF保存模式对性能和安全性的影响

对于三种 AOF保存模式, 它们对服务器主进程的阻塞情况、数据丢失情况对比如下:
在这里插入图片描述

AOF重写、触发方式、混合持久化

AOF 记录数据的变化过程,越来越大,需要重写“瘦身”
Redis 可以在 AOF 体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写。重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。所谓的 “重写” 其实是一个有歧义的词语,实际上,AOF重写并不需要对原有的 AOF文件进行任何写入和读取,它针对的是数据库中键的当前值。

举例如下:
在这里插入图片描述
Redis 不希望 AOF 重写造成服务器无法处理请求,所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是:

  1. 子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
  2. 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

不过,使用子进程也有一个问题需要解决:因为子进程在进行 AOF 重写期间,主进程还需要继续处理命令,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。

为了解决这个问题,Redis 增加了一个 AOF 重写缓存,这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,还会追加到这个缓存中。
在这里插入图片描述

重写过程分析(整个重写操作是绝对安全的)

Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。

当子进程在执行 AOF 重写时,主进程需要执行以下三个工作:

  • 处理命令请求。
  • 将写命令追加到现有的 AOF 文件中。
  • 将写命令追加到 AOF 重写缓存中。

这样一来可以保证: 现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。所有对数据库进行修改的命令都会被记录到 AOF重写缓存中。
当子进程完成 AOF 重写之后,它会向父进程发送一个完成信号,父进程在接到完成信号之后,会调用一个信号处理函数, 并完成以下工作:

  • 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
  • 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。

Redis 数据库里的命令 + AOF 重写过程中的命令 —> 新的 AOF 文件 —> 覆盖老的

当步骤 1 执行完毕之后,现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了。
当步骤 2 执行完毕之后,程序就完成了新旧两个 AOF 文件的交替。

这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接受命令请求了。在整个 AOF 后台重写过程中,只有最后的写入缓存和改名操作会造成主进程阻塞,在其他时候,AOF 后台重写都不会对主进程造成阻塞,这将 AOF 重写对性能造成的影响降到了最低。

以上就是 AOF 后台重写, 也即是 BGREWRITEAOF 命令(AOF重写)的工作原理。

触发方式
1. 配置触发

在 redis.conf 中配置

# 表示当前AOF文件大小超过上一次AOF文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以 启动时AOF文件大小为准
auto-aof-rewrite-percentage 100 
# 限制允许重写最小AOF文件大小,也就是文件大小小于64mb的时候,不需要进行优化 
auto-aof-rewrite-min-size 64mb
2. 执行 bgrewriteAOF命令
127.0.0.1:6379> bgrewriteAOF
Background append only file rewriting started

混合持久化

RDB 和 AOF 各有优缺点,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化。如果把混合持久化打开,aofrewrite 的时候就直接把 RDB 的内容写到 AOF 文件开头。
RDB的头 + AOF的身体 —> appendonly.aof

开启混合持久化

aof-use-rdb-preamble yes

在这里插入图片描述
我们可以看到该 AOF 文件是 RDB 文件的头和 AOF 格式的内容,在加载时,首先会识别 AOF 文件是否以 REDIS 字符串开头,如果是就按 RDB 格式加载,加载完 RDB 后继续按 AOF 格式加载剩余部分。

AOF 文件的载入与数据还原

因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态

Redis 读取 AOF 文件并还原数据库状态的详细步骤如下:
  1. 服务器内部自行创建一个不带网络连接的伪客户端(fake client):因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样
  2. 从 AOF 文件中分析并读取出一条写命令
  3. 使用伪客户端执行被读出的写命令
  4. 一直执行步骤2和步骤3,直到 AOF 文件中的所有写命令都被处理完毕为止

当完成以上步骤之后,AOF 文件所保存的数据库状态就会被完整地还原出来,整个过程如下图所示:
在这里插入图片描述

RDB 与 AOF对比
  1. RDB 存某个时刻的数据快照,采用二进制压缩存储,AOF 存操作命令,采用文本存储(混合)
  2. RDB 性能高、AOF 性能较低
  3. RDB 在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF 设置为每秒保存一次,则最多丢2秒的数据
  4. Redis 以主服务器模式运行,RDB 不会保存过期键值对数据,Redis 以从服务器模式运行,RDB 会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。

AOF 写入文件时,对过期的 key 会追加一条 del 命令,当执行 AOF 重写时,会忽略过期 key 和 del 命令。
在这里插入图片描述

应用场景

  • 内存数据库 RDB + AOF 数据只会丢失少2秒内的数据
  • 字典库:不驱逐,保证数据完整性
  • 用作 DB,不能主从,尽量保证数据量小
  • 缓存服务器,较高性能: 开 RDB ,RDB 性能高
  • 追求高性能,持久化都不开,redis 宕机重新从数据源恢复
  • 不建议只使用 AOF(性能差),一般不开 AOF

在数据还原时:

  • 有 RDB + AOF 则还原 AOF,因为 RDB 会造成文件的丢失,AOF 相对数据要完整
  • 只有 RDB,则还原 RDB

Redis 扩展功能

  • Redis 发布与订阅
  • Redis 事务
  • Redis 与 lua 的整合
  • 慢查询日志
  • 监视器

发布与订阅

Redis 提供了发布订阅功能,可以用于消息的传输,包括三个部分,Publisher,Subscriber 和 Channel。
在这里插入图片描述
发布者和订阅者都是 Redis 客户端,Channel 则为 Redis 服务器端。发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。

频道/模式的订阅与退订

subscribe:订阅 subscribe channel1 channel2 …
Redis 客户端1订阅频道1和频道2

127.0.0.1:6379> subscribe ch1 ch2 
Reading messages... (press Ctrl-C to quit) 
1) "subscribe" 
2) "ch1" 
3) (integer) 1 
1) "subscribe" 
2) "ch2" 
3) (integer) 2

publish:发布消息 publish channel message

Redis 客户端2将消息发布在频道1和频道2上

127.0.0.1:6379> publish ch1 hello 
(integer) 1 
127.0.0.1:6379> publish ch2 world 
(integer) 1

Redis 客户端1接收到频道1和频道2的消息

1) "message" 
2) "ch1" 
3) "hello" 
1) "message"
 2) "ch2" 
 3) "world"

unsubscribe:退订 channel
Redis 客户端1退订频道1

127.0.0.1:6379> unsubscribe ch1 
1) "unsubscribe" 
2) "ch1" 
3) (integer) 0

psubscribe :模式匹配 psubscribe + 模式

Redis 客户端1订阅所有以ch开头的频道

127.0.0.1:6379> psubscribe ch* 
Reading messages... (press Ctrl-C to quit) 
1) "psubscribe" 
2) "ch*" 
3) (integer) 1

Redis 客户端2发布信息在频道5上

127.0.0.1:6379> publish ch5 helloworld 
(integer) 1

Redis 客户端1收到频道5的信息

1) "pmessage" 
2) "ch*" 
3) "ch5" 
4) "helloworld"

punsubscribe 退订模式

127.0.0.1:6379> punsubscribe ch* 
1) "punsubscribe" 
2) "ch*" 
3) (integer) 0

发布订阅的机制

订阅某个频道或模式:

客户端(client):

属性为 pubsub_channels,该属性表明了该客户端订阅的所有频道
属性为 pubsub_patterns,该属性表示该客户端订阅的所有模式

服务器端(Redis Server):

属性为 pubsub_channels,该服务器端中的所有频道以及订阅了这个频道的客户端
属性为 pubsub_patterns,该服务器端中的所有模式和订阅了这些模式的客户端

typedef struct redisClient { 
	... 
	dict *pubsub_channels; // 该client订阅的channels,以channel为key用dict的方式组织 
	list *pubsub_patterns; // 该client订阅的pattern,以list的方式组织
	... 
} redisClient; 

struct redisServer { 
	... 
	// redis server 进程中维护的 channel dict,它以channel 为 key,订阅 channel 的 client list 为 value 
	dict *pubsub_channels; 
	// redis server 进程中维护的 pattern list 
	list *pubsub_patterns; 
	int notify_keyspace_events; 
	... 
};

当客户端向某个频道发送消息时

  • 首先在 redisServer 中的 pubsub_channels 中找出键为该频道的结点,遍历该结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端。
  • 然后,遍历结构体 redisServer 中的 pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。
使用场景
  • 哨兵模式,Redisson 框架使用,在 Redis 哨兵模式中,哨兵通过发布与订阅的方式与 Redis 主服务器和 Redis 从服务器进行通信。
  • Redisson 是一个分布式锁框架,在 Redisson 分布式锁释放的时候,是使用发布与订阅的方式通知的。

事务

所谓事务(Transaction) ,是指作为单个逻辑工作单元执行的一系列操作

ACID 回顾

  • Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。
  • Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。
  • Isolation(隔离性):事务之间不会相互影响。
  • Durability(持久性):事务执行成功后必须全部写入磁盘。

Redis 事务

  • Redis 的事务是通过 multi、exec、discard、watch 这四个命令来完成的。
  • Redis 的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
  • Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
  • Redis 不支持回滚操作
事务命令

multi:用于标记事务块的开始,Redis 会将后续的命令逐个放入队列中,然后使用 exec 原子化地执行这个命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视 key
unwatch:清除监视

key 127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set s1 222 
QUEUED 
127.0.0.1:6379> hset set1 name zhangfei
QUEUED 
127.0.0.1:6379> exec 
1) OK 
2) (integer) 1 
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set s2 333 
QUEUED 
127.0.0.1:6379> hset set2 age 23 
QUEUED 
127.0.0.1:6379> discard 
OK
127.0.0.1:6379> exec 
(error) ERR EXEC without MULTI 
127.0.0.1:6379> watch s1 
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set s1 555 
QUEUED 
127.0.0.1:6379> exec # 此时在没有 exec 之前,通过另一个命令窗口对监控的s1字段进行修改
(nil) 
127.0.0.1:6379> get s1 
222 
127.0.0.1:6379> unwatch 
OK

事务机制

事务的执行
  1. 事务开始
    在 Redis Client 中,有属性 flags,用来表示是否在事务中
    flags=REDIS_MULTI
  2. 命令入队
    Redis Client 将命令存放在事务队列中
    (EXEC,DISCARD,WATCH,MULTI 除外)
  3. 事务队列
    multiCmd *commands 用于存放命令
  4. 执行事务
    Redis Client 向服务器端发送 exec 命令,Redis Server 会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。

如果某条命令在入队过程中发生错误,redisClient 将 flags 置为 REDIS_DIRTY_EXEC,EXEC 命令将会失败返回。但是失败前的命令都会生效,失败后的命令将会丢失,即不再继续执行,此处将造成数据不一致性问题(命令执行报错 或者 未执行),这也是redis 弱事务的体现

typedef struct redisClient {
	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;
Watch的执行

使用 WATCH 命令监视数据库的键
redisDb 有一个 watched_keys 字典,key 是某个被监视的数据的 key,值是一个链表,记录了所有监视这个数据的客户端

监视机制的触发

当修改数据后,监视这个数据的客户端的 flags 置为 REDIS_DIRTY_CAS

事务执行

Redis Client 向服务器端发送 exec 命令,服务器判断 Redis Client 的 flags,如果为 REDIS_DIRTY_CAS,则清空事务队列。

typedef struct redisDb{ 
	// ..... 
	// 正在被WATCH命令监视的键 
	dict *watched_keys; 
	// ..... 
}redisDb;
Redis 的弱事务性
  • Redis 语法错误
    整个事务的命令在队列里都清除
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> sets m1 44 
(error) ERR unknown command `sets`, with args beginning with: `m1`, `44`, 
127.0.0.1:6379> set m2 55 
QUEUED 
127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 
127.0.0.1:6379> get m1 
"22"

flags=multi_dirty

  • Redis 运行错误
    在队列里正确的命令可以执行 (弱事务性)

弱事务性 :

  1. 在队列里正确的命令可以执行 (非原子操作)
  2. 不支持回滚
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set m1 55 
QUEUED 
127.0.0.1:6379> lpush m1 1 2 3 # 不能是语法错误 
QUEUED 
127.0.0.1:6379> exec 
1) OK 
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 
127.0.0.1:6379> get m1 
"55"
  • Redis 不支持事务回滚(为什么呢?)
  1. 大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
  2. Redis 为了性能方面就忽略了事务回滚。(因为支持回滚就必须记录历史版本)

Lua 脚本

lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 应用场景

游戏开发、独立应用脚本、Web应用脚本、扩展和数据库插件。

  • OpenRestry:一个可伸缩的基于 Nginx 的 Web 平台,在 nginx 集成了 Lua 模块的第三方服务器。
  • OpenRestry 通过 Lua 脚本扩展 nginx 功能,可以方便地搭建能够处理超高并发(日活千万级别)、扩展性极高的动态 Web 应用、Web 服务和动态网关,实现了可伸缩的 Web 平台,内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。
  • OpenRestry 可以实现服务鉴权、安全认证、流量控制 & 分流、日志记录 & 监控、灰度发布、负载均衡、请求路由 等服务。
  • 功能与 nginx 类似,就是由于支持 lua 动态脚本,所以更加灵活,使得其可以方便的实现鉴权、限流、分流、日志记录、灰度发布等功能。
  • 类似的还有 Kong(Api Gateway)、tengine(阿里)

创建并修改 lua 环境

下载

地址:http://www.lua.org/download.html
可以本地下载上传到linux,也可以使用curl命令在linux系统中进行在线下载

curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
安装
yum -y install readline-devel ncurses-devel 
tar -zxvf lua-5.3.5.tar.gz 
#在src目录下 
make linux 
或 make install

如果报错,说找不到readline/readline.h, 可以通过yum命令安装

yum -y install readline-devel ncurses-devel

安装完以后再

make linux / make install

最后,直接输入 lua 命令即可进入 lua 的控制台

Lua 环境协作组件

从 Redis 2.6.0 版本开始,通过内置的 lua 编译/解释器,可以使用 EVAL 命令对 lua 脚本进行求值。
脚本的命令是原子的,Redis Server 在执行脚本命令中,不允许插入新的命令
脚本的命令可以复制,Redis Server 在获得脚本后不执行,生成标识返回,Client 根据标识就可以随时执行

EVAL / EVALSHA 命令实现

EVAL 命令

通过执行 redis 的 eval 命令,可以运行一段 lua 脚本。

EVAL script numkeys key [key ...] arg [arg ...]

命令说明:

  • script 参数:是一段 Lua 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。
  • numkeys 参数:用于指定键名参数的个数。
  • key [key …] 参数: 从 EVAL 的第三个参数开始算起,使用了 numkeys 个键(key),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用1为基址的形式访问 ( KEYS[1] , KEYS[2] ,以此类推)。
  • arg [arg …] 参数:可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似 (ARGV[1] 、 ARGV[2] ,诸如此类)。
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
lua 脚本中调用 Redis 命令
  • redis.call():
    • 返回值就是redis命令执行的返回值
    • 如果出错,则返回错误信息,不继续执行
  • redis.pcall():
    • 返回值就是redis命令执行的返回值
    • 如果出错,则记录错误信息,继续执行
  • 注意事项
    • 在脚本中,使用 return 语句将返回值返回给客户端,如果没有 return,则返回 nil
EVALSHA 命令

EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。

Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)

SCRIPT 命令
  • SCRIPT FLUSH :清除所有脚本缓存
  • SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
  • SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它
192.168.24.131:6380> script load "return redis.call('set',KEYS[1],ARGV[1])" 
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d" 
192.168.24.131:6380> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 n2 zhangfei 
OK
192.168.24.131:6380> get n2
  • SCRIPT KILL :杀死当前正在运行的脚本
脚本管理命令实现

使用 redis-cli 直接执行lua脚本。

test.lua

return redis.call('set',KEYS[1],ARGV[1]) 
./redis-cli -h 127.0.0.1 -p 6379 --eval test.lua name:6 , 'caocao' #,两边有空格

list.lua

local key=KEYS[1] 
local list=redis.call("lrange",key,0,-1); 
return list; 
./redis-cli --eval list.lua list

利用 Redis 整合 Lua,主要是为了性能以及事务的原子性。因为 redis 帮我们提供的事务功能太差。

脚本复制

Redis 传播 Lua 脚本,在使用主从模式和开启 AOF 持久化的前提下:
当执行 lua 脚本时,Redis 服务器有两种模式:脚本传播模式 和 命令传播模式。

脚本传播模式

脚本传播模式是 Redis 复制脚本时默认使用的模式
Redis 会将被执行的脚本及其参数复制到 AOF 文件以及从服务器里面。

执行以下命令:

eval "redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])" 2 n1 n2
zhaoyun1 zhaoyun2

那么主服务器将向从服务器发送完全相同的 eval 命令:

eval "redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])" 2 n1 n2
zhaoyun1 zhaoyun2

注意:在这一模式下执行的脚本不能有时间、内部状态、随机函数(spop)等。执行相同的脚本以及参数必须产生相同的效果。在Redis 5,也是处于同一个事务中。

命令传播模式

处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到 AOF 文件以及从服务器里面。

因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间、内部状态、随机函数等,主服务器给所有从服务器复制的写命令仍然是相同的。

为了开启命令传播模式,用户在使用脚本执行任何写操作之前,需要先在脚本里面调用以下函数:

redis.replicate_commands()

只对调用该函数的脚本有效:在使用命令传播模式执行完当前脚本之后,服务器将自动切换回默认的脚本传播模式。
如果我们在主服务器执行以下命令:

eval "redis.replicate_commands();redis.call('set',KEYS[1],ARGV[1]);redis.call('set',K EYS[2],ARGV[2])" 2 n1 n2 zhaoyun11 zhaoyun22

那么主服务器将向从服务器复制以下命令:

EXEC 
*1
$5
MULTI 
*3
$3
set 
$2
n1
$9
zhaoyun11 
*3
$3
set 
$2
n2
$9
zhaoyun22 
*1
$4
EXEC
管道(pipeline),事务和脚本(lua)三者的区别
  • 三者都可以批量执行命令
  • 管道无原子性,命令都是独立的,属于无状态的操作
  • 事务和脚本是有原子性的,其区别在于脚本可借助 Lua 语言可在服务器端存储的便利性定制和简化操作
  • 脚本的原子性要强于事务,脚本执行期间,另外的客户端 其它任何脚本或者命令都无法执行,脚本的执行时间应该尽量短,不能太耗时的脚本

慢查询日志

我们都知道MySQL有慢查询日志

Redis 也有慢查询日志,可用于监视和优化查询

慢查询设置

在 redis.conf 中可以配置和慢查询日志相关的选项:

#执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录 
slowlog-log-slower-than 10000 
#slowlog-max-len 存储慢查询日志条数 
slowlog-max-len 128

Redis 使用列表存储慢查询日志,采用队列方式(FIFO)

  • config set 的方式可以临时设置,redis重启后就无效
  • config set slowlog-log-slower-than 微秒
  • config set slowlog-max-len 条数

查看日志:slowlog get [n]

127.0.0.1:6379> config set slowlog-log-slower-than 0 
OK
127.0.0.1:6379> config set slowlog-max-len 2 
OK
127.0.0.1:6379> set name:001 zhaoyun 
OK
127.0.0.1:6379> set name:002 zhangfei 
OK
127.0.0.1:6379> get name:002 
"zhangfei"
127.0.0.1:6379> slowlog get 
1) 1) (integer) 7 #日志的唯一标识符(uid) 
   2) (integer) 1589774302 #命令执行时的UNIX时间戳 
   3) (integer) 65 #命令执行的时长(微秒) 
   4) 1) "get" #执行命令及参数 
      2) "name:002" 
   5) "127.0.0.1:37277" 
   6) "" 
2) 1) (integer) 6 
   2) (integer) 1589774281 
   3) (integer) 7 
   4) 1) "set" 
      2) "name:002" 
      3) "zhangfei" 
   5) "127.0.0.1:37277"
   6) "" 
   # set 和 get都记录,第一条被移除了。

慢查询记录的保存

在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;

慢查询日志的阅览&删除

初始化日志列表

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)

添加日志实现

在每次执行命令的之前和之后,程序都会记录微秒格式的当前 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) { 
	/* Slowlog disabled,负数表示禁用 */ 
	if (server.slowlog_log_slower_than < 0) return; 
	/* 如果执行时间 > 指定阈值*/ 
	if (duration >= server.slowlog_log_slower_than) 
		/* 创建一个slowlogEntry对象,添加到列表首部*/
		listAddNodeHead(server.slowlog,slowlogCreateEntry(argv,argc,duration)); 
	/* 如果列表长度 > 指定长度 */
	while (listLength(server.slowlog) > server.slowlog_max_len) 
		/* 移除列表尾部元素 */
		listDelNode(server.slowlog,listLast(server.slowlog)); 
}
slowlogPushEntryIfNeeded 函数的作用有两个:
  1. 检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置的时间,如果是的话,就为命令创建一个新的日志, 并将新日志添加到 slowlog 链表的表头。
  2. 检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度,如果是的话,那么将多出来的日志从 slowlog 链表中删除掉。

慢查询定位&处理

使用 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

监视器

Redis 客户端通过执行 MONITOR 命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理的命令请求的相关信息。

此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。
在这里插入图片描述
Redis 客户端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"

Redis 客户端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

实现监视器

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
}

向监视器发送命令信息

利用call函数实现向监视器发送命令

// 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); 
	}
	...... 
}

call 主要调用了 replicationFeedMonitors ,这个函数的作用就是将命令打包为协议,发送给监视器。

Redis 监控平台

grafana、prometheus 以及 redis_exporter。

Grafana 是一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形化选项,可以混合多种风格,支持多个数据源特点。

Prometheus 是一个开源的服务监控系统,它通过HTTP协议从远程的机器收集数据并存储在本地的时序数据库上。

redis_exporter 为 Prometheus 提供了 redis 指标的导出,配合 Prometheus 以及 grafana 进行可视化及监控。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值