详解python调用redis lua内嵌脚本的高级用法

Lua 脚本功能是 Reids 2.6 版本开始提供的高级功能, 我们可以通过redis内嵌的 Lua 环境的进行搞复杂的需求。
使用内置的lua脚本环境可以解决Redis长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。


该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。 

http://xiaorui.cc/?p=3052

我对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


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

5 Responses

  1. 洋洋 2016年9月20日 / 上午8:32

    厉害

  2. 胡阳 2016年3月28日 / 上午10:44

    峰云就是牛

发表评论

邮箱地址不会被公开。 必填项已用*标注