7.高阶篇:Redis 管道与脚本应用
Redis 管道应用
Redis是一种基于客户端--服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:
2.服务端等待命令执行。
3.服务端执行命令。
4.返回结果
这个过程被称为 Round trip time (简称RTT,往返时间)。在实际应用中,可能一个事物或者操作,需要很多次的调用,那么需要N次调用,就需要消耗N次RTT,这个时候我们需要使用pipeline 来解决多次调用,降低这个N, 这样可以提高效率。
一次请求/响应服务器能实现处理新的请求即旧的请求还没被响应。可以一次发送多个命令到服务器,而不用等待回复,只需要在最后的时候统一的结果回复。这就是pipeline的简单介绍。这并不是一个新的技术,但是确实是高效的为我们解决了更多的问题。
我们将通过例子,来给大家实际的讲解和验证我们所阐述的观点和问题。假如处理net和server直接的网络包传输需要 0.1秒 ,(整数方便计算)那么我们使用N个命令的时候,报文至少需要 0.1XN 才可以完成。比如Redis 每秒可以处理100个命令,但是我们这个实际的客户端只能发送10次。这样就降低了我们处理的能力。而使用管道(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由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。
为什么使用Lua
减少RTT,可以将多个请求通过脚本形式一次发送,减少网络的延时以及请求的次数。(和pipeline很类似)
原子操作:Redis 会将整个脚本作为一个整体执行,中间不会插入其他的命令。
命令复用: 客户端发送一个脚本之后,会存储在Redis中,这样可以在其他的客户端也可以调用这个脚本,不需要重新写同样的代码逻辑来完成。
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]}
图片上就是我们执行的结果,大家可以尝试一下。简单的使用就是这么神奇,哈哈!为了更清晰一些,我们将讲述,在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水平,可以多写一些例子,进行测试一下。多多练习,天天向上!
禁止使用 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>
通过以上的例子,我们可以看到如何去加载我们的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值
脚本存在返回1 脚本不存在返回 0
清理Redis内存中以加载的所有lua 脚本: script flush
我们通过 script flush 来完成脚本的清理,执行后可以看到脚本已经不存在了。
关闭正在执行的脚本 script kill
在脚本执行的过程中,可能会由于各种原因,导致脚本执行锁死,或者无法正常结束。由于单线程会影响到我们后续其他客户端的访问。下边我们模拟脚本执行无限循环的场景,仅供参考。当我们从其他客户端打开时,访问数据,会提示我们Redis is busy。 有提示告诉我们该如何解决现有的问题我们可以通过 script kill 命令来结束脚本的运行,来保证我们的服务处于可用状态。
脚本利器、双刃剑。通过script kill 的例子,可以看到假如 lua脚本出现问题,会造成什么样的后果。极有可能需要停服维护,危害可见一斑,希望大家在使用的时候,或者编码的时候谨慎、谨慎、谨慎!如果使用不当、对我们性能和可用性的破坏也是难以想象的!
结束语
通过以上的演示和讲解,基本上都了解了pipeline以及lua脚本的功能,后续我们将在场景应用的高级部分讲解比如我们如何使用脚本,来控制商品的秒杀? 以及如何配合nginx+lua+redis来实现动态的防火墙等等。 期待大家能坚持下去,更高级的部分,将会为大家讲解更多的实用的场景。