5-1-4 Redis持久化机制和扩展功能

一、Redis持久化

学习目标:
理解RDB的原理和执行流程
了解RDB的文件结构
理解AOF的原理和执行流程
理解AOF重写原理和触发方式
掌握RDB和AOF的区别和应用场景

1.1 为什么要持久化

Redis是内存数据库,宕机后数据会消失。
Redis重启后快速恢复数据,要提供持久化机制
Redis持久化是为了快速的恢复数据而不是为了存储数据
Redis有两种持久化方式:RDB和AOF
注意:Redis持久化不保证数据的完整性。
当Redis用作DB时,DB数据要完整,所以一定要有一个完整的数据源(文件、mysql)
在系统启动时,从这个完整的数据源中将数据load到Redis中
数据量较小,不易改变,比如:字典库(xml、Table)

通过info命令可以查看关于持久化的信息


# 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

1.2 RDB

RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照( snapshotting )完成的。

这一刻的数据

不关注过程

1.2.1 触发快照的方式
  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个键被更改则进行快照。

漏斗设计 提供性能

命令显式触发
在客户端输入bgsave命令。

127.0.0.1:6379> bgsave
Background saving started

1.2.2 RDB执行流程(原理)

在这里插入图片描述

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

在这里插入图片描述

1、头部5字节固定为“REDIS”字符串
2、4字节“RDB”版本号(不是Redis版本号),当前为9,填充后为0009
3、辅助字段,以key-value的形式

字段名字段值字段名字段值
redis-ver5.0.5aof-preamble是否开启aof
redis-bits64/32repl-stream-db主从复制
ctime当前时间戳repl-id主从复制
used-mem使用内存repl-offset主从复制

4、存储数据库号码
5、字典大小

6、过期key
7、主要数据,以key-value的形式存储
8、结束标志
9、校验和,就是看文件是否损坏,或者是否被修改。
可以用winhex打开dump.rdb文件查看。

在这里插入图片描述

1.2.4 RDB的优缺点

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

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

1.2 AOF

AOF(append only file)是Redis的另一种持久化方式。Redis默认情况下是不开启的。开启AOF持久化后

Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据库状态的目的,

这样当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。

AOF会记录过程,RDB只管结果

1.2.1 AOF持久化实现

配置 redis.conf

# 可以通过修改redis.conf配置文件中的appendonly参数开启 
appendonly yes

# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。 
dir ./

# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改 
appendfilename appendonly.aof

1.2.2 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 函数都会被调用, 这个函数执行以下两个工作:

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

1.2.3 AOF 保存模式

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

AOF_FSYNC_NO :不保存。 AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐) 以下三个小节将分别讨论这三种保存模式。

不保存
在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。

在这种模式下, SAVE 只会在以下任意一种情况中被执行:
Redis 被关闭 AOF 功能被关闭 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行) 这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。

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

每执行一个命令保存一次
在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。
另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
AOF 保存模式对性能和安全性的影响
对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下

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

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

举例如下:

set s1 11
set s1 22 ------- > set s1 33
set s1 33
没有优化的:
set s1 11
set s1 22
set s1 33
优化后:
set s1 33
lpush list1 1 2 3
lpush list1 4 5 6 -------- > list1 1 2 3 4 5 6
优化后
lpush list1 1 2 3 4 5 6

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重写)的工作原理。

AOF的流程图
在这里插入图片描述

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格式加载剩余部分。

1.2.5 AOF文件的载入与数据还原

因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态 Redis读取AOF文件并还原数据库状态的详细步骤如下: 1、创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服 务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样 2、从AOF文件中分析并读取出一条写命令 3、使用伪客户端执行被读出的写命令 4、一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止 当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来,整个过程如下图所示:

在这里插入图片描述

1.3 RDB与AOF对比

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

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

1.4 应用场景

内存数据库 rdb+aof 数据不容易丢
有原始数据源: 每次启动时都从原始数据源中初始化 ,则 不用开启持久化 (数据量较小)
缓存服务器 rdb 一般 性能高

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

1.4.1 拉勾的配置策略

追求高性能:都不开 redis宕机 从数据源恢复
字典库 : 不驱逐,保证数据完整性 不开持久化
用作DB 不能主从 数据量小
做缓存 较高性能: 开rdb
Redis数据量存储过大,性能突然下降,
fork 时间过长 阻塞主进程
则只开启AOF

二、Redis扩展功能

掌握Redis发布与订阅的命令
理解Redis发布与订阅的机制
知道Redis发布与订阅的使用场景
掌握Redis事务的命令
理解Redis事务的机制
理解Redis事务的弱事务性
理解lua的概念
掌握Redis和lua的整合使用
了解慢查询日志的概念
掌握慢查询的定位和处理
了解监视器的概念
掌握监视器的使用

2.1 发布与订阅

Redis提供了发布订阅功能,可以用于消息的传输
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,该属性表示该客户端订阅的所有模式
服务器端(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,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。

使用场景:哨兵模式,Redisson框架使用

在Redis哨兵模式中,哨兵通过发布与订阅的方式与Redis主服务器和Redis从服务器进行通信。这个我们将在后面的章节中详细讲解。

Redisson是一个分布式锁框架,在Redisson分布式锁释放的时候,是使用发布与订阅的方式通知的,这个我们将在后面的章节中详细讲解。

2.2 事务

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

2.2.1 ACID回顾

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

2.2.2 Redis事务

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

2.2.3 事务命令

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

2.2.4 事务机制

事务的执行

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

如果某条命令在入队过程中发生错误,redisClient将flags置为REDIS_DIRTY_EXEC,EXEC命令将会失败返回。
在这里插入图片描述

typedef struct redisClient{ 
	// flags i
	nt 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

事务执行
RedisClient向服务器端发送exec命令,服务器判断RedisClient的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为了性能方面就忽略了事务回滚。 (回滚记录历史版本)

2.3 Lua脚本

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

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

OpenRestry:一个可伸缩的基于Nginx的Web平台,是在nginx之上集成了lua模块的第三方服务器

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

2.3.1 创建并修改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的控制台

2.3.2 Lua环境协作组件

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

2.3.3 EVAL/EVALSHA命令实现
2.3.3.1 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
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 n1 zhaoyun
2.3.3.2 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
2.3.4 脚本管理命令实现

使用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帮我们提供的事务功能太差

2.3.5 脚本复制

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)等。执行相同的脚本以及参数必须产生相同的效果。在Redis5,也是处于同一个事务中。

命令传播模式

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

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

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

redis.replicate_commands()

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

2.4 管道(pipeline),事务和脚本(lua)三者的区别

三者都可以批量执行命令
管道无原子性,命令都是独立的,属于无状态的操作

事务和脚本是有原子性的,其区别在于脚本可借助Lua语言可在服务器端存储的便利性定制和简化操作

脚本的原子性要强于事务,脚本执行期间,另外的客户端 其它任何脚本或者命令都无法执行,脚本的执行时间应该尽量短,不能太耗时的脚本

2.5慢查询日志

我们都知道MySQL有慢查询日志
Redis也有慢查询日志,可用于监视和优化查询

2.5.1 慢查询设置

在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都记录,第一条被移除了。

2.5.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;


2.5.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)

2.5.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 函数的作用有两个:

  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

2.6 监视器

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"

2.6.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 

}
2.6.2 向监视器发送命令信息

利用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 ,这个函数的作用就是将命令打包为协议,发送给监视器。

2.6.2 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、付费专栏及课程。

余额充值