Redis 脚本使用 Lua 解释器来执行脚本。 Reids 2.6 版本经过内嵌支持 Lua 环境。执行脚本的经常使用命令为 EVAL。html
EVAL script numkeys key [key ...] arg [arg ...] EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
1 EVAL script numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。
2 EVALSHA sha1 numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。
3 SCRIPT EXISTS script [script ...] 查看指定的脚本是否已经被保存在缓存当中。
4 SCRIPT FLUSH 从脚本缓存中移除全部脚本。
5 SCRIPT KILL 杀死当前正在运行的 Lua 脚本。
6 SCRIPT LOAD script 将脚本 script 添加到脚本缓存中,但并不当即执行这个脚本。redis
Redis Eval 命令使用 Lua 解释器执行脚本。算法
EVAL script numkeys key [key ...] arg [arg ...]
参数说明
script: 参数是一段 Lua 5.1 脚本程序。脚本没必要(也不该该)定义为一个 Lua 函数。
numkeys: 用于指定键名参数的个数。
key [key ...]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数能够在 Lua 中经过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
arg [arg ...]: 附加参数,在 Lua 中经过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量相似( ARGV[1] 、 ARGV[2] ,诸如此类)。编程
Redis Evalsha 命令根据给定的 sha1 校验码,执行缓存在服务器中的脚本。
EVALSHA sha1 numkeys key [key ...] arg [arg ...]json
redis 127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'" "232fd51614574cf0867b83d384a5e898cfd24e5a" redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0 "hello moto"
Redis Script Exists 命令用于校验指定的脚本是否已经被保存在缓存当中。
SCRIPT EXISTS script [script ...]数组
redis 127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'" # 载入一个脚本 "232fd51614574cf0867b83d384a5e898cfd24e5a" redis 127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 1 redis 127.0.0.1:6379> SCRIPT FLUSH # 清空缓存 OK redis 127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 0
SCRIPT FLUSH 从脚本缓存中移除全部脚本。
SCRIPT KILL 杀死当前正在运行的 Lua 脚本。
SCRIPT LOAD script 将脚本 script 添加到脚本缓存中,但并不当即执行这个脚本。缓存
这是从一个Lua脚本中使用两个不一样的Lua函数来调用Redis的命令的例子:服务器
redis.call() redis.pcall()
redis.call() 与 redis.pcall()很相似, 他们惟一的区别是当redis命令执行结果返回错误时, redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以Lua表的形式返回
redis.call() 和 redis.pcall() 两个函数的参数能够是任意的 Redis 命令:网络
> eval "return redis.call('set','foo','bar')" 0 OK
须要注意的是,上面这段脚本的确实现了将键 foo 的值设为 bar 的目的,可是,它违反了 EVAL 命令的语义,由于脚本里使用的全部键都应该由 KEYS 数组来传递
,就像这样:数据结构
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
要求使用正确的形式来传递键(key)是有缘由的,**由于不只仅是 EVAL 这个命令,全部的 Redis 命令,在执行以前都会被分析,籍此来肯定命令会对哪些键进行操做。
所以,对于 EVAL 命令来讲,必须使用正确的形式来传递键,才能确保分析工做正确地执行。 **
当 Lua 经过 call() 或 pcall() 函数执行 Redis 命令的时候,命令的返回值会被转换成 Lua 数据结构。 一样地,当 Lua 脚本在 Redis 内置的解释器里运行时,Lua 脚本的返回值也会被转换成 Redis 协议(protocol),而后由 EVAL 将值返回给客户端。
下面两点须要重点注意:
lua中整数和浮点数之间没有什么区别。所以,咱们始终将Lua的数字转换成整数的回复,这样将舍去小数部分。若是你想从Lua返回一个浮点数,你应该将它做为一个字符串
有两个辅助函数从Lua返回Redis的类型。
redis.error_reply(error_string) returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
redis.status_reply(status_string) returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.
return {err="My Error"} return redis.error_reply("My Error")
Redis 使用单个 Lua 解释器去运行全部脚本,而且, Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其余脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很相似。 在其余别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis 有一个内部的脚本缓存机制,所以它不会每次都从新编译脚本。
EVALSHA 命令,它的做用和 EVAL 同样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)。
客户端库的底层实现能够一直乐观地使用 EVALSHA 来代替 EVAL ,并指望着要使用的脚本已经保存在服务器上了,只有当 NOSCRIPT 错误发生时,才使用 EVAL 命令从新发送脚本,这样就能够最大限度地节省带宽。
刷新脚本缓存的惟一办法是显式地调用 SCRIPT FLUSH 命令,这个命令会清空运行过的全部脚本的缓存。一般只有在云计算环境中,才会执行这个命令。
不能访问系统时间或者其余内部状态
Redis 会返回一个错误,阻止这样的脚本运行: 这些脚本在执行随机命令以后(好比 RANDOMKEY 、 SRANDMEMBER 或 TIME 等),还会执行能够修改数据集的 Redis 命令。若是脚本只是执行只读操做,那么就没有这一限制。
每当从 Lua 脚本中调用那些返回无序元素的命令时,执行命令所得的数据在返回给 Lua 以前会先执行一个静默(slient)的字典序排序(lexicographical sorting)。举个例子,由于 Redis 的 Set 保存的是无序的元素,因此在 Redis 命令行客户端中直接执行 SMEMBERS ,返回的元素是无序的,可是,假如在脚本中执行 redis.call(“smembers”, KEYS[1]) ,那么返回的老是排过序的元素。
对 Lua 的伪随机数生成函数 math.random 和 math.randomseed 进行修改,使得每次在运行新脚本的时候,老是拥有一样的 seed 值。这意味着,每次运行脚本时,只要不使用 math.randomseed ,那么 math.random 产生的随机数序列老是相同的。
全局变量保护,为了防止没必要要的数据泄漏进 Lua 环境, Redis 脚本不容许建立全局变量
。若是一个脚本须要在屡次执行之间维持某种状态,它应该使用 Redis key 来进行状态保存。避免引入全局变量的一个诀窍是:将脚本中用到的全部变量都使用 local
关键字定义为局部变量。
Redis Lua解释器可用加载如下Lua库:
base
table
string
math
debug
struct 一个Lua装箱/拆箱的库
cjson 为Lua提供极快的JSON处理
cmsgpack为Lua提供了简单、快速的MessagePack操纵
bitop 为Lua的位运算模块增长了按位操做数。
redis.sha1hex function. 对字符串执行SHA1算法
每个Redis实例都拥有以上的全部类库,以确保您使用脚本的环境都是同样的。
struct, CJSON 和 cmsgpack 都是外部库, 全部其余库都是标准。
redis 127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0 "{\"foo\":\"bar\"}" redis 127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}" "bar" 127.0.0.1:6379> eval 'return cmsgpack.pack({"foo", "bar", "baz"})' 0 "\x93\xa3foo\xa3bar\xa3baz" 127.0.0.1:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz" 1) "foo" 2) "bar" 3) "baz"
在 Lua 脚本中,能够经过调用 redis.log 函数来写 Redis 日志(log):
redis.log(loglevel,message)
其中, message 参数是一个字符串,而 loglevel 参数能够是如下任意一个值:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
上面的这些等级(level)和标准 Redis 日志的等级相对应。
只有那些和当前 Redis 实例所设置的日志等级相同或更高级的日志才会被散发。
如下是一个日志示例:
redis.log(redis.LOG_WARNING, "Something is wrong with this script.") 执行上面的函数会产生这样的信息: [32343] 22 Mar 15:21:39 # Something is wrong with this script.
脚本应该仅仅用于传递参数和对 Redis 数据进行处理,它不该该尝试去访问外部系统(好比文件系统),或者执行任何系统调用。
除此以外,脚本还有一个最大执行时间限制,它的默认值是 5 秒钟,通常正常运做的脚本一般能够在几分之几毫秒以内完成,花不了那么多时间,这个限制主要是为了防止因编程错误而形成的无限循环而设置的。
最大执行时间的长短由 lua-time-limit
选项来控制(以毫秒为单位),能够经过编辑 redis.conf 文件或者使用 CONFIG GET 和 CONFIG SET 命令来修改它。
当一个脚本达到最大执行时间的时候,它并不会自动被 Redis 结束,由于 Redis 必须保证脚本执行的原子性,而中途中止脚本的运行意味着可能会留下未处理完的数据在数据集(data set)里面。
所以,当脚本运行的时间超过最大执行时间后,如下动做会被执行:
Redis 记录一个脚本正在超时运行
Redis 开始从新接受其余客户端的命令请求,可是只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 两个命令会被处理,对于其余命令请求, Redis 服务器只是简单地返回 BUSY 错误。
可使用 SCRIPT KILL
命令将一个仅执行只读命令的脚本杀死,由于只读命令并不修改数据,所以杀死这个脚本并不破坏数据的完整性
若是脚本已经执行过写命令,那么惟一容许执行的操做就是 SHUTDOWN NOSAVE
,它经过中止服务器来阻止当前数据集写入磁盘
一旦在pipeline中由于 EVALSHA 命令而发生 NOSCRIPT 错误,那么这个pipeline就再也没有办法从新执行了,不然的话,命令的执行顺序就会被打乱。
为了防止出现以上所说的问题,客户端库实现应该实施如下的其中一项措施:
老是在pipeline中使用 EVAL 命令
检查pipeline中要用到的全部命令,找到其中的 EVAL 命令,并使用 SCRIPT EXISTS
命令检查要用到的脚本是否是全都已经保存在缓存里面了。若是所需的所有脚本均可以在缓存里找到,那么就能够放心地将全部 EVAL 命令改为 EVALSHA 命令,不然的话,就要在pipeline的顶端(top)将缺乏的脚本用 SCRIPT LOAD
命令加上去。
实现访问者 $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
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 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end
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 中, 其余客户端可继续使用.
案例来源: < Redis实战 > 第六、11章, 构建步骤:
锁申请
首先尝试加锁:
成功则为锁设定过时时间; 返回;
失败检测锁是否添加了过时时间;
wait.
锁释放
检查当前线程是否真的持有了该锁:
持有: 则释放; 返回成功;
失败: 返回失败.
非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.call("EXPIRE", key, lockTimeOut) return 1 elseif redis.call("TTL", key) == -1 then redis.call("EXPIRE", key, lockTimeOut) end return 0
Lua脚本: release
local key = KEYS[1] local identifier = ARGV[1] if redis.call("GET", key) == identifier then redis.call("DEL", key) return 1 end return 0
参考:http://www.redis.cn/commands/...
http://www.redis.net.cn/tutor...
http://www.oschina.net/transl...
http://www.tuicool.com/articl...