利用redis lua腳本實(shí)現(xiàn)時間窗分布式限流
需求背景:
限制某sql在30秒內(nèi)最多只能執(zhí)行3次
需求分析
微服務(wù)分布式部署,既然是分布式限流,首先自然就想到了結(jié)合redis的zset數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)。
分析對zset的操作,有幾個步驟,首先,判斷zset中符合rangeScore的元素個數(shù)是否已經(jīng)達(dá)到閾值,如果未達(dá)到閾值,則add元素,并返回true。如果已達(dá)到閾值,則直接返回false。
代碼實(shí)現(xiàn)
首先,我們需要根據(jù)需求編寫一個lua腳本
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3])) local res = 0 if(redis.call('ZCARD', KEYS[1]) < tonumber(ARGV[5])) then redis.call('ZADD', KEYS[1], tonumber(ARGV[2]), ARGV[1]) res = 1 end redis.call('EXPIRE', KEYS[1], tonumber(ARGV[4])) return res
ARGV[1]: zset element
ARGV[2]: zset score(當(dāng)前時間戳)
ARGV[3]: 30秒前的時間戳
ARGV[4]: zset key 過期時間30秒
ARGV[5]: 限流閾值
private final RedisTemplate<String, Object> redisTemplate; public boolean execLuaScript(String luaStr, List<String> keys, List<Object> args){ RedisScript<Boolean> redisScript = RedisScript.of(luaStr, Boolean.class) return redisTemplate.execute(redisScript, keys, args.toArray()); }
測試一下效果
@SpringBootTest public class ApiApplicationTest { @Test public void test2() throws InterruptedException{ String luaStr = "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[3]))\n" + "local res = 0\n" + "if(redis.call('ZCARD', KEYS[1]) < tonumber(ARGV[5])) then\n" + " redis.call('ZADD', KEYS[1], tonumber(ARGV[2]), ARGV[1])\n" + " res = 1\n" + "end\n" + "redis.call('EXPIRE', KEYS[1], tonumber(ARGV[4]))\n" + "return res"; for (int i = 0; i < 10; i++) { boolean res = execLuaScript(luaStr, Arrays.asList("aaaa"), Arrays.asList("ele"+i, System.currentTimeMillis(),System.currentTimeMillis()-30*1000, 30, 3)); System.out.println(res); Thread.sleep(5000); } } }
測試結(jié)果符合預(yù)期!
擴(kuò)展閱讀
lua腳本每次都需要傳一長串腳本內(nèi)容來回傳輸,會增加網(wǎng)絡(luò)流量和延遲,而且每次都需要服務(wù)器重新解釋和編譯,效率較為低下。因此,不建議在實(shí)際生產(chǎn)環(huán)境中直接執(zhí)行l(wèi)ua腳本,而應(yīng)該使用lua腳本的hash值來進(jìn)行傳輸。
為了方便使用,我們先把方法封裝一下
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<String, Object> 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> T execLuaHashScript(String hash, Class<T> returnType, List<String> 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<String> 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; } }
- 測試一下:
@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 = 0\n" + "if(redis.call('ZCARD', KEYS[1]) < tonumber(ARGV[5])) then\n" + " redis.call('ZADD', KEYS[1], tonumber(ARGV[2]), ARGV[1])\n" + " res = 1\n" + "end\n" + "redis.call('EXPIRE', KEYS[1], tonumber(ARGV[4]))\n" + "return res"; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { context = applicationContext; } @BeforeAll public static void before(){ redisService = context.getBean(RedisService.class); luaHash = redisService.loadScript(LUA_STR); System.out.println("lua腳本hash: "+ luaHash); } @Test public void testLuaHash() throws InterruptedException { for (int i = 0; i < 50; i++) { List<String> 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); } } }
使用的時候在項(xiàng)目啟動時候,把腳本load一下,后續(xù)直接用hash值就行了
搞定收工!
以上就是利用redis lua腳本實(shí)現(xiàn)時間窗分布式限流的詳細(xì)內(nèi)容,更多關(guān)于redis lua時間窗分布式限流的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Redis 8種基本數(shù)據(jù)類型及常用命令和數(shù)據(jù)類型的應(yīng)用場景小結(jié)
Redis是一種基于內(nèi)存操作的數(shù)據(jù)庫,其中多虧于高效的數(shù)據(jù)結(jié)構(gòu),本文主要介紹了Redis 8種基本數(shù)據(jù)類型及常用命令和數(shù)據(jù)類型的應(yīng)用場景小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03Redis全文搜索教程之創(chuàng)建索引并關(guān)聯(lián)源數(shù)據(jù)的教程
RediSearch提供了一種簡單快速的方法對 hash 或者 json 類型數(shù)據(jù)的任何字段建立二級索引,然后就可以對被索引的 hash 或者 json 類型數(shù)據(jù)字段進(jìn)行搜索和聚合操作,這篇文章主要介紹了Redis全文搜索教程之創(chuàng)建索引并關(guān)聯(lián)源數(shù)據(jù),需要的朋友可以參考下2023-12-12redis.clients.jedis.exceptions.JedisDataException:?NOAUTH?
本文主要介紹了redis.clients.jedis.exceptions.JedisDataException:?NOAUTH?Authentication?required數(shù)據(jù)操作異常的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05redis不能訪問本機(jī)真實(shí)ip地址的解決方案
這篇文章主要介紹了redis不能訪問本機(jī)真實(shí)ip地址的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07詳解redis desktop manager安裝及連接方式
這篇文章主要介紹了redis desktop manager安裝及連接方式,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09Redis實(shí)現(xiàn)編碼生成規(guī)則方式
在自動生成編碼時應(yīng)采用“MD+年月日+4位序列號”的規(guī)則,如“MD202310130001”,為避免使用隨機(jī)序列號導(dǎo)致的重復(fù)編碼,建議使用從0開始的自增序列號,此外,使用Redis的incrBy功能實(shí)現(xiàn)序列號自增,可以有效提高效率和降低實(shí)現(xiàn)難度2023-01-01