7.高阶篇:Redis 管道与脚本应用

Redis 管道应用

Redis是一种基于客户端--服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:

1.客户端发送命令请求。
2.服务端等待命令执行。
3.服务端执行命令。
4.返回结果

这个过程被称为 Round trip time (简称RTT,往返时间)。在实际应用中,可能一个事物或者操作,需要很多次的调用,那么需要N次调用,就需要消耗N次RTT,这个时候我们需要使用pipeline 来解决多次调用,降低这个N, 这样可以提高效率。

1.png

一次请求/响应服务器能实现处理新的请求即旧的请求还没被响应。可以一次发送多个命令到服务器,而不用等待回复,只需要在最后的时候统一的结果回复。这就是pipeline的简单介绍。这并不是一个新的技术,但是确实是高效的为我们解决了更多的问题。

2.png

我们将通过例子,来给大家实际的讲解和验证我们所阐述的观点和问题。假如处理net和server直接的网络包传输需要 0.1秒 ,(整数方便计算)那么我们使用N个命令的时候,报文至少需要 0.1XN 才可以完成。比如Redis 每秒可以处理100个命令,但是我们这个实际的客户端只能发送10次。这样就降低了我们处理的能力。而使用管道(pipeline)可以很好的解决这个问题,我们可以一次发送多个命令,减少网络的往返时间。

pipeline 通过减少客户端与Redis通信的次数来实现降低往返的时间,而且pipeline实现的原理是队列,原则上是先进先出,这样可以保证数据的顺序性。

pipeline python实战

我们将通过实际的python的例子,来实际的操作和对比,由于网络环境和数据量值有限,所以可能演示对比的结果并不太明显,只是通过借鉴别人的这个脚本,为大家提供一个使用的方法和对比的思路。

# -*- coding:utf-8 -*-
import redis
import time
from concurrent.futures import ProcessPoolExecutor
r = redis.Redis(host='127.0.0.1', port=6379)
def pipeline_yes():
    with r.pipeline(transaction=True) as p:
        p.sadd('seta', 1).sadd('seta', 2).srem('seta', 2).lpush('lista', 1).lrange('lista', 0, -1)
        start = time.time()
        p.execute()
    print time.time() - start
def pipeline_no():
    start = time.time()
    r.sadd('seta', 31)
    r.sadd('seta', 23)
    r.srem('seta', 23)
    r.lpush('listsa', 1)
    r.lrange('listsa', 0, -1)
    print time.time() - start
def worker():
    for i in range(10):
        pipeline_yes()   #可以通过这个函数调用变更使用pipeline模式还时普通模式
with ProcessPoolExecutor(max_workers=10) as pool:    #开启多进程
    for _ in range(10):        
        pool.submit(worker)
shell下的官方推荐操作
#!/bin/bash
for((i=1;i<=1000000;i++))
do
echo "set k$i v$i" >> /tmp/_t.txt
done
通过命令行使用管道
shell> unix2dos /tmp/_t.txt
shell> cat /tmp/_t.txt | /usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -n 0 --pipe

我们可以通过shell 脚本的方式,尝试多次调用客户端 进行插入数据和后边这种使用pipeline导入进行对比,可以看到速度提高还是比较明显的。
我们除了pipeline的模式之外,还有lua脚本的模式,同样可以批量传递命令来处理,只是这个是把我们的操作封装到lua脚本内,也可以提高速度,当然还有其他用途,后边我将详细讲解。

Redis lua脚本详解

lua 科普

Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。
Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。

为什么使用Lua

减少RTT,可以将多个请求通过脚本形式一次发送,减少网络的延时以及请求的次数。(和pipeline很类似)

原子操作:Redis 会将整个脚本作为一个整体执行,中间不会插入其他的命令。

命令复用: 客户端发送一个脚本之后,会存储在Redis中,这样可以在其他的客户端也可以调用这个脚本,不需要重新写同样的代码逻辑来完成。

Lua 详解

菜鸟Lua教程

篇幅有限,这里暂时不对Lua的语法进行过多的讲解

Redis数据类型与Lua数据类型转换

数据类型之间的转换遵循这样的一个设计原则:如果将一个Redis的值转换成Lua的值,然后在将转换所得的Lua的值转换回Redis值,那么这个转换所得的Redis值应该和最初的Redis值一样。简单总结:这个值,必须可以在lua和Redis之间互换,而且值不变。换句话说,Lua类型和Redis类型之间存在着一一对应的关系。

Redis数据结构 lua数据结构
integer number
bulk String
multi bulk table
status lua的table中有一个ok做对应
error lua的table中有一个err做对应
Nil   bulk, Nil multi bulk lua的boolean的false

脚本的原子性和错误处理

Redis 使用单个的Lua解释器去运行所有脚本,并且Redis也保证脚本会以原子性的方式执行:当某个脚本正在运行的时候,不会有其他的脚本或者命令被执行。在别的客户端看来,脚本的效果要么是不可见的,要不就是已经完成的。所以脚本尽量在执行的时候不要做sleep 缓慢操作,如果堆积的话,其他客户端会因为等待而无法处理其他的脚本或命令请求。
当Redis执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本的错误,错误的输出信息会说明造成错误的原因。

EVAL命令的实现

所有被 Redis 执行的 Lua 脚本, 在 Lua 环境中都会有一个和该脚本相对应的无参数函数: 当调用 EVAL 命令执行脚本时, 程序第一步要完成的工作就是为传入的脚本创建一个相应的 Lua 函数。

shell> redis-cli --eval path/to/redis.lua KEYS[1] KEYS[2] , ARGV[1] ARGV[2] ...
 --eval 告诉redis-cli读取兵执行后边的lua脚本
 path/to/redis.lua   是lua脚本的路径
 KEYS[1] KEYS[2],是要操作的键,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取
 ARGV[1] ARGV[2],参数,在lua脚本中通过ARGV[1], ARGV[2]获取。
 注意: KEYS和ARGV中间的 ',' 两边的空格,不能省略。

我们将通过一个例子,来验证上述的说明:

test.lua

return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}

3.png

图片上就是我们执行的结果,大家可以尝试一下。简单的使用就是这么神奇,哈哈!为了更清晰一些,我们将讲述,在redis内,如何调用这个命令。

下边我们可以通过官方推荐的例子,在redis内操作,来看一下,这个命令究竟如何使用:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20
1) "username"
2) "age"
3) "jack"
4) "20"
# 注意:  中间的  2 代表了keys的个数

lua脚本的使用由于redis在2.6以后的版本中都做了支持,使用起来比较方便。大家可以根据自己的lua水平,可以多写一些例子,进行测试一下。多多练习,天天向上!

注意事项:Redis 在脚本中禁止使用的项目
不提供访问系统状态状态的库(比如系统时间库)。
禁止使用 loadfile 函数。
如果脚本在执行带有随机性质的命令(比如 RANDOMKEY ),或者带有副作用的命令(比如 TIME )之后,试图执行一个写入命令(比如 SET ),那么 Redis 将阻止这个脚本继续运行,并返回一个错误。
如果脚本执行了带有随机性质的读命令(比如 SMEMBERS ),那么在脚本的输出返回给 Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
用 Redis 自己定义的随机生成函数,替换 Lua 环境中 math 表原有的 math.random 函数和 math.randomseed 函数,新的函数具有这样的性质:每次执行 Lua 脚本时,除非显式地调用 math.randomseed ,否则 math.random 生成的伪随机数序列总是相同的。

脚本复用 EVALSHA 与 SCRIPT

在前边我们介绍 eval命令的时候,应该有提到,所有脚本执行执行都会创建一个函数,那么我们下边将通过以此来展示为大家讲解。

EVAL命令在使用的过程中,每次执行都需要发送一次脚本主题内容,那这样的频繁操作会消耗我们的网络带宽。那么我们可以通过Redis的一个内部缓存机制,来解决这个问题。

EVALSHA命令,她的作用和EVAL一样,都是可以执行脚本,但是这个命令第一个参数不是脚本,而是脚本的一个SHA1校验和。

我们可以通过SCRIPT LOAD 来完成 Lua脚本的缓存还有获取 脚本的SHA1值,这样通过EVALSHA 命令来重复执行该脚本,而不用每次都通过带宽来传输脚本内容,节省网络带宽。

我们仍然使用我们上边的脚本为例子,下边为大家演示,如果使用script 配合 evalsha命令完成我们代码缓存到redis ,并且复用:

[root@redis2 tool]# cat test.lua 
return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}
[root@redis2 tool]# /usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379 script load "$(cat test.lua)"    #加载lua脚本,并获取sha1值
"a42059b356c875f0717db19a51f6aaca9ae659ea"
[root@redis2 tool]# 
[root@redis2 tool]# 
[root@redis2 tool]# /usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> 
127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 name age guzhou 30    #通过执行evalsha命令完成脚本调用
1) "name"
2) "age"
3) "guzhou"
4) "30"
127.0.0.1:6379> 
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 name age guzhou 30
1) "name"
2) "age"
3) "guzhou"
4) "30"                #执行效果一样
127.0.0.1:6379> 

4.png

通过以上的例子,我们可以看到如何去加载我们的lua脚本,当我们缓存脚本之后,如何使用evalsha命令来重复使用脚本的。

/usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379 script load "$(cat test.lua)"   #通过script load 加载脚本,加载成功会返回sha1值
evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 name age guzhou 30
#通过 调用脚本的 sha1 值,完成脚本复用。参数传递可以参考eval命令

script 脚本管理

关于如何使用script load 加载脚本到缓存中,这里就不详细描述了,我们下边来解决一下几个问题:

1.如何判断脚本是否在内存中?
2.如何清理内存中的脚本?
3.脚本出现阻塞怎么办?

首先我们可以看一下 script exists sha1 这个命令,其中sha1代表脚本的sha1值

5.png

脚本存在返回1 脚本不存在返回 0

清理Redis内存中以加载的所有lua 脚本: script flush

6.png

我们通过 script flush 来完成脚本的清理,执行后可以看到脚本已经不存在了。

关闭正在执行的脚本 script kill

在脚本执行的过程中,可能会由于各种原因,导致脚本执行锁死,或者无法正常结束。由于单线程会影响到我们后续其他客户端的访问。下边我们模拟脚本执行无限循环的场景,仅供参考。7.png当我们从其他客户端打开时,访问数据,会提示我们Redis is busy。 有提示告诉我们该如何解决现有的问题8.png我们可以通过 script kill 命令来结束脚本的运行,来保证我们的服务处于可用状态。

9.png

脚本利器、双刃剑。通过script kill 的例子,可以看到假如 lua脚本出现问题,会造成什么样的后果。极有可能需要停服维护,危害可见一斑,希望大家在使用的时候,或者编码的时候谨慎、谨慎、谨慎!如果使用不当、对我们性能和可用性的破坏也是难以想象的!

结束语

通过以上的演示和讲解,基本上都了解了pipeline以及lua脚本的功能,后续我们将在场景应用的高级部分讲解比如我们如何使用脚本,来控制商品的秒杀? 以及如何配合nginx+lua+redis来实现动态的防火墙等等。 期待大家能坚持下去,更高级的部分,将会为大家讲解更多的实用的场景。

版权声明:
作者:WaterBear
链接:https://l-t.top/2127.html
来源:雷霆运维
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录