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
厉害
笔误…
笔误..
峰云就是牛
大神别装了 !