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 发送多次请求, 而脚本只需一次即可, 减少网络传输;
-
原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
-
复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.
Lua语言模型
Lua是一种 便于嵌入应用程序 的脚本语言, 具备了作为通用脚本语言的所有功能. 其高速虚拟机实现非常有名(Lua的垃圾回收很有讲究- 增量垃圾回收 ), 在很多虚拟机系性能评分中都取得了优异的成绩. Home lua.org.
以嵌入式为方针设计的Lua, 在默认状态下简洁得吓人. 除了基本的数据类型外, 其他一概没有. 标注库也就 Coroutine、String、Table、Math、 I/O、OS, 再加上Modules包加载而已.
注: 本文仅介绍 Lua 与众不同的设计模型(对比 Java/C/C++、JavaScript、Python 与Go), 语言细节可参考文内和附录推荐的文章以及Lua之父Roberto Ierusalimschy的《Programming in Lua》(中文版: LUA程序设计(第2版)>)
基础
1. 数据类型
变量如果没有特殊说明为全局变量(那怕是语句块 or 函数内), 局部变量前需加local关键字.
2. 关键字
3. 操作符
函数(function)
在 Lua 中, 函数是和字符串、数值和表并列的基本数据结构, 属于第一类对象( first-class-object /一等公民), 可以和数值等其他类型一样赋给变量、作为参数传递, 以及作为返回值接收(闭包):
--
全局函数
:
求阶乘
function
fact
(
n
)
if
n
==
1
then
return
1
else
return
n *
fact
(
n
-
1
)
end
end
--
1.
赋给变量
local
func
=
fact
print
(
"func type: "
..
type
(
func
),
"fact type: "
..
type
(
fact
),
"result: "
..
func
(
4
))
--
2.
闭包
local
function
new_counter
()
local
value
=
0
;
return
function
()
value
=
value
+
1
return
value
end
end
local
counter
=
new_counter
()
print
(
counter
(),
counter
(),
counter
())
--
3.
返回值类似
Go
/
Python
local
random_func
=
function
(
param
)
return
9
,
'a'
,
true
,
"ƒ∂π"
,
param
end
local
var1
,
var2
,
var3
,
var4
,
var5
=
random_func
(
"no param is nil"
)
print
(
var1
,
var2
,
var3
,
var4
,
var5
)
--
4.
变数形参
local
function
square
(...)
local
argv
=
{
...
}
for
i
=
1
,
#argv do
argv
[
i
]
=
argv
[
i
]
*
argv
[
i
]
end
return
table
.
unpack
(
argv
)
end
print
(
square
(
1
,
2
,
3
))
表(table)
Lua最具特色的数据类型就是表(Table), 可以实现数组、Hash、对象所有功能的万能数据类型:
--
array
local
array
=
{
1
,
2
,
3
}
print
(
array
[
1
],
#array)
--
hash
local
hash
=
{
x
=
1
,
y
=
2
,
z
=
3
}
print
(
hash
.
x
,
hash
[
'y'
],
hash
[
"z"
],
#hash)
--
array
&
hash
array
[
'x'
]
=
8
print
(
array
.
x
,
#array)
Tips:
--
pairs
会遍历所有值不为
nil
的索引
,
与此类似的
ipairs
只会从索引
1
开始递遍历到最后一个值不为
nil
的整数索引
.
for
k
,
v
in
pairs
(
_G
)
do
print
(
k
,
" -> "
,
v
,
" type: "
..
type
(
v
))
end
用Hash实现对象的还有JavaScript, 将数组和Hash合二为一的还有PHP.
元表
metatable中的键名称为事件/event, 值称为元方法/metamethod, 我们可通过getmetatable()来获取任一值的metatable, 也可通过setmetatable()来替换table的metatable. Lua 事件一览表:
对于这些操作, Lua 都将其关联到 metatable 的事件Key, 当 Lua 需要对一个值发起这些操作时, 首先会去检查其metatable中是否有对应的事件Key, 如果有则调用之以控制Lua解释器作出响应.
MetaMethods
MetaMethods主要用作一些类似C++中的运算符重载操作, 如重载+运算符:
MetaTables 与 面向对象
Lua本来就不是设计为一种面向对象语言, 因此其面向对象功能需要通过元表(metatable)这种非常怪异的方式实现, Lua并不直接支持面向对象语言中常见的类、对象和方法: 其对象和类通过表实现, 而方法是通过函数来实现.
上面的Event一览表内我们看到有__index这个事件重载,这个东西主要是重载了find key操作, 该操作可以让Lua变得有点面向对象的感觉(类似JavaScript中的prototype). 通过Lua代码模拟:
local
function
gettable_event
(
t
,
key
)
local
h
if
type
(
t
)
==
"table"
then
local
value
=
rawget
(
t
,
key
)
if
value
~=
nil
then
return
value
end
h
=
getmetatable
(
t
).
__index
if
h
==
nil
then
return
nil
end
else
h
=
getmetatable
(
t
).
__index
if
h
==
nil
then
error
(
"error"
)
end
end
if
type
(
h
)
==
"function"
then
--
call the handler
return
(
h
(
t
,
key
))
else
--
or
repeat opration on it
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
))
对于任何事件, Lua的处理都可以归结为以下逻辑:
-
如果存在规定的操作则执行它;
-
否则从元表中取出各事件对应的__开头的元素, 如果该元素为函数, 则调用;
-
如果该元素不为函数, 则用该元素代替table来执行事件所对应的处理逻辑.
这里的代码仅作模拟, 实际的行为已经嵌入Lua解释器, 执行效率要远高于这些模拟代码.
方法调用的实现
面向对象的基础是创建对象和调用方法. Lua中, 表作为对象使用, 因此创建对象没有问题, 关于调用方法, 如果表元素为函数的话, 则可直接调用:
--
从
obj
取键为
x
的值
,
将之视为
function
进行调用
obj
.
x
(
foo
)
不过这种实现方法调用的方式, 从面向对象角度来说还有2个问题:
-
首先: obj.x这种调用方式, 只是将表obj的属性x这个函数对象取出而已, 而在大多数面向对象语言中, 方法的实体位于类中, 而非单独的对象中. 在JavaScript等基于原型的语言中, 是以原型对象来代替类进行方法的搜索, 因此每个单独的对象也并不拥有方法实体. 在Lua中, 为了实现基于原型的方法搜索, 需要使用元表的__index事件:
如果我们有两个对象a和b,想让b作为a的prototype需要setmetatable(a, {__index = b}), 如下例: 为obj设置__index加上proto模板来创建另一个实例:
proto
=
{
x
=
function
()
print
(
"x"
)
end
}
local
obj
=
{}
setmetatable
(
obj
,
{
__index
=
proto
})
obj
.
x
()
proto变成了原型对象, 当obj中不存在的属性被引用时, 就会去搜索proto.
-
其次: 通过方法搜索得到的函数对象只是单纯的函数, 而无法获得最初调用方法的表(接收器)相关信息. 于是, 过程和数据就发生了分离.JavaScript中, 关于接收器的信息可由关键字this获得, 而在Python中通过方法调用形式获得的并非单纯的函数对象, 而是一个“方法对象” –其接收器会在内部作为第一参数附在函数的调用过程中.
而Lua准备了支持方法调用的语法糖:obj:x(). 表示obj.x(obj), 也就是: 通过冒号记法调用的函数, 其接收器会被作为第一参数添加进来(obj的求值只会进行一次, 即使有副作用也只生效一次).
--
这个语法糖对定义也有效
function
proto
:
y
(
param
)
print
(
self
,
param
)
end
-
Tips
:
用冒号记法定义的方法
,
调用时最好也用冒号记法
,
避免参数错乱
obj
:
y
(
"parameter"
)
基于原型的编程
Lua虽然能够进行面向对象编程, 但用元表来实现, 仿佛把对象剖开看到五脏六腑一样.
《代码的未来》中松本行弘老师向我们展示了一个基于原型编程的Lua库, 通过该库, 即使没有深入解Lua原始机制, 也可以实现面向对象:
--
--
Author
:
Matz
--
Date
:
16
/
9
/
24
--
Time
:
下午
5
:
13
--
--
Object
为所有对象的上级
Object
=
{}
--
创建现有对象副本
function
Object
:
clone
()
local
object
=
{}
--
复制表元素
for
k
,
v
in
pairs
(
self
)
do
object
[
k
]
=
v
end
--
设定元表
:
指定向自身
`
转发
`
setmetatable
(
object
,
{
__index
=
self
})
return
object
end
--
基于类的编程
function
Object
:
new
(...)
local
object
=
{}
--
设定元表
:
指定向自身
`
转发
`
setmetatable
(
object
,
{
__index
=
self
})
--
初始化
object
:
init
(...)
return
object
end
--
初始化实例
function
Object
:
init
(...)
--
默认不进行任何操作
end
Class
=
Object
:
new
()
另存为prototype.lua, 使用时只需require()引入即可:
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
)
Redis – Lua
在传入到Redis的Lua脚本中可使用redis.call()/redis.pcall()函数调用Reids命令:
redis
.
call
(
"set"
,
"foo"
,
"bar"
)
local
value
=
redis
.
call
(
"get"
,
"foo"
)
redis.call()返回值就是Reids命令的执行结果, Redis回复与Lua数据类型的对应关系如下:
Reids返回值类型
|
Lua数据类型
|
整数
|
数值
|
字符串
|
字符串
|
多行字符串
|
表(数组)
|
状态回复
|
表(只有一个ok字段存储状态信息)
|
错误回复
|
表(只有一个err字段存储错误信息)
|
注: Lua 的 false 会转化为空结果.
redis-cli提供了EVAL与EVALSHA命令执行Lua脚本:
-
EVAL
EVAL script numkeys key [key ...] arg [arg ...]
key和arg两类参数用于向脚本传递数据, 他们的值可在脚本中使用KEYS和ARGV两个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集合无序, 因此即使两个集合内元素相同, 其输出结果也并不一样), 这类命令包括SINTER、SUNION、SDIFF、SMEMBERS、HKEYS、HVALS、KEYS 等.
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环境协作组件
EVAL命令原理
EVAL命令执行分为以下三个步骤:
-
定义Lua函数:
在 Lua 环境内定义 Lua函数 : 名为f_前缀+脚本 SHA1 校验和, 体为脚本内容本身. 优势:
-
执行脚本步骤简单, 调用函数即可;
-
函数的局部性可保持 Lua 环境清洁, 减少垃圾回收工作量, 且避免使用全局变量;
-
只要记住 SHA1 校验和, 即可在不知脚本内容的情况下, 直接调用 Lua 函数执行脚本(EVALSHA命令实现).
-
将脚本保存到lua_scripts字典;
-
执行脚本函数:
执行刚刚在定义的函数, 间接执行 Lua 脚本, 其准备和执行过程如下:
1). 将EVAL传入的键名和参数分别保存到KEYS和ARGV, 然后将这两个数组作为全局变量传入到Lua环境;
2). 为Lua环境装载超时处理hook(handler), 可在脚本出现运行超时时让通过SCRIPT KILL停止脚本, 或SHUTDOWN关闭Redis;
3). 执行脚本函数;
4). 移除超时hook;
5). 将执行结果保存到客户端输出缓冲区, 等待将结果返回客户端;
6). 对Lua环境执行垃圾回收.
对于会产生随机结果但无法排序的命令(如只产生一个元素, 如 SPOP、SRANDMEMBER、RANDOMKEY、TIME), Redis在这类命令执行后将脚本状态置为lua_random_dirty, 此后只允许脚本调用只读命令, 不允许修改数据库值.
实践
使用Lua脚本重新构建带有过期时间的分布式锁.
案例来源: Redis实战> 第6、11章, 构建步骤:
-
锁申请
-
成功则为锁设定过期时间; 返回;
-
失败检测锁是否添加了过期时间;
-
锁释放
非Lua实现
String
acquireLockWithTimeOut
(
Jedis
connection
,
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
(
InterruptedException
ignored
)
{
}
}
return
null
;
}
boolean
releaseLock
(
Jedis
connection
,
String
lockName
,
String
identifier
)
{
String
key
=
"lock:"
+
lockName
;
connection
.
watch
(
key
);
// 确保当前线程还持有锁
if
(
identifier
.
equals
(
connection
.
get
(
key
)))
{
Transaction
transaction
=
connection
.
multi
();
transaction
.
del
(
key
);
return
transaction
.
exec
().
isEmpty
();
}
connection
.
unwatch
();
return
false
;
}
Lua脚本实现
Lua脚本: acquire
local
key
=
KEYS
[
1
]
local
identifier
=
ARGV
[
1
]
local
lockTimeOut
=
ARGV
[
2
]
--
锁定成功
if
redis
.
call
(
"SETNX"
,
key
,
identifier
)
==
1
then
redis
.