需求背景:
限制某sql在30秒内最多只能执行3次
需求分析
微服务分布式部署,既然是分布式限流,首先自然就想到了结合redis的zset数据结构来实现。
分析对zset的操作,有几个步骤,首先,判断zset中符合rangeScore的元素个数是否已经达到阈值,如果未达到阈值,则add元素,并返回true。如果已达到阈值,则直接返回false。
代码实现
首先,我们需要根据需求编写一个lua脚本
1 2 3 | redis.call( 'ZREMRANGEBYSCORE' , KEYS[ 1 ], 0 , tonumber(ARGV[ 3 ])) local res = 0 if (redis.call( 'ZCARD' , KEYS[ 1 ]) |
ARGV[1]: zset element
ARGV[2]: zset score(当前时间戳)
ARGV[3]: 30秒前的时间戳
ARGV[4]: zset key 过期时间30秒
ARGV[5]: 限流阈值
1 2 3 4 5 6 7 8 | private final RedisTemplate redisTemplate; public boolean execLuaScript(String luaStr, List keys, List<object data-origwidth= "" data-origheight= "" style= "width: 1264px;" > args){ RedisScript redisScript = RedisScript.of(luaStr, Boolean. class ) return redisTemplate.execute(redisScript, keys, args.toArray()); } </object> |
测试一下效果
1 2 3 4 5 6 7 | @SpringBootTest public class ApiApplicationTest { @Test public void test2() throws InterruptedException{ String luaStr = "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3]))n" + "local res = 0n" + " if (redis.call( 'ZCARD' , KEYS[ 1 ]) |
测试结果符合预期!
扩展阅读
lua脚本每次都需要传一长串脚本内容来回传输,会增加网络流量和延迟,而且每次都需要服务器重新解释和编译,效率较为低下。因此,不建议在实际生产环境中直接执行lua脚本,而应该使用lua脚本的hash值来进行传输。
为了方便使用,我们先把方法封装一下
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | import lombok.RequiredArgsConstructor; import org.springframework.data.redis.connection.RedisScriptingCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.stereotype.Component; import java.util.List; /** * @author 敖癸 * @formatter:on * @since 2024/3/25 */ @Component @RequiredArgsConstructor public class RedisService { private final RedisTemplate redisTemplate; private static RedisScriptingCommands commands; private static RedisSerializer keySerializer; private static RedisSerializer valSerializer; public String loadScript(String luaStr) { byte [] bytes = RedisSerializer.string().serialize(luaStr); return this .getCommands().scriptLoad(bytes); } public T execLuaHashScript(String hash, Class returnType, List keys, Object[] args) { byte [][] keysAndArgs = toByteArray( this .getKeySerializer(), this .getValSerializer(), keys, args); return this .getCommands().evalSha(hash, ReturnType.fromJavaType(returnType), keys.size(), keysAndArgs); } private static byte [][] toByteArray(RedisSerializer keySerializer, RedisSerializer argsSerializer, List keys, Object[] args) { final int keySize = keys != null ? keys.size() : 0 ; byte [][] keysAndArgs = new byte [args.length + keySize][]; int i = 0 ; if (keys != null ) { for (String key : keys) { keysAndArgs[i++] = keySerializer.serialize(key); } } for (Object arg : args) { if (arg instanceof byte []) { keysAndArgs[i++] = ( byte []) arg; } else { keysAndArgs[i++] = argsSerializer.serialize(arg); } } return keysAndArgs; } private RedisScriptingCommands getCommands() { if (commands == null ) { commands = redisTemplate.getRequiredConnectionFactory().getConnection().scriptingCommands(); } return commands; } private RedisSerializer getKeySerializer() { if (keySerializer == null ) { keySerializer = redisTemplate.getKeySerializer(); } return keySerializer; } private RedisSerializer getValSerializer() { if (valSerializer == null ) { valSerializer = redisTemplate.getValueSerializer(); } return valSerializer; } } |
- 测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @SpringBootTest @TestInstance (TestInstance.Lifecycle.PER_CLASS) public class ApiApplicationTest implements ApplicationContextAware { private static ApplicationContext context; private static RedisService redisService; public static String luaHash; private final static String LUA_STR = "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3]))n" + "local res = 0n" + "if(redis.call('ZCARD', KEYS[1]) keys = Collections.singletonList(" aaaa"); Object[] args = new Object[]{ "ele" + i, System.currentTimeMillis(), System.currentTimeMillis() - 30 * 1000 , 30 , 3 }; Boolean b = redisService.execLuaHashScript(luaHash, Boolean. class , keys, args); System.out.println(b); Thread.sleep( 3000 ); } } } |
使用的时候在项目启动时候,把脚本load一下,后续直接用hash值就行了
搞定收工!
以上就是利用redis lua脚本实现时间窗分布式限流的详细内容,更多关于redis lua时间窗分布式限流的资料请关注IT俱乐部其它相关文章!