Redis中lua脚本使用
Redis 为用户提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。
# EVAL命令
Redis提供了EVAL命令可以使开发者像调用其他Redis内置命令一样调用脚本:
[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]
可以通过key和arg这两个参数向脚本中传递数据,他们的值可以在脚本中分别使用KEYS和ARGV 这两个类型的全局变量访问。比如我们通过脚本实现一个set命令,通过在redis客户端中调用,那么执行的语句是:
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world
OK
2
EVAL命令是根据 key参数的数量-也就是上面例子中的1来将后面所有参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时也不能省略这个参数。如果没有参数则为0
127.0.0.1:6379> eval "return redis.call('get','hello')" 0
"world"
2
# EVALSHA命令
考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要。
- Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中。
- 执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了就执行脚本,否则返回“NOSCRIPT No matching script,Please use EVAL”。
# 将脚本加入缓存并生成sha1命令
127.0.0.1:6379> script load "return redis.call('get','hello')"
"fc1c184a3f0f566037349a820acef20fc7ece599"
# 调用eval命令
127.0.0.1:6379> evalsha "fc1c184a3f0f566037349a820acef20fc7ece599" 0
"world"
2
3
4
5
6
# 脚本的原子性
Redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令。所有的命令必须等待脚本执行完以后才能执行。为了防止某个脚本执行时间过程导致Redis无法提供服务。Redis提供了lua-time-limit参数限制脚本的最长运行时间。默认是5秒钟。 当脚本运行时间超过这个限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性),而是返回BUSY的错误。
# 使用场景
了解Redis中Lua原子性之后,就可以使用lua脚本来灵活的使用redis的事务,这里举几个简单的例子:
场景1:我们要判断一个IP是不是第一次访问,如果是第一次访问,那么返回状态1,否则插入该ip,并返回状态0:
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1
(integer) 0
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1
(integer) 1
2
3
4
场景2:使用redis限制30分钟内一个IP只允许访问5次:
思路:每次想把当前的时间插入到redis的list中,然后判断list长度是否达到5次,如果大于5次,那么取出队首的元素,和当前时间进行判断,如果在30分钟之内,则返回-1,其它情况返回1。
eval "redis.call('rpush', KEYS[1],ARGV[1]);if (redis.call('llen',KEYS[1]) >tonumber(ARGV[2])) then if tonumber(ARGV[1])-redis.call('lpop', KEYS[1])<tonumber(ARGV[3]) then return -1 else return 1 end else return 1 end" 1 'test_127.0.0.1' 1451460590 5 1800
通过上面两个场景可以看到,我们仅仅使用了lua的if语句,就可以实现这么方便的操作,如果使用其它的lua语法,肯定更加方便。