Redis中的Lua脚本入门 简介 Redis是高性能的键值对内存数据库,除了做缓存中间件的基本作用外还有很多用途,比如布隆过滤器、分布式锁等。Redis的单个命令都是原子性的,有时候我们希望能够组合多个Redis命令,并希望能保证复合操作的原子性 。Redis在2.6版本中引入了一个特性来解决这个问题,这就是Redis执行Lua脚本。
Lua脚本基础语法 Lua语言广泛作为其它语言的嵌入脚本,其语法简单,小巧,源码一共才200多K。接下来介绍Lua脚本的一些常用语法。
注释
变量 1 2 3 4 5 6 7 8 9 10 11 12 local a = 1 a, b = 0 , 1 a, b = b, a print (a)
数据类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 print (type ("Hello world" )) print (type (10.4 *3 ))print (type (print ))print (type (true ))print (type (nil ))a = {} a["key" ] = "value" key = 10 a[key] = 22 for k, v in pairs (a) do print (k .. " : " .. v) end b = {"a" , "b" } for k, v in pairs (b) do print ("k : " , k) end c = {k1="v1" , k2=2 } for k, v in pairs (c) do print (k .. " : " .. v) end a = {1 , 2 , 3 , 4 , 5 } print (table .unpack (a, 1 , #a))
判断结构 1 2 3 4 5 6 7 8 9 local a = 10 if a < 10 then print ('a小于10' ) elseif a < 20 then print ('a小于20,大于等于10' ) else print ('a大于等于20' ) end
循环结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 while (condition)do statements end a = 10 while (a < 20 )do a = a + 1 end print ("a 的值为:" , a) for i=10 ,1 ,-1 do print (i) end b = {"a" , "b" } for k, v in pairs (b) do print ("k : " , k) end
函数 1 2 3 4 5 6 7 8 function 函数名(参数列表) 函数逻辑代码 return 返回值 end
运算符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Redis中的Lua脚本 EVAL命令 在Redis中使用EVAL命令执行Lua脚本。在Lua脚本中使用redis.call()执行Redis命令。Lua脚本在Redis中是以原子方式执行的,在Redis服务器执行EVAL命令时,在命令执行完毕并向调用者返回结果之前,只会执行当前命令指定的Lua脚本包含的所有逻辑,其它客户端发送的命令将被阻塞,直到EVAL命令执行完毕为止。
EVAL
命令
script
Lua 脚本
numkeys
指定Lua脚本键的数量,即 KEYS 数组的长度
key
传递给Lua脚本零到多个键,空格隔开,在Lua 脚本中通过 **KEYS[index]**来获取对应的值,其中1 <= INDEX <= numkeys
arg
是传递给脚本的零到多个附加参数,空格隔开,在Lua脚本中通过**ARGV[index]**来获取对应的值
redis.call() 在Lua脚本中使用redis.call()执行Redis命令。参数个数可变。
1 2 3 eval "return redis.call('set', KEYS[1], ARGV[1])" 1 jujuyi hello该命令相当于执行了set jujuyi hello
Redis限流Lua脚本案例 通过Lua脚本,可以实现很多复杂功能,下面给出一个通过Redis的Lua脚本进行限流的案例(时间窗口限流)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 local username = KEYS[1 ]local timeWindow = tonumber (ARGV[1 ])local rediskey = "user_access_limit:" .. usernamelocal currentAccessCount = redis.call("INCR" , rediskey)if currentAccessCount == 1 then redis.call("EXPIRE" , rediskey, timeWindow) end return currentAccessCount
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private String timeWindow = "2" ;private int limit = 5 ;private String LUA_SCRIPT_PATH = "mylua.lua" ;private StringRedisTemplate stringRedisTemplate;public void accessLimit (String username) { DefaultRedisScript<Long> redisScript = new DefaultRedisScript <>(); redisScript.setScriptSource(new ResourceScriptSource (new ClassPathResource (LUA_SCRIPT_PATH))); redisScript.setResultType(Long.class); Long result; try { result = stringRedisTemplate.execute( redisScript, List.of(username), timeWindow); } catch (Throwable ex) { log.error("执行LUA脚本出错" , ex); throw ex; } if (result == null || result > limit) { throw new RuntimeException ("您的操作过于频繁" ); } }
扩展 Redis还支持Lua脚本管理,可通过script load保存Lua脚本,会返回一个脚本标识,后续可通过evalsha命令执行已经保存的脚本以减少脚本的反复传输。