Lua 脚本功能是 Reids 2.6 版本开始提供的高级功能, 我们可以通过redis内嵌的 Lua 环境的进行搞复杂的需求。
使用内置的lua脚本环境可以解决Redis长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。
我对redis lua的两个粗浅的看法:
1. Lua作为脚本自己本身的性能是很高效的,有尝试过nginx lua组合的朋友应该能感受到。redis lua适合在单机单实例中使用,因为现在市面上的redis proxy都没有实现对于lua的调度支持。 大多数redis proxy代理只是实现了command和key的一致性hash而已。
问题是我们为了高性能往往都是一个实例,一个cpu核心. 所以在redis集群的场景下这redis不适合.
2. redis lua 虽然内置了很多的模块组件,已经足够我们去写复杂的逻辑了。 但redis lua为了安全着想,屏蔽了很多的基本命令。 比如 os.time(), Date, hash 。
我为什么会需要os.Time() ,因为我需要做时序队列,为什么需要hash,因为有去重的需求,我把文档做成hash md5,扔到set集合里。但redis lua没有内置hash的函数或方法。
值得高兴的是redis lua含有解析构建json的cjson,还有能处理二进制MessagePack的cMessagePack。
Redis 对 Lua 环境做了一些列相应的安全措施:
1. 不提供访问系统状态状态的库,时间也不可以。 虽然通过redis.call(“TIME”) 可以拿到时间戳,但这时间戳不能写入任务一个键值里,只能提供比对的功能。
2. 禁止使用 loadfile 函数, 也就是 require “os”
3. 如果脚本执行了带有随机性质的读命令(比如 SMEMBERS ),那么在脚本的输出返回给 Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
经过这一系列的调整之后, Redis 可以保证被执行的脚本:
1. 没有有害的随机性。
2. 对于同样的输入参数和数据集,总是产生相同的写入命令。
最重要的一点是redis lua脚本会首先尽量的执行脚本里的逻辑,redis会阻塞其他的指令操作,因为内置的lua进行数据库操作是不经过网络io这一层,所以他的执行效率是最快的。但如果你初次之外还有一堆的小请求,那么对于整体的性能来说肯定会有所影响的。
所以说,一定不要让你的lua脚本执行时间太长,要分而治之,不要把所有逻辑放到一个lua脚本里面。
redis是可以针对lua进行超时控制的。默认是不允许lua脚本超过5秒的。 – redis: command=config name=lua-time-limit value=100 ,时间单位是ms毫秒.
127.0.0.1:6379> config get lua-time-limit 1) "lua-time-limit" 2) "5000" 127.0.0.1:6379>
下面是redis lua的基本用法,lua本身语法也干练,所以大家看起来也不觉得难。redis.call(command命令.) argv, keys是参数,必须是这两个名字.
lua1 = """
redis.call("select", ARGV[1])
return redis.call("get",KEYS[1])
"""
script1 = r.register_script(lua1)
下面是个比较完整的例子:
#xiaorui.cc
import redis
pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
r = redis.Redis(connection_pool=pool)
lua1 = """
redis.call("select", ARGV[1])
return redis.call("get",KEYS[1])
"""
script1 = r.register_script(lua1)
lua2 = """
redis.call("select", ARGV[1])
local ret = redis.call("get",KEYS[1])
redis.call("select", ARGV[2])
return ret
"""
script2 = r.register_script(lua2)
print r.get("mykey")
print script2( keys=["mykey"], args = [1,0] )
print r.get("mykey"), "ok"
print
print r.get("mykey")
print script1( keys=["mykey"], args = [1] )
print r.get("mykey")
我在使用python调用lua中遇到的问题, 提示说是没有os模块,如果我require “os”,也会提示require错误的。
$ python r.py
Traceback (most recent call last):
File "r.py", line 45, in <module>
script( keys=["task_queue","task_queue_faild","task_queue_ack"] )
File "/Library/Python/2.7/site-packages/redis/client.py", line 2699, in __call__
return client.evalsha(self.sha, len(keys), *args)
File "/Library/Python/2.7/site-packages/redis/client.py", line 1944, in evalsha
return self.execute_command('EVALSHA', sha, numkeys, *keys_and_args)
File "/Library/Python/2.7/site-packages/redis/client.py", line 573, in execute_command
return self.parse_response(connection, command_name, **options)
File "/Library/Python/2.7/site-packages/redis/client.py", line 585, in parse_response
response = connection.read_response()
File "/Library/Python/2.7/site-packages/redis/connection.py", line 582, in read_response
raise response
redis.exceptions.ResponseError: Error running script (call to f_e0b13b03d37c6fda9a68b66a7954133179fa3b1c): @enable_strict_lua:15: user_script:2: Script attempted to access unexisting global variable 'os' #xiaorui.cc
下面的问题是引用了时间引起的, 上面说过redis不可以把获取的时间,insert到任何结构类型里。
下面是我dtrace的追踪调用的日志, 没有看到他跟redis有多余的网络请求.
dtrace: error on enabled probe ID 2496 (ID 292: syscall::madvise:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2438 (ID 408: syscall::sendto:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2438 (ID 408: syscall::sendto:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2438 (ID 408: syscall::sendto:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2542 (ID 200: syscall::recvfrom:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2496 (ID 292: syscall::madvise:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2496 (ID 292: syscall::madvise:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2438 (ID 408: syscall::sendto:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2542 (ID 200: syscall::recvfrom:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2496 (ID 292: syscall::madvise:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2496 (ID 292: syscall::madvise:return): invalid user access in action #5 at DIF offset 0 dtrace: error on enabled probe ID 2496 (ID 292: syscall::madvise:return): invalid user access in action #5 at DIF offset 0
这里是本文最有料的地方, redis lua的各方面优缺点我都有描述。 我这边最看重他的是节省网络io的优点。 下面的代码是伪业务逻辑,其实我线上的业务逻辑更加繁琐,对于redis的请求次数更多。
下面代码本身不是很复杂,但是需要来回的从redis pull push操作。另外键值中的value基本是在200KB大小,所以他是来回的经过redis网络消耗可想而知。
首先我会判断他的service_level级别,如果是ddos,那么我会根据队列的大小进行lpop队列,zadd时序队列,我这边针对每条记录进行cjson解析json,如果他的字段符合depth <10 ,我还会进行incr操作。。。。
上面的伪业务逻辑不需要理解,你需要着想的是,如果这里不采用redis lua脚本,这一次次的网络io花费的时间… … 下面是我写的一个复杂的python redis lua场景脚本,代码很easy。
#xiaorui.cc
import time
import json
import requests
import redis
pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
r = redis.Redis(connection_pool=pool)
content = requests.get("http://www.hao123.com/").content
task_queue = "task_queue"
task_queue_faild = "task_queue_faild"
task_queue_ack = "task_queue_ack"
lua2 = """
local result = {}
local level = redis.call("get","service_level")
print(level)
if (level == "ddos") then
local countz = redis.call("LLEN",KEYS[1])
local offset = countz
print(offset)
if (countz > 1000) then
offset = 100
end
for i=offset,1,-1 do
local ret = redis.call("lpop",KEYS[1])
local res = redis.call("rpush",KEYS[2],ret)
local res = redis.call("zadd",KEYS[3],KEYS[4],ret)
if (cjson.decode(ret)["depth"] < 10 ) then
local link_id = redis.call("incr", "counter")
end
end
table.insert(result, ret);
end
return result
else
return “xiaorui.cc"
end
"""
script = r.register_script(lua2)
r.set("service_level","ddos")
for i in range(1000):
r.rpush(task_queue,json.dumps({"depth":i,"blog":"xiaorui.cc","content":content}))
s = time.time()
for i in range(1):
r.rpush(task_queue,json.dumps({"depth":3,"blog":"xiaorui.cc","content":content}))
ldata = script( keys=["task_queue","task_queue_faild","task_queue_ack",int(time.time())])
print len(ldata)
print time.time()-s
这段redis lua运行后的结果是这样的,
#xiaorui.cc
# ruifengyun @ xiaorui in ~ [16:58:19] tty:s004 L:1 N:341 C:0
python r.py
100
1.18791913986
# ruifengyun @ xiaorui in ~ [16:58:39] tty:s004 L:1 N:342 C:0 python r.py
100
1.47876381874
# ruifengyun @ xiaorui in ~ [16:58:49] tty:s004 L:1 N:343 C:0
$ python r.py
100
1.42990279198
下面是redis server输出的日志。
#xiaorui.cc 19600:M 27 Mar 09:35:46.078 * Background saving terminated with success 19600:M 27 Mar 09:40:47.072 * 100 changes in 300 seconds. Saving... 19600:M 27 Mar 09:40:47.085 * Background saving started by pid 21815 21815:C 27 Mar 09:41:17.898 * DB saved on disk 19600:M 27 Mar 09:41:18.044 * Background saving terminated with success 3810 ddos 4711 19600:M 27 Mar 09:46:19.015 * 100 changes in 300 seconds. Saving... 19600:M 27 Mar 09:46:19.023 * Background saving started by pid 22030
在stackoverflow看到了一个帖子,是有关什么时候才用到redis lua组合的讨论. 很多论点跟我想的一样…
http://stackoverflow.com/questions/30869919/redis-lua-when-to-really-use-it
END

厉害
笔误…
笔误..
峰云就是牛
大神别装了 !