ew21w 发表于 2015-3-31 08:25:21

Redis中使用Lua脚本的开发思路

Redis提供了通过eval命令来执行Lua脚本。下面通过几个小例子来讲述如何在Redis服务端执行Lua脚本。

1. 执行Lua脚本的几个命令如下:


命令格式说明对应Jedis客户端Jedis对象的方法之一(有更多重载方法)
EVAL script numkeys key arg 执行Lua脚本
public Object eval(String script, int keyCount, String... params)

EVALSHA sha1 numkeys key arg 根据给定的 sha1 校验码,对缓存在服务器中的脚本进行求值public Object evalsha(String sha1, int keyCount, String... params)
SCRIPT LOAD script将给定的脚本缓存,不执行,并返回sha1校验值public String scriptLoad(String script)
SCRIPT EXISTS sha1 给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中public List<Boolean> scriptExists(String... sha1)
SCRIPT FLUSH
清除所有 Lua 脚本缓存

SCRIPT KILL杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效(如果已经执行了写操作,则需要通过shutdown nosave命令来处理)


2.通过redis-cli客户端执行Lua脚本


1
redis-cli --eval myscript.lua key1 key2 , arg1 arg2 arg3





    需要注意的是用逗号来分割key和参数,这里与在交互式模式下执行evel命令有所不同。

3.实际案例

   场景一:对一个特定请求1秒钟只允许访问10次,当符合请求访问条件时,返回True,否则返回False。
   Java客户端操作Redis服务,实现代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
   * 访问控制
   *
   * 1秒内最多可访问10次
   *
   * @param key
   * @return
   */
    public boolean isAccess(String key) {
      String rkey = "acc:" + key;
      long value = jedis.incr(rkey);
      if (value == 1) {
            jedis.expire(rkey, 1);
            return true;
      } else {
            boolean rs = value <= 10;
            return rs;
      }
    }





INCR命令作为计数器,如果rkey存在,则增加1返回最终值,否则初始化值为0,然后加1。如上程序,如果访问rkey不存在,则表示第一次请求,这时对其rkey设置过期时间为1秒,否则比较其值是否超过制定请求数的阀值10.

用Lua脚本来完成这一操作:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--[[
Judge status
KEYS:key
ARGV:request numbers
ARGV:expires times seconds
--]]

local key, rqn, exp= KEYS, ARGV, ARGV;
local value=redis.call("incr", key);
redis.log(redis.LOG_NOTICE, "incr "..key);
if(tonumber(value) == 1)then
   redis.call("expire", key,exp);
   redis.log(redis.LOG_NOTICE, "expire "..key.." "..exp)
   return true;
else
   return tonumber(value) <= tonumber(rqn);
end





   通过Java客户端代码实现该功能存在一定缺陷,比如每1秒就需要操作1个incr和expire命令,并且该命令是由客户端通过网络发起的,而使用Lua脚本则既可以保证操作的原子性,又能使每次操作只需要一个key即可在服务器端完成相应的判断操作。可以通过SCRIPT LOAD的方式将脚本缓存到服务器,通过sha1校验值+参数(Key,ARG)来执行,减轻网络传输,也对该功能做到较好的封装。

场景二:指定模式key批量删除
    redis目前提供的删除命令del仅支持删除指定数量的key,并不能通过指定模式key来进行删除,比如:del *user 删除以user结尾的key。

在redis中提供了keys命令,该命令可以通过指定模式key来获取key列表,下面通过keys和del命令组合实现一个指定模式key批量删除的命令。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--[[
Pattern delete key
KEYS:pattern
--]]

redis.log(redis.LOG_NOTICE, "call keys "..KEYS);

local keys=redis.call("keys", KEYS);
local count = 0;
if(keys and (table.maxn(keys) > 0)) then
    for index, key in ipairs(keys) do
      redis.log(redis.LOG_NOTICE, "del "..key);
      count = count +redis.call("del", key);
    end
end
return count;





需要注意的是场景二可以作为一种思路,通过Lua脚本组合redis内置命令来实现特定功能的命令。而这里的模式key批量删除并未一个好的命令,因为如果key的数量很大时,将会有比较严重的性能问题。redis默认限制Lua脚本执行时间最大为5秒,如果超过5秒将继续接受来自客户端的请求,并简单的返回BUSY结果。这时候则需要SCRIPT KILL或者SHUTDOWN NOSAVE命令做相应的处理。因此应该尽力保证脚本的执行速度极快。


场景三:生成随机数

对于Redis而且,脚本执行在相同数据集,相同参数下执行写命令具有一致性的。其不依赖与隐式的数据集,脚本执行过程中不同执行时期的状态变化,也不依赖外部I/O设备的输入。

要符合Redis服务执行的脚本条件,需要注意的地方比较多,可以参见:                         http://redis.io/commands/eval

下面是实现随机数列表的Lua脚本:


1
2
3
4
5
6
7
8
9
10
11
12
13
--[[
Random lpush a list key-value
KEYS:key name
ARGV:ramdom seed value
ARGV:add element count
--]]

math.randomseed(ARGV);
for i=1, ARGV, 1 do
    redis.call("lpush", KEYS, math.random());
end
redis.log(redis.LOG_NOTICE, "lpush " .. KEYS);
return true;





上述脚本通过改变randomseed函数的参数来实现随机数,如果两次执行上述脚本,ARGV参数值相同,则产生的随机数是相同的。

通过执行上述脚本,记录每次生产的值,然后删除对应key,再次生成。






对比上述结果,在执行该脚本时,随机数的生成由seed参数(第一个参数)决定的。
相同随机数种子下生成的随机数是相同的,如果再次执行脚本,指定生成的随机数个数n小于已经生成的随机数个数m,则取已经生成的前n个,如果指定生成的随机数个数n大于已经生成的随机数个数m,则次数再生成(n-m)个随机数,并固定下来。

4.Redis中使用Lua脚本总结
    Redis内置了Lua解释器,这为操作Redis服务器和数据提供了巨大的灵活性。
    文中几个场景并不见得实际,有效,但并不能掩盖Lua与Redis结合将为Redis的使用提供了更大的想象和操作空间。
    我们可以通过Lua来实现更多特定功能的命令;用Lua来封装复杂了Redis操作的业务;计数,统计,分析,收集数据;实现业务操作事务控制等等。更多场景,还需在实际中不断摸索和尝试。

页: [1]
查看完整版本: Redis中使用Lua脚本的开发思路