Lua 语言模型与 Redis 应用

hm8617 4年前
   <p>从 2.6 版本起,Redis 开始支持 Lua 脚本,可以让开发者自己扩展 Redis。本文主要介绍了 Lua 语言不一样的设计模型(相比于Java/C/C++、JS、PHP),以及 Redis 对 Lua 的扩展,最后结合 Lua 与 Redis 实现了一个支持过期时间的分布式锁。希望读者可以在读完后, 体会到 Lua 这门语言不一样的设计哲学,可以更加得心应手地使用/扩展 Redis。</p>    <h2><strong>引</strong></h2>    <p>案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次.</p>    <ul>     <li>非脚本实现</li>    </ul>    <pre>  <code class="language-lua">private boolean accessLimit(String ip, int limit, int time, Jedisjedis) {      boolean result = true;         String key = "rate.limit:" + ip;      if (jedis.exists(key)) {          long afterValue = jedis.incr(key);          if (afterValue > limit) {              result = false;          }      } else {          Transactiontransaction = jedis.multi();          transaction.incr(key);          transaction.expire(key, time);          transaction.exec();      }      return result;  }  </code></pre>    <ul>     <li> <p>以上代码有两点缺陷</p>      <ol>       <li> <p>可能会出现竞态条件: 解决方法是用 WATCH 监控 rate.limit:$IP 的变动, 但较为麻烦;</p> </li>       <li> <p>以上代码在不使用 pipeline 的情况下最多需要向Redis请求5条指令, 传输过多.</p> </li>      </ol> </li>    </ul>    <ul>     <li> <p>Lua脚本实现</p> <p>Redis 允许将 Lua 脚本传到 Redis 服务器中执行, 脚本内可以调用大部分 Redis 命令, 且 Redis 保证脚本的 <strong>原子性</strong>:</p>      <ul>       <li> <p>首先需要准备Lua代码: script.lua</p> </li>      </ul> </li>    </ul>    <pre>  <code class="language-lua">--  -- CreatedbyIntelliJIDEA.  -- User: jifang  -- Date: 16/8/24  -- Time: 下午6:11  --     localkey = "rate.limit:" .. KEYS[1]  locallimit = tonumber(ARGV[1])  localexpire_time = ARGV[2]     localis_exists = redis.call("EXISTS", key)  if is_exists == 1 then      if redis.call("INCR", key) > limitthen          return 0      else          return 1      end  else      redis.call("SET", key, 1)      redis.call("EXPIRE", key, expire_time)      return 1  end  </code></pre>    <ul>     <li>Java</li>    </ul>    <pre>  <code class="language-lua">private boolean accessLimit(String ip, int limit, int timeout, Jedisconnection) 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 {      Readerreader = new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName));      return CharStreams.toString(reader);  }  </code></pre>    <ul>     <li>Lua 嵌入 Redis 优势:      <ol>       <li>减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;</li>       <li>原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;</li>       <li>复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.</li>      </ol> </li>    </ul>    <h2><strong>Lua语言模型</strong></h2>    <p>Lua是一种 <strong> <em>便于嵌入应用程序</em> </strong> 的脚本语言, 具备了作为通用脚本语言的所有功能. 其高速虚拟机实现非常有名(Lua的垃圾回收很有讲究- <em>增量垃圾回收</em> ), 在很多虚拟机系性能评分中都取得了优异的成绩. <strong>Home</strong> <a href="/misc/goto?guid=4959719685126884164" rel="nofollow,noindex">lua.org</a> .</p>    <p>以 <strong>嵌入式</strong> 为方针设计的Lua, 在默认状态下简洁得吓人. 除了基本的数据类型外, 其他一概没有. 标注库也就 <em>Co</em>routine 、 String 、 Table 、 Math 、 I/O 、 OS , 再加上 Modules包加载 而已. </p>    <p>注: 本文仅介绍 Lua 与众不同的 <strong>设计模型</strong> (对比 <strong>Java/C/C++</strong> 、 <strong>JavaScript</strong> 、 <strong>Python</strong> 与 <strong>Go</strong> ), 语言细节可参考文内和附录推荐的文章以及Lua之父 <strong>Roberto Ierusalimschy</strong> 的《Programming in Lua》(中文版: LUA程序设计(第2版)>)</p>    <h3><strong>基础</strong></h3>    <p><strong>1. 数据类型</strong></p>    <ul>     <li> <p>作为通用脚本语言, Lua的数据类型如下:</p>      <ul>       <li> <p>数值型:<br> 全部为浮点数型, 没有整型;<br> 只有 nil 和 false 作为布尔值的 false , 数字 0 和空串( ‘’ / ‘’ )都是 true ;</p> </li>       <li> <p>字符串</p> </li>       <li> <p>用户自定义类型</p> </li>       <li> <p>函数(function)</p> </li>       <li> <p>表(table)</p> </li>      </ul> </li>    </ul>    <p>变量如果没有特殊说明为全局变量(那怕是语句块 or 函数内), 局部变量前需加 local 关键字.</p>    <p><strong>2. 关键字</strong></p>    <p><img src="https://simg.open-open.com/show/add41afbe6543a7f963df2794af1003a.png"></p>    <p><strong>3. 操作符</strong></p>    <p><img src="https://simg.open-open.com/show/0d5093e8687500c4ed6e86f0e0c5f981.png"></p>    <ul>     <li> <p>Tips:</p>      <ul>       <li> <p>数学操作符的操作数如果是字符串会自动转换成数字;</p> </li>       <li> <p>连接 .. 自动将数值转换成字符串;</p> </li>       <li> <p>比较操作符的结果一定是布尔类型, 且会严格判断数据类型( '1' != 1 );</p> </li>      </ul> </li>    </ul>    <h3><strong>函数(function)</strong></h3>    <p>在 Lua 中, 函数是和字符串、数值和表并列的基本数据结构, 属于 <strong>第一类对象</strong> ( <em>first-class-object</em> /一等公民), 可以和数值等其他类型一样 <strong> 赋给变量 </strong> 、 <strong> 作为参数传递 </strong> , 以及作为 <strong> 返回值接收(闭包) </strong> :</p>    <ul>     <li> <p>使用方式类似JavaScript:</p> </li>    </ul>    <pre>  <code class="language-lua">-- 全局函数: 求阶乘  function fact(n)      if n == 1 then          return 1      else          return n * fact(n - 1)      end  end     -- 1. 赋给变量  localfunc = fact  print("func type: " .. type(func), "fact type: " .. type(fact), "result: " .. func(4))     -- 2. 闭包  localfunction new_counter()      localvalue = 0;      return function()          value = value + 1          return value      end  end     localcounter = new_counter()  print(counter(), counter(), counter())     -- 3. 返回值类似Go/Python  localrandom_func = function(param)      return 9, 'a', true, "ƒ∂π", param  end     localvar1, var2, var3, var4, var5 = random_func("no param is nil")  print(var1, var2, var3, var4, var5)     -- 4. 变数形参  localfunction square(...)      localargv = { ... }      for i = 1, #argv do          argv[i] = argv[i] * argv[i]      end      return table.unpack(argv)  end     print(square(1, 2, 3))  </code></pre>    <h3><strong>表(table)</strong></h3>    <p>Lua最具特色的数据类型就是 <strong>表(Table)</strong> , 可以实现 <strong>数组</strong> 、 Hash 、 <strong>对象</strong> 所有功能的万能数据类型:</p>    <pre>  <code class="language-lua">-- array  localarray = { 1, 2, 3 }  print(array[1], #array)     -- hash  localhash = { x = 1, y = 2, z = 3 }  print(hash.x, hash['y'], hash["z"], #hash)     -- array & hash  array['x'] = 8  print(array.x, #array)  </code></pre>    <ul>     <li> <p>Tips:</p>      <ul>       <li> <p>数组索引从 1 开始;</p> </li>       <li> <p>获取数组长度操作符 # 其’长度’只包括以 <strong>(正)整数</strong> 为索引的数组元素.</p> </li>       <li> <p>Lua用 <strong>表管理全局变量</strong> , 将其放入一个叫 _G 的table内:</p> </li>      </ul> </li>    </ul>    <pre>  <code class="language-lua">-- pairs会遍历所有值不为nil的索引, 与此类似的ipairs只会从索引1开始递遍历到最后一个值不为nil的整数索引.  for k, v in pairs(_G) do      print(k, " -> ", v, " type: " .. type(v))  end  </code></pre>    <p>用 Hash 实现对象的还有 <strong>JavaScript</strong> , 将数组和 Hash 合二为一的还有 <strong>PHP</strong> .</p>    <p>元表</p>    <p>Every value in Lua can have a <strong>metatable/元表</strong> . 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 <em>addition</em> , Lua checks for a function in the field <em>“__add”</em> of the value’s metatable. If it finds one, Lua calls this function to perform the addition.</p>    <p>The key for each event in a metatable is a string with the event name prefixed by two underscores __ ; the corresponding values are called metamethods. In the previous example, the key is “__add” and the metamethod is the function that performs the addition.</p>    <p>metatable中的键名称为 <strong>事件/event</strong> , 值称为 <strong>元方法/metamethod</strong> , 我们可通过 getmetatable() 来获取任一值的 <strong>metatable</strong> , 也可通过 setmetatable() 来替换 <strong>table</strong> 的 <strong>metatable</strong> . Lua 事件一览表:</p>    <p><img src="https://simg.open-open.com/show/6a540702f17875a9aba0139503e3e26b.png"></p>    <p>对于这些操作, Lua 都将其关联到 metatable 的事件Key, 当 Lua 需要对一个值发起这些操作时, 首先会去检查其 <strong>metatable</strong> 中是否有对应的事件Key, 如果有则调用之以 <strong>控制Lua解释器作出响应</strong> .</p>    <p><strong>MetaMethods</strong></p>    <p>MetaMethods主要用作一些类似C++中的 <strong>运算符重载</strong> 操作, 如重载 + 运算符:</p>    <pre>  <code class="language-lua">localfrac_a = { numerator = 2, denominator = 3 }  localfrac_b = { numerator = 4, denominator = 8 }     localoperator = {      __add = function(f1, f2)          localret = {}          ret.numerator = f1.numerator * f2.denominator + f1.denominator * f2.numerator          ret.denominator = f1.denominator * f2.denominator          return ret      end,         __tostring = function(self)          return "{ " .. self.numerator .. " ," .. self.denominator .. " }"      end  }     setmetatable(frac_a, operator)  setmetatable(frac_b, operator)     localfrac_res = frac_a + frac_b  setmetatable(frac_res, operator) -- 使tostring()方法生效  print(tostring(frac_res))  </code></pre>    <p>关于更多Lua事件处理可参考文档: <a href="/misc/goto?guid=4959719685210771626" rel="nofollow,noindex">Metamethods</a> .</p>    <p><strong>MetaTables 与 面向对象</strong></p>    <p>Lua本来就不是设计为一种 <strong>面向对象</strong> 语言, 因此其面向对象功能需要通过 <strong>元表(metatable)</strong> 这种非常怪异的方式实现, Lua并不直接支持面向对象语言中常见的类、对象和方法: 其 对象 和 类 通过 表 实现, 而 方法 是通过 函数 来实现.</p>    <p>上面的 <strong>Event一览表</strong> 内我们看到有 __index 这个事件重载,这个东西主要是重载了 find key 操作, 该操作可以让Lua变得有点面向对象的感觉(类似JavaScript中的 <em>prototype</em> ). 通过Lua代码模拟:</p>    <pre>  <code class="language-lua">localfunction gettable_event(t, key)      local h      if type(t) == "table" then          localvalue = rawget(t, key)          if value ~= nilthen              return value          end             h = getmetatable(t).__index          if h == nilthen              return nil          end      else          h = getmetatable(t).__index          if h == nilthen              error("error")          end      end         if type(h) == "function" then          -- callthehandler          return (h(t, key))      else          -- or repeatoprationonit          return h[key]      end  end     -- 测试  obj = { 1, 2, 3 }  op = {      x = function()          return "xx"      end  }     setmetatable(obj, { __index = op['x'] })  print(gettable_event(obj, x))  </code></pre>    <ul>     <li>对于任何事件, Lua的处理都可以归结为以下逻辑:      <ol>       <li>如果存在规定的操作则执行它;</li>       <li>否则从元表中取出各事件对应的 __ 开头的元素, 如果该元素为函数, 则调用;</li>       <li>如果该元素不为函数, 则用该元素代替 table 来执行事件所对应的处理逻辑.</li>      </ol> </li>    </ul>    <p>这里的代码仅作模拟, 实际的行为已经嵌入Lua解释器, 执行效率要远高于这些模拟代码.</p>    <p><strong>方法调用的实现</strong></p>    <p>面向对象的基础是创建对象和调用方法. Lua中, 表作为对象使用, 因此创建对象没有问题, 关于调用方法, 如果表元素为函数的话, 则可直接调用:</p>    <pre>  <code class="language-lua">-- 从obj取键为x的值, 将之视为function进行调用  obj.x(foo)  </code></pre>    <p>不过这种实现方法调用的方式, 从面向对象角度来说还有2个问题:</p>    <ul>     <li> <p>首先: obj.x 这种调用方式, 只是将表 obj 的属性 x 这个 <strong>函数对象</strong> 取出而已, 而在大多数面向对象语言中, <strong>方法的实体位于类中, 而非单独的对象中</strong> . 在JavaScript等 <strong>基于原型</strong> 的语言中, 是 <strong>以原型对象来代替类进行方法的搜索</strong> , 因此 <strong>每个单独的对象也并不拥有方法实体</strong> . 在Lua中, 为了实现基于原型的方法搜索, 需要使用元表的 __index 事件:</p> <p>如果我们有两个对象 a 和 b ,想让 b 作为 a 的 <strong>prototype</strong> 需要 setmetatable(a, {__index = b}) , 如下例: 为 obj 设置 __index 加上 proto 模板来创建另一个实例:</p> </li>    </ul>    <pre>  <code class="language-lua">proto = {      x = function()          print("x")      end  }     localobj = {}  setmetatable(obj, { __index = proto })  obj.x()  </code></pre>    <p>proto 变成了原型对象, 当 obj 中不存在的属性被引用时, 就会去搜索 proto .</p>    <ul>     <li> <p>其次: 通过方法搜索得到的函数对象只是单纯的函数, 而无法获得最初调用方法的表( <strong>接收器</strong> )相关信息. 于是, <strong>过程和数据就发生了分离</strong> .JavaScript中, 关于接收器的信息可由关键字 this 获得, 而在Python中通过方法调用形式获得的 <strong>并非单纯的函数对象</strong> , 而是一个 <strong>“方法对象”</strong> –其接收器会在内部 <strong>作为第一参数附在函数的调用过程中</strong> .</p> <p>而Lua准备了支持方法调用的 <strong>语法糖</strong> : obj:x() . 表示 obj.x(obj) , 也就是: 通过冒号记法调用的函数, 其接收器会被作为第一参数添加进来 ( obj 的求值只会进行一次, 即使有副作用也只生效一次).</p> </li>    </ul>    <pre>  <code class="language-lua">-- 这个语法糖对定义也有效  function proto:y(param)      print(self, param)  end     - Tips: 用冒号记法定义的方法, 调用时最好也用冒号记法, 避免参数错乱  obj:y("parameter")  </code></pre>    <p>更多MetaTable介绍可参考文档 <a href="/misc/goto?guid=4959719685312386651" rel="nofollow,noindex">Metatable</a> 与博客 <a href="/misc/goto?guid=4959719685393280375" rel="nofollow,noindex">metatable和metamethod</a> .</p>    <p><strong>基于原型的编程</strong></p>    <p>Lua虽然能够进行面向对象编程, 但用元表来实现, 仿佛把对象剖开看到五脏六腑一样.</p>    <p>《代码的未来》中松本行弘老师向我们展示了一个基于原型编程的Lua库, 通过该库, 即使没有深入解Lua原始机制, 也可以实现面向对象:</p>    <pre>  <code class="language-lua">--  -- Author: Matz  -- Date: 16/9/24  -- Time: 下午5:13  --     -- Object为所有对象的上级  Object = {}     -- 创建现有对象副本  function Object:clone()      localobject = {}         -- 复制表元素      for k, v in pairs(self) do          object[k] = v      end         -- 设定元表: 指定向自身`转发`      setmetatable(object, { __index = self })         return object  end     -- 基于类的编程  function Object:new(...)      localobject = {}         -- 设定元表: 指定向自身`转发`      setmetatable(object, { __index = self })         -- 初始化      object:init(...)         return object  end     -- 初始化实例  function Object:init(...)      -- 默认不进行任何操作  end     Class = Object:new()  </code></pre>    <p>另存为 <strong>prototype.lua</strong> , 使用时只需 require() 引入即可:</p>    <pre>  <code class="language-lua">require("prototype")     -- Point类定义  Point = Class:new()  function Point:init(x, y)      self.x = x      self.y = y  end     function Point:magnitude()      return math.sqrt(self.x ^ 2 + self.y ^ 2)  end     -- 对象定义  point = Point:new(3, 4)  print(point:magnitude())     -- 继承: Point3D定义  Point3D = Point:clone()  function Point3D:init(x, y, z)      self.x = x      self.y = y      self.z = z  end     function Point3D:magnitude()      return math.sqrt(self.x ^ 2 + self.y ^ 2 + self.z ^ 2)  end     p3 = Point3D:new(1, 2, 3)  print(p3:magnitude())     -- 创建p3副本  ap3 = p3:clone()  print(ap3.x, ap3.y, ap3.z)  </code></pre>    <h2><strong>Redis – Lua</strong></h2>    <p>在传入到Redis的Lua脚本中可使用 redis.call() / redis.pcall() 函数调用Reids命令:</p>    <pre>  <code class="language-lua">redis.call("set", "foo", "bar")  localvalue = redis.call("get", "foo")  </code></pre>    <p>redis.call() 返回值就是Reids命令的执行结果, Redis回复与Lua数据类型的对应关系如下:</p>    <table>     <thead>      <tr>       <th>Reids返回值类型</th>       <th>Lua数据类型</th>      </tr>     </thead>     <tbody>      <tr>       <td>整数</td>       <td>数值</td>      </tr>      <tr>       <td>字符串</td>       <td>字符串</td>      </tr>      <tr>       <td>多行字符串</td>       <td>表(数组)</td>      </tr>      <tr>       <td>状态回复</td>       <td>表(只有一个 ok 字段存储状态信息)</td>      </tr>      <tr>       <td>错误回复</td>       <td>表(只有一个 err 字段存储错误信息)</td>      </tr>     </tbody>    </table>    <p>注: Lua 的 false 会转化为空结果.</p>    <p>redis-cli提供了 EVAL 与 EVALSHA 命令执行Lua脚本:</p>    <ul>     <li>EVAL<br> EVAL script numkeys key [key ...] arg [arg ...]<br> <em>key</em> 和 <em>arg</em> 两类参数用于向脚本传递数据, 他们的值可在脚本中使用 KEYS 和 ARGV 两个table访问: KEYS 表示要操作的键名, ARGV 表示非键名参数(并非强制).</li>     <li>EVALSHA<br> EVALSHA 命令允许通过脚本的 <strong>SHA1</strong> 来执行(节省带宽), Redis在执行 EVAL / SCRIPT LOAD 后会计算脚本 <strong>SHA1</strong> 缓存, EVALSHA 根据 <strong>SHA1</strong> 取出缓存脚本执行.</li>    </ul>    <h3><strong>创建Lua环境</strong></h3>    <p>为了在 Redis 服务器中执行 Lua 脚本, Redis 内嵌了一个 Lua 环境, 并对该环境进行了一系列修改, 从而确保满足 Redis 的需要. 其创建步骤如下:</p>    <ul>     <li>创建基础 Lua 环境, 之后所有的修改都基于该环境进行;</li>     <li>载入函数库到 Lua 环境, 使 Lua 脚本可以使用这些函数库进行数据操作: 如基础库(删除了 loadfile() 函数)、Table、String、Math、Debug等标准库, 以及CJSON、 Struct(用于Lua值与C结构体转换)、 cmsgpack等扩展库(Redis 禁用Lua标准库中与文件或系统调用相关函数, 只允许对 Redis 数据处理).</li>     <li>创建全局表 redis , 其包含了对 Redis 操作的函数, 如 redis.call() 、 redis.pcall() 等;</li>     <li>替换随机函数: 为了确保相同脚本可在不同机器上产生相同结果, Redis 要求所有传入服务器的 Lua 脚本, 以及 Lua 环境中的所有函数, 都必须是无副作用的 <strong>纯函数</strong> , 因此Redis使用自制函数替换了 Math 库中原有的 math.random() 和 math.randomseed() .</li>     <li>创建辅助排序函数: 对于 Lua 脚本来说, 另一个可能产生数据不一致的地方是那些 <strong>带有不确定性质的命令</strong> (如: 由于 set 集合无序, 因此即使两个集合内元素相同, 其输出结果也并不一样), 这类命令包括 <strong> <em>SINTER</em> </strong> 、 <strong> <em>SUNION</em> </strong> 、 <strong> <em>SDIFF</em> </strong> 、 <strong> <em>SMEMBERS</em> </strong> 、 <strong> <em>HKEYS</em> </strong> 、 <strong> <em>HVALS</em> </strong> 、 <strong> <em>KEYS</em> </strong> 等.<br> Redis 会创建一个辅助排序函数 __redis__compare_helper , 当执行完以上命令后, Redis会调用 table.sort() 以 __redis__compare_helper 作为辅助函数对命令返回值排序.</li>     <li>创建错误处理函数: Redis创建一个 __redis__err__handler 错误处理函数, 当调用 redis.pcall() 执行 Redis 命令出错时, 该函数将打印异常详细信息.</li>     <li>Lua全局环境保护: 确保传入脚本内不会将额外的全局变量导入到 Lua 环境内. <p>小心: Redis 并未禁止用户修改已存在的全局变量.</p> </li>     <li>完成Redis的 lua 属性与Lua环境的关联:</li>    </ul>    <p style="text-align:center"><br> <img src="https://simg.open-open.com/show/a8eb9eb0b846254bd9c8c638a1190a60.png"></p>    <p>整个 Redis 服务器只需创建一个 Lua 环境.</p>    <h3><strong>Lua环境协作组件</strong></h3>    <ul>     <li>Redis创建两个用于与Lua环境协作的组件: <strong>伪客户端</strong> – 负责执行 Lua 脚本中的 Redis 命令, lua_scripts <strong>字典</strong> – 保存 Lua 脚本:      <ul>       <li>伪客户端<br> 执行Reids命令必须有对应的客户端状态, 因此执行 <strong>Lua 脚本内的 Redis 命令</strong> 必须为 Lua 环境专门创建一个伪客户端, 由该客户端处理 Lua 内所有命令: redis.call() / redis.pcall() 执行一个Redis命令步骤如下:<br> <img src="https://simg.open-open.com/show/d00994a737286b408f7cdf094b4ac6df.png"></li>       <li>lua_scripts 字典<br> 字典key为脚本 <strong>SHA1</strong> 校验和, value为 <strong>SHA1</strong> 对应脚本内容, 所有被 EVAL 和 SCRIPT LOAD 载入过的脚本都被记录到 lua_scripts 中, 便于实现 SCRIPT EXISTS 命令和脚本复制功能.</li>      </ul> </li>    </ul>    <h3><strong>EVAL命令原理</strong></h3>    <p>EVAL 命令执行分为以下三个步骤:</p>    <ol>     <li>定义Lua函数:<br> 在 Lua 环境内定义 <strong>Lua函数</strong> : 名为 f_ 前缀+脚本 <strong>SHA1</strong> 校验和, 体为 <strong>脚本内容本身</strong> . 优势:      <ul>       <li>执行脚本步骤简单, 调用函数即可;</li>       <li>函数的局部性可保持 Lua 环境清洁, 减少垃圾回收工作量, 且避免使用全局变量;</li>       <li>只要记住 <strong>SHA1</strong> 校验和, 即可在不知脚本内容的情况下, 直接调用 Lua 函数执行脚本( EVALSHA 命令实现).</li>      </ul> </li>     <li>将脚本保存到 lua_scripts 字典;</li>     <li>执行脚本函数:<br> 执行刚刚在定义的函数, 间接执行 Lua 脚本, 其准备和执行过程如下:<br> 1). 将 EVAL 传入的键名和参数分别保存到 KEYS 和 ARGV , 然后将这两个数组作为全局变量传入到Lua环境;<br> 2). 为Lua环境装载超时处理 hook ( handler ), 可在脚本出现运行超时时让通过 SCRIPT KILL 停止脚本, 或 SHUTDOWN 关闭Redis;<br> 3). 执行脚本函数;<br> 4). 移除超时 hook ;<br> 5). 将执行结果保存到客户端输出缓冲区, 等待将结果返回客户端;<br> 6). 对Lua环境执行垃圾回收.</li>    </ol>    <p>对于 <strong>会产生随机结果但无法排序的命令</strong> (如只产生一个元素, 如 <strong> <em>SPOP</em> </strong> 、 <strong> <em>SRANDMEMBER</em> </strong> 、 <strong> <em>RANDOMKEY</em> </strong> 、 <strong> <em>TIME</em> </strong> ), Redis在这类命令执行后将脚本状态置为 lua_random_dirty , 此后只允许脚本调用只读命令, 不允许修改数据库值.</p>    <h2><strong>实践</strong></h2>    <p>使用Lua脚本重新构建带有过期时间的分布式锁.</p>    <p>案例来源: Redis实战> 第6、11章, 构建步骤:</p>    <ul>     <li>锁申请      <ul>       <li>首先尝试加锁:        <ul>         <li>成功则为锁设定过期时间; 返回;</li>         <li>失败检测锁是否添加了过期时间;</li>        </ul> </li>       <li>wait.</li>      </ul> </li>     <li>锁释放      <ul>       <li>检查当前线程是否真的持有了该锁:        <ul>         <li>持有: 则释放; 返回成功;</li>         <li>失败: 返回失败.</li>        </ul> </li>      </ul> </li>    </ul>    <h3><strong>非Lua实现</strong></h3>    <pre>  <code class="language-lua">String acquireLockWithTimeOut(Jedisconnection, String lockName, long acquireTimeOut, int lockTimeOut) {      String identifier = UUID.randomUUID().toString();      String key = "lock:" + lockName;         long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;      while (System.currentTimeMillis() < acquireTimeEnd) {          // 获取锁并设置过期时间          if (connection.setnx(key, identifier) != 0) {              connection.expire(key, lockTimeOut);              return identifier;          }          // 检查过期时间, 并在必要时对其更新          else if (connection.ttl(key) == -1) {              connection.expire(key, lockTimeOut);          }             try {              Thread.sleep(10);          } catch (InterruptedExceptionignored) {          }      }      return null;  }     boolean releaseLock(Jedisconnection, String lockName, String identifier) {      String key = "lock:" + lockName;         connection.watch(key);      // 确保当前线程还持有锁      if (identifier.equals(connection.get(key))) {          Transactiontransaction = connection.multi();          transaction.del(key);          return transaction.exec().isEmpty();      }      connection.unwatch();         return false;  }  </code></pre>    <h3><strong>Lua脚本实现</strong></h3>    <ul>     <li> <p>Lua脚本: acquire</p> </li>    </ul>    <pre>  <code class="language-lua">localkey = KEYS[1]  localidentifier = ARGV[1]  locallockTimeOut = ARGV[2]     -- 锁定成功  if redis.call("SETNX", key, identifier) == 1 then      redis.call("EXPIRE", key, lockTimeOut)      return 1  elseif redis.call("TTL", key) == -1 then      redis.call("EXPIRE", key, lockTimeOut)  end  return 0  </code></pre>    <ul>     <li>Lua脚本: release</li>    </ul>    <pre>  <code class="language-lua">localkey = KEYS[1]  localidentifier = ARGV[1]     if redis.call("GET", key) == identifierthen      redis.call("DEL", key)      return 1  end  return 0  </code></pre>    <ul>     <li> <p>Pre工具: 脚本执行器</p> </li>    </ul>    <pre>  <code class="language-lua">/**  * @author jifang  * <a href='http://www.jobbole.com/members/chchxinxinjun'>@since</a> 16/8/25 下午3:35.  */  public class ScriptCaller {         private static final ConcurrentMap<String, String> SHA_CACHE = new ConcurrentHashMap<>();         private String script;         private ScriptCaller(String script) {          this.script = script;      }         public static ScriptCallergetInstance(String script) {          return new ScriptCaller(script);      }         public Object call(Jedisconnection, List<String> keys, List<String> argv, boolean forceEval) {          if (!forceEval) {              String sha = SHA_CACHE.get(this.script);              if (Strings.isNullOrEmpty(sha)) {                  // load 脚本得到 sha1 缓存                  sha = connection.scriptLoad(this.script);                  SHA_CACHE.put(this.script, sha);              }                 return connection.evalsha(sha, keys, argv);          }             return connection.eval(script, keys, argv);      }  }  </code></pre>    <ul>     <li> <p>Client</p> </li>    </ul>    <pre>  <code class="language-lua">public class Client {         private ScriptCalleracquireCaller = ScriptCaller.getInstance(              "local key = KEYS[1]\n" +              "local identifier = ARGV[1]\n" +              "local lockTimeOut = ARGV[2]\n" +              "\n" +              "if redis.call(\"SETNX\", key, identifier) == 1 then\n" +              "    redis.call(\"EXPIRE\", key, lockTimeOut)\n" +              "    return 1\n" +              "elseif redis.call(\"TTL\", key) == -1 then\n" +              "    redis.call(\"EXPIRE\", key, lockTimeOut)\n" +              "end\n" +              "return 0"      );         private ScriptCallerreleaseCaller = ScriptCaller.getInstance(              "local key = KEYS[1]\n" +              "local identifier = ARGV[1]\n" +              "\n" +              "if redis.call(\"GET\", key) == identifier then\n" +              "    redis.call(\"DEL\", key)\n" +              "    return 1\n" +              "end\n" +              "return 0"      );         @Test      public void client() {          Jedisjedis = new Jedis("127.0.0.1", 9736);          String identifier = acquireLockWithTimeOut(jedis, "ret1", 200 * 1000, 300);          System.out.println(releaseLock(jedis, "ret1", identifier));      }         String acquireLockWithTimeOut(Jedisconnection, String lockName, long acquireTimeOut, int lockTimeOut) {          String identifier = UUID.randomUUID().toString();             List<String> keys = Collections.singletonList("lock:" + lockName);          List<String> argv = Arrays.asList(identifier,                  String.valueOf(lockTimeOut));             long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;          boolean acquired = false;          while (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) {              if (1 == (long) acquireCaller.call(connection, keys, argv, false)) {                  acquired = true;              } else {                  try {                      Thread.sleep(10);                  } catch (InterruptedExceptionignored) {                  }              }          }             return acquired ? identifier : null;      }         boolean releaseLock(Jedisconnection, String lockName, String identifier) {          List<String> keys = Collections.singletonList("lock:" + lockName);          List<String> argv = Collections.singletonList(identifier);          return 1 == (long) releaseCaller.call(connection, keys, argv, true);      }  }  </code></pre>    <p> </p>    <p> </p>    <p>来自:http://blog.jobbole.com/106456/</p>    <p> </p>