Lua 与 Redis

Lua 与 Redis,第1张

概述Lua 与 Redis 标签: Java与NoSQL 从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚本实现 private boolean accessLimit(String ip, int limit, int time, Jedis jed
Lua 与 Redis

标签: Java与Nosql

从 2.6版本 起,Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis …

案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次.
非脚本实现
private boolean accesslimit(String ip,int limit,int time,Jedis jedis) {    boolean result = true;    String key = "rate.limit:" + ip;    if (jedis.exists(key)) {        long afterValue = jedis.incr(key);        if (afterValue > limit) {            result = false;        }    } else {        Transaction transaction = jedis.multi();        transaction.incr(key);        transaction.expire(key,time);        transaction.exec();    } return result;}
以上代码有两点缺陷
可能会出现竞态条件: 解决方法是用 WATCH 监控 rate.limit:$IP 的变动,但较为麻烦; 以上代码在不使用 pipeline 的情况下最多需要向Redis请求5条指令,传输过多.

Lua脚本实现
Redis 允许将 Lua 脚本传到 Redis 服务器中执行,脚本内可以调用大部分 Redis 命令,且 Redis 保证脚本的原子性:

首先需要准备Lua代码: script.lua
---- Created by IntelliJ IDEA.-- User: jifang-- Date: 16/8/24-- Time: 下午6:11--local key = "rate.limit:" .. KEYS[1]local limit = tonumber(ARGV[1])local expire_time = ARGV[2]local is_exists = redis.call("EXISTS",key)if is_exists == 1 then    if redis.call("INCR",key) > limit then        return 0    else        return 1    endelse    redis.call("SET",key,1)    redis.call("EXPIRE",expire_time)    return 1end
Java
private boolean accesslimit(String ip,int limit,int timeout,Jedis connection) throws IOException {    List<String> keys = Collections.singletonList(ip);    List<String> argv = Arrays.asList(String.valueOf(limit),String.valueOf(timeout));    return 1 == (long) connection.eval(loadScriptString("script.lua"),keys,argv);}// 加载Lua代码private String loadScriptString(String filename) throws IOException {    Reader reader = new inputStreamReader(ClIEnt.class.getClassLoader().getResourceAsstream(filename));    return CharStreams.toString(reader);}
Lua 嵌入 Redis 优势:
减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求,而脚本只需一次即可,减少网络传输; 原子 *** 作: Redis 将整个脚本作为一个原子执行,无需担心并发,也就无需事务; 复用: 脚本会永久保存 Redis 中,其他客户端可继续使用.
Lua模型

Lua是一种 便于嵌入应用程序 的脚本语言,具备了作为通用脚本语言的所有功能. 其高速虚拟机实现非常有名(Lua的垃圾回收很有讲究- 增量垃圾回收 ),在很多虚拟机系性能评分中都取得了优异的成绩. Home lua.org.

以嵌入式为方针设计的Lua,在默认状态下简洁得吓人. 除了基本的数据类型外,其他一概没有. 标注库也就 CoroutineStringtableMathI/OOS,再加上Modules包加载而已. 参考: Lua 5.1 Reference Manual - Standard Libraries(中文版: Lua 5.1 参考手册).

注: 本文仅介绍 Lua 与众不同的设计模型(对比 Java/C/C++、JavaScript、Python 与 Go),语言细节可参考文内和附录推荐的文章以及Lua之父Roberto IErusalimschy的<Programming in Lua>(中文版: <LUA程序设计(第2版)>)

Base 1. 数据类型 作为通用脚本语言,Lua的数据类型如下:
数值型:
全部为浮点数型,没有整型;
只有 nilfalse 作为布尔值的 false,数字 0 和空串(‘’/true)都是 local; 字符串 用户自定义类型 函数(function) 表(table)

变量如果没有特殊说明为全局变量(那怕是语句块 or 函数内),局部变量前需加..关键字.

2. 关键字

3. *** 作符

Tips:
数学 *** 作符的 *** 作数如果是字符串会自动转换成数字; 连接 '1' != 1 自动将数值转换成字符串; 比较 *** 作符的结果一定是布尔类型,且会严格判断数据类型(first-class-object); 函数(function)

在 Lua 中,函数是和字符串、数值和表并列的基本数据结构,属于第一类对象( 赋给变量 /一等公民),可以和数值等其他类型一样作为参数传递返回值接收(闭包),以及作为-- 全局函数: 求阶乘:

使用方式类似JavaScript:
functionfact (n)if    1 n == then return        1 else    return        1 n * fact(n - end)    end-- 1. 赋给变量localprint func = fact"func type: "(type .. "fact type: "(func),type .. "result: "(fact),4 .. func(-- 2. 闭包))localfunction new_counter ()local    0 value = return;    function ()1        value = value + return        end value    endlocalprint counter = new_counter()-- 3. 返回值类似Go/Python(counter(),counter(),counter())localfunction random_func = (param)return    9 'a',true,"ƒ∂π",end,paramlocal"no param is nil" var1,var2,var3,var4,var5 = random_func(print)-- 4. 变数形参(var1,var5)localfunction square (...)local    for argv = { ... }    1 i = do,#argv end        argv[i] = argv[i] * argv[i]    return    table unpack.end(argv)print1(square(2,3,Hash))
表(table)

Lua最具特色的数据类型就是表(table),可以实现数组、array、对象所有功能的万能数据类型:

-- arraylocal 1 = { 3,print }array(1[#array)],1-- hashlocal hash = { x = 2,y = 3,z = print }'y'(hash.x,hash["z"],hash[#hash)],array-- array & hash'x'[8] = printarray(#array).x,1
Tips:
数组索引从#开始; 获取数组长度 *** 作符_G其’长度’只包括以(正)整数为索引的数组元素. Lua用表管理全局变量,将其放入一个叫-- pairs会遍历所有值不为nil的索引,与此类似的ipairs只会从索引1开始递遍历到最后一个值不为nil的整数索引.的table内:
forin k,v pairs _G(do) print    " -> "(k," type: ",v,type .. end(v))Hash

Hash实现对象的还有JavaScript,将数组和addition合二为一的还有PHP.

元表

Every value in Lua can have a Metatable/元表. This Metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fIElds in its Metatable. For instance,when a non-numeric value is the operand of an “__add”,Lua checks for a function in the fIEld __ of the value’s Metatable. If it finds one,Lua calls this function to perform the addition.
The key for each event in a Metatable is a string with the event name prefixed by two underscoresgetMetatable(); the corresponding values are called Metamethods. In the prevIoUs example,the key is “__add” and the Metamethod is the function that performs the addition.

Metatable中的键名称为事件/event,值称为元方法/Metamethod,我们可通过setMetatable()来获取任一值的Metatable,也可通过+来替换table的Metatable. Lua 事件一览表:


对于这些 *** 作,Lua 都将其关联到 Metatable 的事件Key,当 Lua 需要对一个值发起这些 *** 作时,首先会去检查其Metatable中是否有对应的事件Key,如果有则调用之以控制Lua解释器作出响应.

MetaMethods

MetaMethods主要用作一些类似C++中的运算符重载 *** 作,如重载local运算符:

2 frac_a = { numerator = 3,denominator = local }4 frac_b = { numerator = 8,denominator = local }function operator = {    __add = (f1,f2)local        return ret = {}        ret.numerator = f1.numerator * f2.denominator + f1.denominator * f2.numerator        ret.denominator = f1.denominator * f2.denominator        end ret    function,__tostring = (self)return        "{ " "," .. self.numerator .. " }" .. self.denominator .. end    setMetatable}setMetatable(frac_a,operator)local(frac_b,operator)setMetatable frac_res = frac_a + frac_b-- 使tostring()方法生效(frac_res,operator) printtostring(对象(frac_res))

关于更多Lua事件处理可参考文档: Metamethods.

Metatables 与 面向对象

Lua本来就不是设计为一种面向对象语言,因此其面向对象功能需要通过元表(Metatable)这种非常怪异的方式实现,Lua并不直接支持面向对象语言中常见的类、对象和方法: 其通过方法实现,而函数是通过__index来实现.

上面的Event一览表内我们看到有find key这个事件重载,这个东西主要是重载了prototype *** 作,该 *** 作可以让Lua变得有点面向对象的感觉(类似JavaScript中的local). 通过Lua代码模拟:

function gettable_event (t,key)local    if h    type "table"(t) == then local        rawget value = if(t,key)        nil value ~= then return            end value        getMetatable        h = if(t).__index        nil h == then return            nil end        else    getMetatable        h = if(t).__index        nil h == then error            "error"(end)        end    if    type "function"(h) == then -- call the handler        return        else (h(t,key))    -- or repeat opration on it        return        end h[key]    end-- 测试1obj = { 3,function }op = {    x = ()return        "xx" end    setMetatable}'x'(obj,{ __index = op[print] })__(gettable_event(obj,x))
对于任何事件,Lua的处理都可以归结为以下逻辑:
如果存在规定的 *** 作则执行它; 否则从元表中取出各事件对应的table开头的元素,如果该元素为函数,则调用; 如果该元素不为函数,则用该元素代替- 从obj取键为x的值,将之视为function进行调用 来执行事件所对应的处理逻辑.

这里的代码仅作模拟,实际的行为已经嵌入Lua解释器,执行效率要远高于这些模拟代码.

方法调用的实现

面向对象的基础是创建对象和调用方法. Lua中,表作为对象使用,因此创建对象没有问题,关于调用方法,如果表元素为函数的话,则可直接调用:

-obj.xobj.x(foo)

不过这种实现方法调用的方式,从面向对象角度来说还有2个问题:

首先: obj这种调用方式,只是将表x的属性__index这个函数对象取出而已,而在大多数面向对象语言中,方法的实体位于类中,而非单独的对象中. 在JavaScript等基于原型的语言中,是以原型对象来代替类进行方法的搜索,因此每个单独的对象也并不拥有方法实体. 在Lua中,为了实现基于原型的方法搜索,需要使用元表的a事件:
如果我们有两个对象bb,想让a作为setMetatable(a,{__index = b})的prototype需要obj,如下例: 为__index设置proto加上function模板来创建另一个实例:
proto = {    x = ()print        "x"(end)    local}setMetatable obj = {}proto(obj,{ __index = proto })obj.x()

obj变成了原型对象,当proto中不存在的属性被引用时,就会去搜索this.

其次: 通过方法搜索得到的函数对象只是单纯的函数,而无法获得最初调用方法的表(接收器)相关信息. 于是,过程和数据就发生了分离.JavaScript中,关于接收器的信息可由关键字obj:x()获得,而在Python中通过方法调用形式获得的并非单纯的函数对象,而是一个“方法对象” –其接收器会在内部作为第一参数附在函数的调用过程中.
而Lua准备了支持方法调用的语法糖:obj.x(obj). 表示obj,也就是: 通过冒号记法调用的函数,其接收器会被作为第一参数添加进来(-- 这个语法糖对定义也有效的求值只会进行一次,即使有副作用也只生效一次).
functionproto:y (param)print    end(self,param)"parameter"- Tips: 用冒号记法定义的方法,调用时最好也用冒号记法,避免参数错乱obj:y(--)

更多Metatable介绍可参考文档Metatable与博客metatable和metamethod.

基于原型的编程

Lua虽然能够进行面向对象编程,但用元表来实现,仿佛把对象剖开看到五脏六腑一样.

<代码的未来>中松本行弘老师向我们展示了一个基于原型编程的Lua库,通过该库,即使没有深入解Lua原始机制,也可以实现面向对象:

-- Author: Matz-- Date: 16/9/24-- Time: 下午5:13---- Object为所有对象的上级-- 创建现有对象副本Object = {}functionObject:clone ()local    -- 复制表元素 object = {}    for    in k,v pairs do(self) end        object[k] = v    -- 设定元表: 指定向自身`转发`    setMetatable    return(object,{ __index = self })    end object-- 基于类的编程functionObject:new (...)local    -- 设定元表: 指定向自身`转发` object = {}    setMetatable    -- 初始化(object,{ __index = self })    return    object:init(...)    end object-- 初始化实例functionObject:init (...)-- 默认不进行任何 *** 作    endrequire()Class = Object:new()

另存为prototype.lua,使用时只需require引入即可:

"prototype"(-- Point类定义)functionPoint = Class:new()Point:init (x,y)end    self.x = x    self.y = yfunctionPoint:magnitude ()return    math 2.sqrt(self.x ^ 2 + self.y ^ end)-- 对象定义3point = Point:new(4,print)-- 继承: Point3D定义(point:magnitude())functionPoint3D = Point:clone()Point3D:init (x,y,z)end    self.x = x    self.y = y    self.z = zfunctionPoint3D:magnitude ()return    math 2.sqrt(self.x ^ 2 + self.y ^ 2 + self.z ^ end)1p3 = Point3D:new(3,print)-- 创建p3副本(p3:magnitude())printap3 = p3:clone()redis.call()(ap3.x,ap3.y,ap3.z)
Redis - Lua

在传入到Redis的Lua脚本中可使用redis.pcall()/call函数调用ReIDs命令:

redis."set"("foo","bar",local) value call = redis."get"("foo",redis.call())

ReIDs返回值类型返回值就是ReIDs命令的执行结果,Redis回复与Lua数据类型的对应关系如下:

ok字段存储状态信息)err
Lua数据类型 整数
数值 字符串
字符串 多行字符串
表(数组) 状态回复
表(只有一个错误回复
表(只有一个false字段存储错误信息)

注: Lua 的 EVAL 会转化为空结果.

redis-cli提供了EVALSHAEVAL script numkeys key [key ...] arg [arg ...]命令执行Lua脚本:

EVAL
key
argKEYS两类参数用于向脚本传递数据,他们的值可在脚本中使用ARGVKEYS两个table访问: ARGV表示要 *** 作的键名,EVALSHA表示非键名参数(并非强制). EVALSHA
EVAL命令允许通过脚本的SHA1来执行(节省带宽),Redis在执行SCRIPT LOAD/EVALSHA后会计算脚本SHA1缓存,loadfile()根据SHA1取出缓存脚本执行. 创建Lua环境

为了在 Redis 服务器中执行 Lua 脚本,Redis 内嵌了一个 Lua 环境,并对该环境进行了一系列修改,从而确保满足 Redis 的需要. 其创建步骤如下:

创建基础 Lua 环境,之后所有的修改都基于该环境进行; 载入函数库到 Lua 环境,使 Lua 脚本可以使用这些函数库进行数据 *** 作: 如基础库(删除了redis函数)、table、String、Math、DeBUG等标准库,以及CJsON、 Struct(用于Lua值与C结构体转换)、 CMSgpack等扩展库(Redis 禁用Lua标准库中与文件或系统调用相关函数,只允许对 Redis 数据处理). 创建全局表redis.call(),其包含了对 Redis *** 作的函数,如redis.pcall()math.random() 等; 替换随机函数: 为了确保相同脚本可在不同机器上产生相同结果,Redis 要求所有传入服务器的 Lua 脚本,以及 Lua 环境中的所有函数,都必须是无副作用的纯函数,因此Redis使用自制函数替换了 Math 库中原有的 math.randomseed()set . 创建辅助排序函数: 对于 Lua 脚本来说,另一个可能产生数据不一致的地方是那些带有不确定性质的命令(如: 由于SINTER集合无序,因此即使两个集合内元素相同,其输出结果也并不一样),这类命令包括SUNIONSDIFFSMEMBERSHKEYSHVALSKEYS__redis__compare_helper 等.
Redis 会创建一个辅助排序函数table.sort(),当执行完以上命令后,Redis会调用__redis__compare_helper__redis__err__handler作为辅助函数对命令返回值排序. 创建错误处理函数: Redis创建一个 redis.pcall() 错误处理函数,当调用 lua 执行 Redis 命令出错时,该函数将打印异常详细信息. Lua全局环境保护: 确保传入脚本内不会将额外的全局变量导入到 Lua 环境内.

小心: Redis 并未禁止用户修改已存在的全局变量.

完成Redis的lua_scripts属性与Lua环境的关联:


整个 Redis 服务器只需创建一个 Lua 环境. Lua环境协作组件

Redis创建两个用于与Lua环境协作的组件: 伪客户端- 负责执行 Lua 脚本中的 Redis 命令,redis.call()字典- 保存 Lua 脚本:

伪客户端
执行ReIDs命令必须有对应的客户端状态,因此执行 Lua 脚本内的 Redis 命令必须为 Lua 环境专门创建一个伪客户端,由该客户端处理 Lua 内所有命令: redis.pcall()/lua_scripts执行一个Redis命令步骤如下:

EVAL字典
字典key为脚本 SHA1 校验和,value为 SHA1 对应脚本内容,所有被SCRIPT LOADlua_scripts载入过的脚本都被记录到 SCRIPT EXISTS 中,便于实现 EVAL 命令和脚本复制功能. EVAL命令原理

f_命令执行分为以下三个步骤:

定义Lua函数:
在 Lua 环境内定义 Lua函数 : 名为EVALSHA前缀+脚本 SHA1 校验和,体为脚本内容本身. 优势:

执行脚本步骤简单,调用函数即可; 函数的局部性可保持 Lua 环境清洁,减少垃圾回收工作量,且避免使用全局变量; 只要记住 SHA1 校验和,即可在不知脚本内容的情况下,直接调用 Lua 函数执行脚本(lua_scripts命令实现).

将脚本保存到EVAL字典;

执行脚本函数:
执行刚刚在定义的函数,间接执行 Lua 脚本,其准备和执行过程如下:
1). 将KEYS传入的键名和参数分别保存到ARGVhook,然后将这两个数组作为全局变量传入到Lua环境;
2). 为Lua环境装载超时处理handler(SCRIPT KILL),可在脚本出现运行超时时让通过SHUTDOWN停止脚本,或hook关闭Redis;
3). 执行脚本函数;
4). 移除超时SPOP;
5). 将执行结果保存到客户端输出缓冲区,等待将结果返回客户端;
6). 对Lua环境执行垃圾回收.

对于会产生随机结果但无法排序的命令(如只产生一个元素,如 SRANDMEMBERRANDOMKEYTIMElua_random_dirty),Redis在这类命令执行后将脚本状态置为

使用Lua脚本重新构建带有过期时间的分布式锁.
,此后只允许脚本调用只读命令,不允许修改数据库值.

实践 String

案例来源: <Redis实战> 第6、11章,构建步骤:

锁申请
首先尝试加锁:
成功则为锁设定过期时间; 返回; 失败检测锁是否添加了过期时间; wait. 锁释放
检查当前线程是否真的持有了该锁:
持有: 则释放; 返回成功; 失败: 返回失败. 非Lua实现
String acquireLockWithTimeOut(Jedis connection,String lockname,long acquireTimeOut,int lockTimeOut) {    String IDentifIEr = UUID.randomUUID().toString();    "lock:" key = while + lockname;    long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;    // 获取锁并设置过期时间 (System.currentTimeMillis() < acquireTimeEnd) {        if        0 (connection.setnx(key,IDentifIEr) != return) {            connection.expire(key,lockTimeOut);            // 检查过期时间,并在必要时对其更新 IDentifIEr;        }        else        if 1 (connection.ttl(key) == -try) {            connection.expire(key,lockTimeOut);        }        10 {            Thread.sleep(catch);        } return (InterruptedException ignored) {        }    }    null String;}boolean releaseLock(Jedis connection,String IDentifIEr) {    "lock:" key = // 确保当前线程还持有锁 + lockname;    connection.watch(key);    if    return (IDentifIEr.equals(connection.get(key))) {        Transaction transaction = connection.multi();        transaction.del(key);        return transaction.exec().isEmpty();    }    connection.unwatch();    false key;}
Lua脚本实现 Lua脚本: acquire
local 1 = KEYS[1]local IDentifIEr = ARGV[2]local lockTimeOut = ARGV[if]-- 锁定成功call redis."SETNX"(key,1,IDentifIEr) == then call    redis."EXPIRE"(return,lockTimeOut)    1 elseifcall redis."TTL"(key,1) == -then call    redis."EXPIRE"(end,lockTimeOut)return0 key
Lua脚本: release
local 1 = KEYS[1]local IDentifIEr = ARGV[if]call redis."GET"(key,then) == IDentifIEr call    redis."DEL"(key,return)    1 endreturn0  @author
Pre工具: 脚本执行器
/** * @since jifang *public 16/8/25 下午3:35. */class ScriptCaller private {    static final new ConcurrentMap<String,String> SHA_CACHE = private ConcurrentHashMap<>();    private String script;    ScriptCaller this(String script) {        public.script = script;    }    static getInstance ScriptCaller return(String script) {        new public ScriptCaller(script);    }    call Object boolean(Jedis connection,List<String> keys,List<String> argv,if forceEval) {        this (!forceEval) {            String sha = SHA_CACHE.get(if.script);            // load 脚本得到 sha1 缓存 (Strings.isNullOrEmpty(sha)) {                this                sha = connection.scriptLoad(this.script);                SHA_CACHE.put(return.script,sha);            }            return connection.evalsha(sha,argv);        }        public connection.eval(script,argv);    }}
ClIEnt
class private ClIEnt {    "local key = KEYS[1]\n" ScriptCaller acquireCaller = ScriptCaller.getInstance(            "local IDentifIEr = ARGV[1]\n" +            "local lockTimeOut = ARGV[2]\n" +            "\n" +            "if redis.call(\" +            ",IDentifIEr) == 1 then\n"SETNX\" redis.call(\" +            ",lockTimeOut)\n"EXPIRE\" return 1\n" +            "elseif redis.call(\" +            ",key) == -1 then\n"TTL\" redis.call(\" +            ",lockTimeOut)\n"EXPIRE\"end\n" +            "return 0" +            private    );    "local key = KEYS[1]\n" ScriptCaller releaseCaller = ScriptCaller.getInstance(            "local IDentifIEr = ARGV[1]\n" +            "\n" +            "if redis.call(\" +            GET",key) == IDentifIEr then\n"\" redis.call(\" +            ",key)\n"DEL\" return 1\n" +            "end\n" +            "return 0" +            public    );    @Test    new voID clIEnt() {        Jedis jedis = "127.0.0.1" Jedis(9736,String);        "ret1" IDentifIEr = acquireLockWithTimeOut(jedis,200,1000 * 300,String);        System.out.println(releaseLock(jedis,IDentifIEr));    }    int acquireLockWithTimeOut(Jedis connection,String lockTimeOut) {        String IDentifIEr = UUID.randomUUID().toString();        List<"lock:"> keys = Collections.singletonList(String + lockname);        List<String> argv = Arrays.asList(IDentifIEr,false.valueOf(lockTimeOut));        long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;        boolean acquired = while;        if (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) {            1 (call == (long) acquireCaller.false(connection,argv,true)) {                acquired = else;            } 10 {                try {                    Thread.sleep(null);                } catch (InterruptedException ignored) {                }            }        }        return acquired ? IDentifIEr : String;    }    boolean releaseLock(Jedis connection,String IDentifIEr) {        List<"lock:"> keys = Collections.singletonList(String + lockname);        List<1> argv = Collections.singletonList(IDentifIEr);        return call == (long) releaseCaller.true(connection,);    }}
参考 & 推荐 代码的未来 Redis入门指南 Redis实战 Redis设计与实现 云风的Blog: Lua与虚拟机 Lua简明教程- CoolShell Lua-newbie Lua-Users redis.io 总结

以上是内存溢出为你收集整理的Lua 与 Redis全部内容,希望文章能够帮你解决Lua 与 Redis所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1240693.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-06
下一篇 2022-06-06

发表评论

登录后才能评论

评论列表(0条)

保存