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

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

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

    • 首先需要准备Lua代码: script.lua
  1. --
  2. -- Created by IntelliJ IDEA.
  3. -- User: jifang
  4. -- Date: 16/8/24
  5. -- Time: 下午6:11
  6. --
  7. local key = "rate.limit:" .. KEYS[1]
  8. local limit = tonumber(ARGV[1])
  9. local expire_time = ARGV[2]
  10. local is_exists = redis.call("EXISTS", key)
  11. if is_exists == 1 then
  12. if redis.call("INCR", key) > limit then
  13. return 0
  14. else
  15. return 1
  16. end
  17. else
  18. redis.call("SET", key, 1)
  19. redis.call("EXPIRE", key, expire_time)
  20. return 1
  21. end
  • Java
  1. private boolean accessLimit(String ip, int limit, int timeout, Jedis connection) throws IOException {
  2. List<String> keys = Collections.singletonList(ip);
  3. List<String> argv = Arrays.asList(String.valueOf(limit), String.valueOf(timeout));
  4. return 1 == (long) connection.eval(loadScriptString("script.lua"), keys, argv);
  5. }
  6. // 加载Lua代码
  7. private String loadScriptString(String fileName) throws IOException {
  8. Reader reader = new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName));
  9. return CharStreams.toString(reader);
  10. }

  • Lua 嵌入 Redis 优势: 
    1. 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
    2. 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
    3. 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.

Lua模型

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

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

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


Base

1. 数据类型

  • 作为通用脚本语言, Lua的数据类型如下:

    • 数值型: 
      全部为浮点数型, 没有整型; 
      只有 nil 和 false 作为布尔值的 false , 数字 0 和空串(‘’/‘\0’)都是 true;
    • 字符串
    • 用户自定义类型
    • 函数(function)
    • 表(table)

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


2. 关键字


3. 操作符

  • Tips:

    • 数学操作符的操作数如果是字符串会自动转换成数字;
    • 连接 .. 自动将数值转换成字符串;
    • 比较操作符的结果一定是布尔类型, 且会严格判断数据类型('1' != 1);

函数(function)

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

  • 使用方式类似JavaScript:
  1. -- 全局函数: 求阶乘
  2. function fact(n)
  3. if n == 1 then
  4. return 1
  5. else
  6. return n * fact(n - 1)
  7. end
  8. end
  9. -- 1. 赋给变量
  10. local func = fact
  11. print("func type: " .. type(func), "fact type: " .. type(fact), "result: " .. func(4))
  12. -- 2. 闭包
  13. local function new_counter()
  14. local value = 0;
  15. return function()
  16. value = value + 1
  17. return value
  18. end
  19. end
  20. local counter = new_counter()
  21. print(counter(), counter(), counter())
  22. -- 3. 返回值类似Go/Python
  23. local random_func = function(param)
  24. return 9, 'a', true, "ƒ∂π", param
  25. end
  26. local var1, var2, var3, var4, var5 = random_func("no param is nil")
  27. print(var1, var2, var3, var4, var5)
  28. -- 4. 变数形参
  29. local function square(...)
  30. local argv = { ... }
  31. for i = 1, #argv do
  32. argv[i] = argv[i] * argv[i]
  33. end
  34. return table.unpack(argv)
  35. end
  36. print(square(1, 2, 3))

表(table)

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

  1. -- array
  2. local array = { 1, 2, 3 }
  3. print(array[1], #array)
  4. -- hash
  5. local hash = { x = 1, y = 2, z = 3 }
  6. print(hash.x, hash['y'], hash["z"], #hash)
  7. -- array & hash
  8. array['x'] = 8
  9. print(array.x, #array)
  • Tips:

    • 数组索引从1开始;
    • 获取数组长度操作符#其’长度’只包括以(正)整数为索引的数组元素.
    • Lua用表管理全局变量, 将其放入一个叫_G的table内:
  1. -- pairs会遍历所有值不为nil的索引, 与此类似的ipairs只会从索引1开始递遍历到最后一个值不为nil的整数索引.
  2. for k, v in pairs(_G) do
  3. print(k, " -> ", v, " type: " .. type(v))
  4. end

Hash实现对象的还有JavaScript, 将数组和Hash合二为一的还有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 addition, Lua checks for a function in the field “__add” 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 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.

metatable中的键名称为事件/event, 值称为元方法/metamethod, 我们可通过getmetatable()来获取任一值的metatable, 也可通过setmetatable()来替换tablemetatable. Lua 事件一览表: 
 
对于这些操作, Lua 都将其关联到 metatable 的事件Key, 当 Lua 需要对一个值发起这些操作时, 首先会去检查其metatable中是否有对应的事件Key, 如果有则调用之以控制Lua解释器作出响应.


MetaMethods

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

  1. local frac_a = { numerator = 2, denominator = 3 }
  2. local frac_b = { numerator = 4, denominator = 8 }
  3. local operator = {
  4. __add = function(f1, f2)
  5. local ret = {}
  6. ret.numerator = f1.numerator * f2.denominator + f1.denominator * f2.numerator
  7. ret.denominator = f1.denominator * f2.denominator
  8. return ret
  9. end,
  10. __tostring = function(self)
  11. return "{ " .. self.numerator .. " ," .. self.denominator .. " }"
  12. end
  13. }
  14. setmetatable(frac_a, operator)
  15. setmetatable(frac_b, operator)
  16. local frac_res = frac_a + frac_b
  17. setmetatable(frac_res, operator) -- 使tostring()方法生效
  18. print(tostring(frac_res))

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


MetaTables 与 面向对象

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

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

  1. local function gettable_event(t, key)
  2. local h
  3. if type(t) == "table" then
  4. local value = rawget(t, key)
  5. if value ~= nil then
  6. return value
  7. end
  8. h = getmetatable(t).__index
  9. if h == nil then
  10. return nil
  11. end
  12. else
  13. h = getmetatable(t).__index
  14. if h == nil then
  15. error("error")
  16. end
  17. end
  18. if type(h) == "function" then
  19. -- call the handler
  20. return (h(t, key))
  21. else
  22. -- or repeat opration on it
  23. return h[key]
  24. end
  25. end
  26. -- 测试
  27. obj = { 1, 2, 3 }
  28. op = {
  29. x = function()
  30. return "xx"
  31. end
  32. }
  33. setmetatable(obj, { __index = op['x'] })
  34. print(gettable_event(obj, x))
  • 对于任何事件, Lua的处理都可以归结为以下逻辑: 
    1. 如果存在规定的操作则执行它;
    2. 否则从元表中取出各事件对应的__开头的元素, 如果该元素为函数, 则调用;
    3. 如果该元素不为函数, 则用该元素代替table来执行事件所对应的处理逻辑.

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


方法调用的实现

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

  1. -- obj取键为x的值, 将之视为function进行调用
  2. obj.x(foo)

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

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

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

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

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


基于原型的编程

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

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

  1. --
  2. -- Author: Matz
  3. -- Date: 16/9/24
  4. -- Time: 下午5:13
  5. --
  6. -- Object为所有对象的上级
  7. Object = {}
  8. -- 创建现有对象副本
  9. function Object:clone()
  10. local object = {}
  11. -- 复制表元素
  12. for k, v in pairs(self) do
  13. object[k] = v
  14. end
  15. -- 设定元表: 指定向自身`转发`
  16. setmetatable(object, { __index = self })
  17. return object
  18. end
  19. -- 基于类的编程
  20. function Object:new(...)
  21. local object = {}
  22. -- 设定元表: 指定向自身`转发`
  23. setmetatable(object, { __index = self })
  24. -- 初始化
  25. object:init(...)
  26. return object
  27. end
  28. -- 初始化实例
  29. function Object:init(...)
  30. -- 默认不进行任何操作
  31. end
  32. Class = Object:new()

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

  1. require("prototype")
  2. -- Point类定义
  3. Point = Class:new()
  4. function Point:init(x, y)
  5. self.x = x
  6. self.y = y
  7. end
  8. function Point:magnitude()
  9. return math.sqrt(self.x ^ 2 + self.y ^ 2)
  10. end
  11. -- 对象定义
  12. point = Point:new(3, 4)
  13. print(point:magnitude())
  14. -- 继承: Point3D定义
  15. Point3D = Point:clone()
  16. function Point3D:init(x, y, z)
  17. self.x = x
  18. self.y = y
  19. self.z = z
  20. end
  21. function Point3D:magnitude()
  22. return math.sqrt(self.x ^ 2 + self.y ^ 2 + self.z ^ 2)
  23. end
  24. p3 = Point3D:new(1, 2, 3)
  25. print(p3:magnitude())
  26. -- 创建p3副本
  27. ap3 = p3:clone()
  28. print(ap3.x, ap3.y, ap3.z)

Redis - Lua

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

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

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

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

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

redis-cli提供了EVALEVALSHA命令执行Lua脚本:

  • EVAL 
    EVAL script numkeys key [key ...] arg [arg ...] 
    keyarg两类参数用于向脚本传递数据, 他们的值可在脚本中使用KEYSARGV两个table访问: KEYS表示要操作的键名, ARGV表示非键名参数(并非强制).
  • EVALSHA 
    EVALSHA命令允许通过脚本的SHA1来执行(节省带宽), Redis在执行EVAL/SCRIPT LOAD后会计算脚本SHA1缓存, EVALSHA根据SHA1取出缓存脚本执行.

创建Lua环境

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

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

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

  • 完成Redis的lua属性与Lua环境的关联: 
     
    整个 Redis 服务器只需创建一个 Lua 环境.

Lua环境协作组件

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

    • 伪客户端 
      执行Reids命令必须有对应的客户端状态, 因此执行 Lua 脚本内的 Redis 命令必须为 Lua 环境专门创建一个伪客户端, 由该客户端处理 Lua 内所有命令: redis.call()/redis.pcall()执行一个Redis命令步骤如下: 
    • lua_scripts字典 
      字典key为脚本 SHA1 校验和, value为 SHA1 对应脚本内容, 所有被EVALSCRIPT LOAD载入过的脚本都被记录到 lua_scripts 中, 便于实现 SCRIPT EXISTS 命令和脚本复制功能.

EVAL命令原理

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

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

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

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

对于会产生随机结果但无法排序的命令(如只产生一个元素, 如 SPOPSRANDMEMBERRANDOMKEYTIME), Redis在这类命令执行后将脚本状态置为lua_random_dirty, 此后只允许脚本调用只读命令, 不允许修改数据库值.


实践

  1. 使用Lua脚本重新构建带有过期时间的分布式锁.

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

  • 锁申请

    • 首先尝试加锁:

      • 成功则为锁设定过期时间; 返回;
      • 失败检测锁是否添加了过期时间;
    • wait.
  • 锁释放 
    • 检查当前线程是否真的持有了该锁:

      • 持有: 则释放; 返回成功;
      • 失败: 返回失败.

非Lua实现

  1. String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) {
  2. String identifier = UUID.randomUUID().toString();
  3. String key = "lock:" + lockName;
  4. long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
  5. while (System.currentTimeMillis() < acquireTimeEnd) {
  6. // 获取锁并设置过期时间
  7. if (connection.setnx(key, identifier) != 0) {
  8. connection.expire(key, lockTimeOut);
  9. return identifier;
  10. }
  11. // 检查过期时间, 并在必要时对其更新
  12. else if (connection.ttl(key) == -1) {
  13. connection.expire(key, lockTimeOut);
  14. }
  15. try {
  16. Thread.sleep(10);
  17. } catch (InterruptedException ignored) {
  18. }
  19. }
  20. return null;
  21. }
  22. boolean releaseLock(Jedis connection, String lockName, String identifier) {
  23. String key = "lock:" + lockName;
  24. connection.watch(key);
  25. // 确保当前线程还持有锁
  26. if (identifier.equals(connection.get(key))) {
  27. Transaction transaction = connection.multi();
  28. transaction.del(key);
  29. return transaction.exec().isEmpty();
  30. }
  31. connection.unwatch();
  32. return false;
  33. }

Lua脚本实现

  • Lua脚本: acquire
  1. local key = KEYS[1]
  2. local identifier = ARGV[1]
  3. local lockTimeOut = ARGV[2]
  4. -- 锁定成功
  5. if redis.call("SETNX", key, identifier) == 1 then
  6. redis.call("EXPIRE", key, lockTimeOut)
  7. return 1
  8. elseif redis.call("TTL", key) == -1 then
  9. redis.call("EXPIRE", key, lockTimeOut)
  10. end
  11. return 0
  • Lua脚本: release
  1. local key = KEYS[1]
  2. local identifier = ARGV[1]
  3. if redis.call("GET", key) == identifier then
  4. redis.call("DEL", key)
  5. return 1
  6. end
  7. return 0
  • Pre工具: 脚本执行器
  1. /**
  2. * @author jifang
  3. * @since 16/8/25 下午3:35.
  4. */
  5. public class ScriptCaller {
  6. private static final ConcurrentMap<String, String> SHA_CACHE = new ConcurrentHashMap<>();
  7. private String script;
  8. private ScriptCaller(String script) {
  9. this.script = script;
  10. }
  11. public static ScriptCaller getInstance(String script) {
  12. return new ScriptCaller(script);
  13. }
  14. public Object call(Jedis connection, List<String> keys, List<String> argv, boolean forceEval) {
  15. if (!forceEval) {
  16. String sha = SHA_CACHE.get(this.script);
  17. if (Strings.isNullOrEmpty(sha)) {
  18. // load 脚本得到 sha1 缓存
  19. sha = connection.scriptLoad(this.script);
  20. SHA_CACHE.put(this.script, sha);
  21. }
  22. return connection.evalsha(sha, keys, argv);
  23. }
  24. return connection.eval(script, keys, argv);
  25. }
  26. }
  • Client
  1. public class Client {
  2. private ScriptCaller acquireCaller = ScriptCaller.getInstance(
  3. "local key = KEYS[1]\n" +
  4. "local identifier = ARGV[1]\n" +
  5. "local lockTimeOut = ARGV[2]\n" +
  6. "\n" +
  7. "if redis.call(\"SETNX\", key, identifier) == 1 then\n" +
  8. " redis.call(\"EXPIRE\", key, lockTimeOut)\n" +
  9. " return 1\n" +
  10. "elseif redis.call(\"TTL\", key) == -1 then\n" +
  11. " redis.call(\"EXPIRE\", key, lockTimeOut)\n" +
  12. "end\n" +
  13. "return 0"
  14. );
  15. private ScriptCaller releaseCaller = ScriptCaller.getInstance(
  16. "local key = KEYS[1]\n" +
  17. "local identifier = ARGV[1]\n" +
  18. "\n" +
  19. "if redis.call(\"GET\", key) == identifier then\n" +
  20. " redis.call(\"DEL\", key)\n" +
  21. " return 1\n" +
  22. "end\n" +
  23. "return 0"
  24. );
  25. @Test
  26. public void client() {
  27. Jedis jedis = new Jedis("127.0.0.1", 9736);
  28. String identifier = acquireLockWithTimeOut(jedis, "ret1", 200 * 1000, 300);
  29. System.out.println(releaseLock(jedis, "ret1", identifier));
  30. }
  31. String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) {
  32. String identifier = UUID.randomUUID().toString();
  33. List<String> keys = Collections.singletonList("lock:" + lockName);
  34. List<String> argv = Arrays.asList(identifier,
  35. String.valueOf(lockTimeOut));
  36. long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
  37. boolean acquired = false;
  38. while (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) {
  39. if (1 == (long) acquireCaller.call(connection, keys, argv, false)) {
  40. acquired = true;
  41. } else {
  42. try {
  43. Thread.sleep(10);
  44. } catch (InterruptedException ignored) {
  45. }
  46. }
  47. }
  48. return acquired ? identifier : null;
  49. }
  50. boolean releaseLock(Jedis connection, String lockName, String identifier) {
  51. List<String> keys = Collections.singletonList("lock:" + lockName);
  52. List<String> argv = Collections.singletonList(identifier);
  53. return 1 == (long) releaseCaller.call(connection, keys, argv, true);
  54. }
  55. }

参考 & 推荐
代码的未来
Redis入门指南
Redis实战
Redis设计与实现
云风的Blog: Lua与虚拟机
Lua简明教程- CoolShell
Lua-newbie
Lua-Users
redis.io

Redis结合Lua脚本实现高并发原子性操作的更多相关文章

  1. 快速入门Redis调用Lua脚本及使用场景介绍

    Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储.大多数开发人员可能听说过redis可以运行 Lua 脚本,但是可能不知道redis在什么情况下需要使用到Lua脚本. 一.阅读本文 ...

  2. redis中lua脚本的简单使用

    一.背景 在使用redis的过程中,发现有些时候需要原子性去操作redis命令,而redis的lua脚本正好可以实现这一功能.比如: 扣减库存操作.限流操作等等. redis的pipelining虽然 ...

  3. 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁

    spring boot基于redis的LUA脚本 实现分布式锁[都是基于redis单点下] 一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁 1.pom.xml &l ...

  4. .Net Core使用分布式缓存Redis:Lua脚本

    一.前言 运行环境window,redis版本3.2.1.此处暂不对Lua进行详细讲解,只从Redis的方面讲解. 二.Redis的Lua脚本 在Redis的2.6版本推出了脚本功能,允许开发者使用L ...

  5. 要想用活Redis,Lua脚本是绕不过去的坎

    前言 Redis 当中提供了许多重要的高级特性,比如发布与订阅,Lua 脚本等.Redis 当中也提供了自增的原子命令,但是假如我们需要同时执行好几个命令的同时又想让这些命令保持原子性,该怎么办呢?这 ...

  6. PHP中使用redis执行lua脚本示例

    摸索了一下在PHP中如何使用redis执行lua脚本,写了一个脚本如下,供以后参考 <?php $redis = new Redis(); #实例化redis类 $redis->conne ...

  7. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  8. Java高并发--原子性可见性有序性

    Java高并发--原子性可见性有序性 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 原子性:指一个操作不可中断,一个线程一旦开始,直到执行完成都不会被其他线程干扰.换 ...

  9. 为什么 redis 单线程却能支撑高并发

    redis 和 memcached 有什么区别?redis 的线程模型是什么?为什么 redis 单线程却能支撑高并发? 这个是问 redis 的时候,最基本的问题吧,redis 最基本的一个内部原理 ...

随机推荐

  1. SSM框架中写sql在dao文件中以注解的方式

    1以注解方式 //两个参数其中一个是对象需写,对象.属性 @Update("update delivery_address set consignee = #{address.consign ...

  2. <转>Go语言TCP Socket编程

    授权转载: Tony Bai 原文连接: https://tonybai.com/2015/11/17/tcp-programming-in-golang/ Golang的主要 设计目标之一就是面向大 ...

  3. ASP.NET Core快速入门学习笔记(第3章:依赖注入)

    课程链接:http://video.jessetalk.cn/course/explore 良心课程,大家一起来学习哈! 任务16:介绍 1.依赖注入概念详解 从UML和软件建模来理解 从单元测试来理 ...

  4. outlook2013 关闭时最小化到任务栏的完美解决方法

    使用 Keep Outlook Running 加载项 文件->选项->加载项 点击最下面的“转到”按钮 *用管理员身份运行Outlook才可以将 Keep Outlook Running ...

  5. The type 'Expression<>' is defined in an assembly that is not referenced.You must add a reference to assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

    在我将一个.net framework 4.0+mvc4+ef5的项目,升级到.net framework 4.6.1+mvc5+ef6之后,解决了所有的升级带来的问题,唯独在razor的cshtml ...

  6. 【WPF开发备忘】使用MVVM模式开发中列表控件内的按钮事件无法触发解决方法

    实际使用MVVM进行WPF开发的时候,可能会用到列表控件中每行一个编辑或删除按钮,这时直接去绑定,发现无法响应: <DataGridTemplateColumn Header="操作& ...

  7. Python 官方文档解读(2):threading 模块

    使用 Python 可以编写多线程程序,注意,这并不是说程序能在多个 CPU 核上跑.如果你想这么做,可以看看关于 Python 并行计算的,比如官方 Wiki. Python 线程的主要应用场景是一 ...

  8. 2018-2019-2 20165239其米仁增《网络对抗》Exp1 PC平台逆向破解

    一.实验内容 1.掌握NOP, JNE, JE, JMP, CMP汇编指令的机器码(0.5分) 2.掌握反汇编与十六进制编程器 (0.5分) 3.能正确修改机器指令改变程序执行流程(0.5分) 4.能 ...

  9. 2018-2019-2 网络对抗技术 20165319 Exp3 免杀原理与实践

    免杀原理及基础问题回答 免杀原理: 免杀指的是一种能使病毒木马免于被杀毒软件查杀的技术.由于免杀技术的涉猎面非常广,其中包含反汇编.逆向工程.系统漏洞等黑客技术,所以难度很高,一般人不会或没能力接触这 ...

  10. npm 安装cnpm淘宝镜像时报错解决

    详细报错 D:\workspace\es61> npm install -g cnpm --registry=https://registry.npm.taobao.org npm WARN d ...