拉勾教育学习-笔记分享のRedis"苏醒" II

548 阅读25分钟

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
Redis 是分布式学习中必须要掌握的重点技术,可以多花时间啃一啃

合抱之木,生于毫末;九层之台,起于垒土

Redis官网

三、通讯协议及事件处理机制

part 1 - 通信协议

Redis是单进程单线程的。
应用系统和Redis通过Redis协议(RESP)进行交互

「请求响应模式」

  • 串行的请求模式 (ping-pong):串行化是最简单模式,客户端与服务器端建立长连接
    • 连接通过心跳机制检测(ping-pong) ack应答
      客户端发送请求,服务端响应,客户端收到响应后,再发起第二个请求,服务器端再响应
    • 特点:有问有答、性能较低
  • 双工的请求响应模式 (pipeline):批量请求,批量响应;请求响应交叉进行,不会混淆
    • pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回
    • 通过pipeline,一次pipeline(n条命令)= 一次网络时间 + n次命令时间
    • 通过 Jedis 可以很方便的使用 pipeline:
      Jedis redis = new Jedis("XXX.XXX.XXX.XXX", 6379);
      redis.auth("123456"); // 授权密码,对应redis.conf中的 requirepass 参数
      Pipeline pipe = redis.pipelined();
      for (int i = 0; i < 50000; i ++) {
      	pipe.set("key_" + String.valueOf(i), string.valueOf(i));
      }
      // 封装后将 pipe 一次性发给 Redis
      pipe.sync();
      
  • 原子化的批量请求响应模式 (事务):Redis可以利用事务机制批量执行命令。后面会详细讲解。
  • 发布订阅模式 (pub/sub):发布订阅模式是:一个客户端触发,多个客户端被动接收,通过服务器中转。后面会详细讲解。
  • 校本化的批量执行 (lua):客户端向服务器端提交一个lua脚本,服务器端执行该脚本。后面会详细讲解。

「请求数据格式」

Redis客户端与服务器交互采用序列化协议(RESP),通过字符串数组的形式来表示所要执行命令的参数。

  • 内联格式:使用telnet给Redis发送命令,首字符为Redis命令名的字符,格式为 str1 str2 str3...
    • 若系统不存在telnet命令,请使用 yum install telnet-server.x86_64yum install telnet.x86_64 进行安装(具体版本使用yum list telnet* 查看)
  • 规范格式 (redis-cli)
    • 间隔符号,在Linux下是\r\n,在Windows下是\n
    • 简单字符串 Simple Strings, 以 "+" 加号 开头
    • 错误 Errors, 以 "-" 减号 开头
    • 整数型 Integer, 以 ":" 冒号开头
    • 大字符串类型 Bulk Strings, 以 "$" 美元符号开头,长度限制512M
    • 数组类型 Arrays,以 "\*" 星号开头
    • 举个栗子:
      • 控制台可视状态:
        redis> set mykey Hello
        "OK"
        
      • 协议底层实际发送的请求数据
        *3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
        => *3 表示发出的命令是 3个元素的数组
        => $3 表示接下来是一个长度为3的 Bulk String
        => $5 表示接下来是一个长度为5的 Bulk String
        
      • 协议底层实际收到的响应数据
        +OK\r\n
        

「命令处理流程」

整个流程包括:服务器启动监听、接收命令请求并解析、执行命令请求、返回命令回复等

  • Server启动时监听socket
    • 启动时将调用initServer()函数:
      • 在该函数中创建 EventLoop
      • 然后注册 事件(socket)连接处理器
      • 最后设置监听socket的处理函数
  • 建立Client
    • redis-cli 建立 socket
    • redis-server 为每个连接(socket)注册一个Client对象
    • 注册 事件(socket)读取处理器
    • 指定事件处理函数
  • 读取socket数据到输入缓冲区
    • 从client中读取客户端的 查询缓冲区 内容
  • 解析获取命令
    • 将输入缓冲区中的数据解析成对应的命令
    • 判断是单条命令还是多条命令并调用相应的解析器解析
  • 执行命令并输出
    • 解析成功后调用 processCommand 方法执行命令
    • 将结果写出当前缓冲区
    • 注册 事件(socket)写入处理器 等待返回命令

「协议响应格式」

  • 状态恢复 首字节为 "+"
    • "+OK"
  • 错误回复 首字节为 "-"
    • "-ERR unknown command 'XXXX'"
  • 整数回复 首字节为 ":"
    • :10
  • 批量回复 首字节为 "$"
    • $10 mykey
  • 多条批量回复 首字节为 "*"
    • *3

「协议解析及处理」

  • 协议解析
    1. 解析命令请求参数数量:
      命令请求参数数量的协议格式为 *N\r\n ,其中N就是数量
      • 举个栗子:
        127.0.0.1:6379> SET name:1 Archie
        
        打开对应的aof文件可以看到协议内容如下:
        *3\r\n$3\r\nSET\r\n$7\r\nname:1\r\nArchie\r\n
        => 于是去掉开头 * 号,首个\r\n后
        => 参数数量 N 就是 3
        
    2. 循环解析请求参数:
      首字符必须是"$",使用"/r"定位到行尾,之间的数是参数的长度,从/n后到下一个"$"之间就是参数的值了
      • 再拿上述栗子:
        *3\r\n$3\r\nSET\r\n$7\r\nname:1\r\nArchie\r\n
        => 参数1 --> SET
        => 参数2 --> name:1
        => 参数3 --> Archie
        
    3. 协议执行
  • 调用命令
    • 校验成功后,会调用call函数执行命令,并记录命令执行时间和调用次数(如果执行命令时间过长还要记录慢查询日志)
  • 返回结果
    • 根据不同的协议格式返回对应类型的结果。

part 2 - 事件处理机制

Redis 是经典的驱动机制,自己实现了一套简洁的事件驱动库 ae_event
ae_event 只关注网络IO以及定时器,主要处理两类事件:文件事件、时间事件。

「文件事件」

本质是Socket的读写事件,也即IO事件。用于处理 Redis 服务器和客户端之间的网络IO。

文件事件处理器使用IO多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数。

  • 组成部分
    1. 套接字:每当一个套接字准备好执行 accept、read、write和 close 等操作时,就会产生一个文件事件。
      因为 Redis 通常会连接多个套接字,所以多个文件事件有可能并发的出现
    2. IO多路复用程序:监听多个套接字,并向文件事件派发器传递那些产生了事件的套接字
      • select函数:调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fd列表,来找到就绪的描述符。
      • poll函数:poll使用一个 pollfd的指针实现,pollfd结构包含了要监视的event和发生的event,不再使用select“参 数-值”传递的方式。
      • epoll函数:相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中
      • kqueue函数:kqueue 是 unix 下的一个IO多路复用库。最初是2000年Jonathan Lemon在FreeBSD系统上开发的一个高性能的事件通知接口。注册一批socket描述符到 kqueue 以后,当其中的描述符状态发生变化时,kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了
    3. 文件事件分派器:对传输过来的套接字进行分类,并且派给对应的处理器(handler)
    4. 事件处理器
      • | 连接应答处理器 |:
        当Redis服务器进行初始化时,程序会将这个连接应答处理器和服务监听套件字的 AE_READABLE 事件关联起来;当有客户端用 connect 函数连接服务器监听套接字时,套接字就会产生 AE_READABLE 事件,引发连接应答处理器执行,并执行相应的套接字应答操作
      • | 命令请求处理器 |:
        当一个客户端通过连接应答处理器成功连接到服务器时,服务器会将客户端套接字的 AE_READABLE 事件和命令请求处理器关联起来,当客户端向服务器发送命令请求时,套接字就会产生 AE_READABLE 事件,引发命令请求处理器执行,并执行相应的套接字读入操作
      • | 命令回复处理器 |:
        当服务器有命令回复需要传递给客户端时,服务端会将客户端套接字的 AE_WRITABLE 事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生 AE_WRITABLE 事件,引发命令回复处理器执行,并执行相应的套接字写入操作
  • 完整过程
    1. 服务器监听套件字的 AE_READABLE 事件
      当客户端发送连接请求产生 AE_READABLE 事件,服务端会对客户端的连接请求进行应答,将客户端套接字的 AE_READABLE 事件与命令请求处理器关联,客户端可以向服务端发送命令请求了
    2. 客户端向服务端发送一个命令请求,客户端套接字将产生 AE_READABLE 事件,引发命令处理器去执行
      执行命令将产生相应的命令回复,服务端将客户端套接字的 AE_WRITABLE 事件与命令回复处理器关联
    3. 客户端尝试读取命令回复时,客户端套接字将产生 AE_WRITABLE 事件,触发命令回复处理器执行
      当命令回复处理器将命令回复全部写入套接字之后,服务器就会接触客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联

「时间事件」

Redis 服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的

服务器所有的时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

正常模式下的Redis服务器只使用 serverCron 一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件,所以不影响事件执行的性能

  • serverCron函数:时间事件核心管理函数
    • 更新redis服务器各类统计信息,包括时间、内存占用、数据库占用等情况
    • 清理数据库中的过期键值对
    • 关闭和清理连接失败的客户端
    • 尝试进行aof和rdb持久化操作
    • 如果服务器是主服务器,会定期将数据向从服务器做同步操作
    • 如果处于集群模式,对集群定期进行同步与连接测试操作

    redis服务器开启后,就会周期性执行此函数,直到redis服务器关闭为止。默认每秒执行10次,平均100毫秒执行一次,可以在 redis 配置文件的 hz 选项,调整该函数每秒执行的次数。

  • 事件分类
    • 定时事件:让一段程序在指定的时间之后执行一次
    • 周期性事件:让一段程序每隔指定时间就执行一次(serverCron 是典型的周期性事件)

「aeEventLoop」

aeEventLoop是整个事件驱动的核心,它管理着文件事件表和时间事件列表;
不断地循环处理着就绪的文件事件到期的时间事件

aeEventLoop 结构体如下:

typedef struct aeEventLoop { 
	//最大文件描述符的值 
    int maxfd; /* highest file descriptor currently registered */ 
    //文件描述符的最大监听数 
    int setsize; /* max number of file descriptors tracked */ 
    //用于生成时间事件的唯一标识id 
    long long timeEventNextId; 
    //用于检测系统时间是否变更(判断标准 now<lastTime) 
    time_t lastTime; /* Used to detect system clock skew */ 
    //注册的文件事件 
    aeFileEvent *events; /* Registered events */ 
    //已就绪的事件 
    aeFiredEvent *fired; /* Fired events */ 
    //注册要使用的时间事件 
    aeTimeEvent *timeEventHead; 
    //停止标志,1表示停止 
    int stop; 
    //这个是处理底层特定API的数据,对于epoll来说,该结构体包含了epoll fd和epoll_event 
    void *apidata; /* This is used for polling API specific data */ 
    //在调用processEvent前(即如果没有事件则睡眠),调用该处理函数 
    aeBeforeSleepProc *beforesleep; 
    //在调用aeApiPoll后,调用该函数 
    aeBeforeSleepProc *aftersleep; 
} aeEventLoop;

其中 aeFileEvent 表示已经注册并需要监听的事件
结构体如下:

typedef struct aeFileEvent { 
   // 监听事件类型掩码, 
   // 值可以是 AE_READABLE 或 AE_WRITABLE , 
   // 或者 AE_READABLE | AE_WRITABLE 
   int mask; /* one of AE_(READABLE|WRITABLE) */ 
   // 读事件处理器 
   aeFileProc *rfileProc; 
   // 写事件处理器 
   aeFileProc *wfileProc; 
   // 多路复用库的私有数据 
   void *clientData; 
} aeFileEvent;

其中 aeFiredEvent 表示已就绪的文件事件
结构体如下:

typedef struct aeFiredEvent { 
   // 已就绪文件描述符
   int fd;
   // 事件类型掩码,值可以是 AE_READABLE 或 AE_WRITABLE 或者是两者的或(||) 
   void *clientData; 
} aeFileEvent;

其中 aeTimeEvent 表示时间事件
结构体如下:

typedef struct aeTimeEvent { 
   /* 全局唯一ID */ 
   long long id; /* time event identifier. */ 
   /* 秒精确的UNIX时间戳,记录时间事件到达的时间*/ 
   long when_sec; /* seconds */ 
   /* 毫秒精确的UNIX时间戳,记录时间事件到达的时间*/ 
   long when_ms; /* milliseconds */ 
   /* 时间处理器 */ 
   aeTimeProc *timeProc; 
   /* 事件结束回调函数,析构一些资源*/ 
   aeEventFinalizerProc *finalizerProc; 
   /* 私有数据 */ 
   void *clientData; 
   /* 前驱节点 */ 
   struct aeTimeEvent *prev;
   /* 后继节点 */ 
   struct aeTimeEvent *next; 
} aeTimeEvent;

四、持久化

part 1 - 为什么要持久化

  • Redis是内存数据库,宕机后数据会消失
  • Redis重启后快速恢复数据,要提供持久化机制
  • Redis持久化是为了快速的恢复数据而不是为了存储数据
  • Redis有两种持久化方式:RDBAOF

Redis持久化不保证数据的完整性

part 2 - RDB

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

  • 快照触发方式
    1. 按照配置进行触发:
      • 在 redis.conf 中配置:
        # 不使用RDB存储(禁用主从)
        save "" 
        # 15分钟(900s)内至少1个键被更改时 ==> 触发快照
        save 900 1
        # 1分钟(60s)内至少10000个键被更改时 ==> 触发快照
        save 60 10000
        
    2. 执行 save 或 bgsave 命令
    3. 执行 flushall 命令
    4. 执行 主从复制 操作
  • RDB执行流程
    1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
    2. 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
    3. 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
    4. 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
    5. 进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项。
  • RDB文件结构
    • REDIS:头部5字节固定为“REDIS”字符串
    • RDB_VERSION:4字节“RDB”版本号(不是Redis版本号).如:当前为9,填充后为0009
    • AUX_FIELD_KEY_VALUE_PAIRS:辅助字段,以key-value的形式
    • DB_NUM:存储数据库号码
    • DB_DICT_SIZE:字典大小
    • EXPIRE_DICT_SIZE:过期key
    • KEY_VALUE_PAIRS:主要数据,以key-value的形式存储
    • EOF:结束标志
    • CHECK_SUM:校验和,就是看文件是否损坏,或者是否被修改
  • 优点
    • RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
    • 主进程fork子进程,可以最大化Redis性能,主进程不能太大,Redis的数据量不能太大,复制过程中主进程阻塞
  • 缺点
    • 不保证数据完整性,会丢失最后一次快照以后更改的所有数据

part 3 - AOF

Append Only File,Redis持久化数据的另一种方式
这种方式以日志形式记录每一条操作,当redis恢复数据时,还原所有操作.

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

  • 持久化的实现
    • 配置 redis.conf
      # 可以通过修改redis.conf配置文件中的appendonly参数开启
      appendonly yes
      # AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
      dir ./
      # 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
      appendfilename appendonly.aof
      
  • AOF执行流程
    1. 所有的写入命令会追加到aof_buf中
    2. AOF缓冲区根据对应的策略向硬盘做同步操作
    3. 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
    4. 当Redis服务器重启时,可以加载AOF文件进行数据恢复
  • AOF原理
    • 命令传播:当一个 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 文件保存到磁盘中。
  • AOF保存模式
    1. AOF_FSYNC_NO :不保存。
    2. AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
    3. AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)

part 4 - RDB <- VS -> AOF

  1. RDB存某个时刻的数据快照,采用二进制压缩存储,AOF存操作命令,采用文本存储(混合)
  2. RDB性能高、AOF性能较低
  3. RDB在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多丢2秒的数据
  4. Redis以主服务器模式运行,RDB不会保存过期键值对数据,Redis以从服务器模式运行,RDB会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。
  5. RDB文件紧凑小巧,RDB文件生成又子进程完成,不会阻塞主进程,并且可以利用多核CPU资源,数据的恢复速度也比AOF快,但是RDB方式容易丢失数据,有些公司为了充分利用CPU资源,将Redis进程与cpu核心进行绑定,进行RDB时子进程与父进程会发生资源竞争,影响服务吞吐。
  6. AOF更加安全,可以将数据更加及时的同步到文件中,但是AOF需要较多的磁盘IO开支,AOF文件尺寸较大,文件内容恢复数度相对较慢。

五、拓展功能

part - 1 发布与订阅

Redis提供了发布订阅功能,可以用于消息的传输

Redis的发布订阅机制包括三个部分,publisher,subscriber 和 Channel

发布者订阅者都是Redis客户端Channel则为Redis服务器端

「频道模式的订阅与退订」

  • 订阅&发布
    • 普通订阅:subscribe ch1 ch2 chn...
    • 发布:publish chn MESSAGE
    • 匹配订阅:psubscribe ch*
    • 通道退订:unsubscribe ch1 ch2 chn...
    • 批量退订:punsubscribe ch*

「发布订阅的机制」

  • 客户端
    • pubsub_channels: 客户端订阅的所有频道集合
    • pubsub_patterns: 客户端订阅的所有模式
  • 服务器端
    • 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;
    ...
};
  1. 当客户端向某个频道发送消息时,Redis首先在redisServer中的pubsub_channels中找出键为该频道的结点,遍历该结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端。
  2. 然后,遍历结构体redisServer中的pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。

part - 2 事务

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

  1. Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。
  2. Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。
  3. Isolation(隔离性):事务之间不会相互影响。
  4. Durability(持久性):事务执行成功后必须全部写入磁盘。
  • Redis中的事务
    • Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
    • Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
    • Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
    • Redis不支持回滚操作
  • 事务命令
    • multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列
    • exec:执行命令队列
    • discard:清除命令队列
    • watch:监视key
    • unwatch:清除监视key
    • 栗子1
    • 栗子2
  • 事务机制
    • 事务的执行
      1. 事务开始:RedisClient中存在属性-flags,用于标识 是否处于事务中
      2. 命令入队:RedisClient将命令存放在事务队列中(EXEC,DISCARD,WATCH,MULTI 除外)
      3. 事务队列:存放命令的结构体,代码如下:
        typedef struct multiCmd {
            // 参数
            robj **argv;
            // 参数数量
            int argc;
            // 命令指针
            struct redisCommand *cmd;
        }
        
      4. 执行事务 :RedisClient向服务器端发送exec命令,RedisServer会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。
    • Watch的执行
      => redisDb有一个watched_keys字典,key是某个被监视的数据的key,值是一个链表.记录了所有监视这个数据的客户端
      => 当修改数据后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS
      => RedisClient向服务器端发送exec命令,服务器判断RedisClient的flags,如果为REDIS_DIRTY_CAS,则清空事务队列
      typedef struct redisDb {
          ...
          // 正在被 WATCH 命令监视的键
          dict *watched_keys;
          ...
      }
      
    • Redis的弱事务性
      • Redis语法错误
      • Redis运行错误
      • Redis不支持事务回滚
        • 大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
        • Redis为了性能方面就忽略了事务回滚

part - 3 Lua脚本

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

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

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

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

「创建并修改lua环境」

  • 下载
    1. 本地下载后上传至linux环境,下载地址
    2. 使用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
    

    若报错说明找不到 readline / readline.h,需通过yum命令安装:
    yum -y install readline-devel ncurses-devel

  • 进入lua控制台 直接输入 lua 即可

「Lua环境协作组件」

从Redis2.6.0版本开始,通过 内置的lua编译/解释器,可以使用 EVAL 命令对lua脚本进行求值

  • 脚本的命令是原子的,RedisServer在执行脚本命令中,不允许插入新的命令
  • 脚本的命令可以复制,RedisServer在获得脚本后不执行,生成标识返回,Client根据标识就可以随时执行

「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变量类似

举个栗子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 userName Archie

「EVALSHA」

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

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

(需要结合下一小节 SCRIPT 命令获取到 SHA1摘要 后进行操作 )

「SCRIPT命令」

  • SCRIPT FLUSH:清除所有脚本缓存
  • SCRIPT EXISTS:根据给定的脚本校验,检查指定的脚本是否存在于脚本缓存
  • SCRIPT LOAD:将一个脚本缓存,返回 SHA1摘要,但并不是立即运行它
  • SCRIPT KILL: 杀死当前正在运行的脚本

结合上一节 EVALSHA 举个栗子

「脚本管理命令实现」

我们也可以直接使用redis-cli执行lua脚本:

进入 redis/bin/ ,创建xxx.lua写入lua命令并使用 redis-cli执行语句

举个栗子A: 创建 test.lua 脚本

return redis.call('set',KEYS\[1\],ARGV\[1\])

执行 ./redis-cli -h 127.0.0.1 -p 6379 --eval test.lua userAge , 25 (','号左右有空格; 在本机时候执行时,可以不写 -h 127.0.0.1 ; 在默认端口时,可以不写 -p 6379)

举个栗子B: 创建 list.lua 脚本

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

执行 ./redis-cli --eval list.lua list (','号左右有空格)

「脚本复制」

当执行lua脚本时,Redis服务器主要有以下两种模式:

  • 脚本传播模式

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

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

  • 命令传播模式

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

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

  • 【管道(pipeline)、事务、脚本(lua)的区别】

    1. 管道无原子性,命令都是独立的,属于无状态的操作
    2. 事务和脚本是有原子性的,其区别在于脚本可借助Lua语言可在服务器端存储的便利性定制和简化操作
    3. 脚本的原子性要强于事务,脚本执行期间,另外的客户端 其它任何脚本或者命令都无法执行,脚本的执行时间应该尽量短,不能太耗时的脚本

part - 4 慢查询日志设置

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

  1. 在redis.conf中可以配置和慢查询日志相关的选项:
# 执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录
slowlog-log-slower-than 10000
# slowlog-max-len 存储慢查询日志条数
slowlog-max-len 128
  1. 当然也可以使用 config set 命令进行临时设置(redis重启后无效):
> config set slowlog-log-slower-than 微妙
> config set slowlog-max-len 条数
  1. 查看日志: slowlog get [n]

举个栗子B

part - 5 监视器

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

此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。

  • Redis监控平台
    • Grafana:一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形化选项,可以混合多种风格,支持多个数据源特点
    • Prometheus:一个开源的服务监控系统,它通过HTTP协议从远程的机器收集数据并存储在本地的时序数据库上
      • redis_exporter:redis_exporter为Prometheus提供了redis指标的导出,配合Prometheus以及grafana进行可视化及监控