20210806_lua脚本学习笔记
1概述
1.1为什么lua脚本具有原子性
Redis保证以原子方式执行脚本,
redis会为lua脚本执行创建伪客户端模拟客户端调用redis执行命令,伪客户端执行lua脚本是排他的。
- 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
- 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他进程或者进程的命令插入。(最重要)
- 复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。
Redis使用同一个Lua解释器来执行所有命令,同时,Redis保证以一种原子性的方式来执行脚本:当lua脚本在执行的时候,不会有其他脚本和命令同时执行,这种语义类似于 MULTI/EXEC。从别的客户端的视角来看,一个lua脚本要么不可见,要么已经执行完。
然而这也意味着,执行一个较慢的lua脚本是不建议的,由于脚本的开销非常低,构造一个快速执行的脚本并非难事。但是你要注意到,当你正在执行一个比较慢的脚本时,所以其他的客户端都无法执行命令。
2代码实战
2.1maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>technicaltools</artifactId>
<groupId>com.kikop</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>myluascriptdemo</artifactId>
<dependencies>
<dependency>
<groupId>com.kikop</groupId>
<artifactId>mytechcommon</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<!--1.jedis for redis.clients-->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<!--2.guava-->
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!--3.spring-core-->
<!--for ClassPathResource api-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
2.2工具类
package com.kikop.util;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.springframework.core.io.ClassPathResource;
import redis.clients.jedis.Jedis;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* @author kikop
* @version 1.0
* @project Name: myluascriptdemo
* @file Name: MyJedisLuaTimeWindowLimiterUtil
* @desc Redis基于时间窗限流lua脚本
* 控制一个时间窗口内的请求数,如某个接口,每天、每时、每分、每秒的调用量
* @date 2021/8/7
* @time 13:00
* @by IDE: IntelliJ IDEA
*/
public class MyJedisLuaTimeWindowLimiterUtil {
// https://www.iteye.com/blog/jinnianshilongnian-2305117
// 因为Redis的限制(Lua中有写操作不能使用带随机性质的读操作,如TIME),不能在Redis Lua中使用TIME获取时间戳,因此只好从应用获取然后传入,
// 在分布式时间窗的情况下,慎用!
// 在某些极端情况下(机器时钟不准的情况下),限流会存在一些小问题。
// // 初始化化一次 sampled from DispatcherServlet.defaultStrategies
private static final Jedis jedis;
static {
try {
jedis = new Jedis("localhost", 6379);
} catch (Exception ex) {
throw new RuntimeException("constructor jedis instance failed!");
}
}
private String luaScript; // lua脚本内容
private String key;
private String limit;
private String expireTime;
public MyJedisLuaTimeWindowLimiterUtil(String scriptFile, String key, String limit, String expireTime) {
this.key = key;
this.limit = limit;
this.expireTime = expireTime;
try {
//->/E:/workdirectory/Dev/remotestudy/technicaltools/myredisdemo/myluascriptdemo/target/classes/mytestlimit.lua
String file = this.getClass().getResource("/" + scriptFile).getFile();
this.luaScript = Files.asCharSource(new File(file), Charset.defaultCharset()).read();
} catch (IOException e) {
e.printStackTrace();
}
// try {
// this.luaScript = Files.asCharSource(new ClassPathResource(scriptFile).getFile(), Charset.defaultCharset()).read();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
/**
* 阻塞式进行时间窗限流
*
* @return true:在限流范围内
*/
public boolean acauire() {
// key[1]:key
// argv[1]:limit
// argv[2]:expiretime
return (long) jedis.eval(luaScript, 1, key, limit, expireTime)
== 1L;
}
//
// /**
// * 控制 1秒 内不超过3个
// *
// * @param key
// * @param limit 限流大小
// * @param expireTime 过期时间
// * @return
// * @throws IOException
// */
// public static boolean acquire1(String key, String limit, String expireTime) throws IOException {
//
// // 1.guava 老API
// // String luaScript = Files.toString(new File("mytimewindowlimit.lua"),Charset.defaultCharset());
//
// // 2.新API
// String resourcePath = IOUtil.getResourcePath("/mytimewindowlimit.lua");
// String luaScript = Files.asCharSource(new File(resourcePath),
// Charset.defaultCharset()).read();
// return (Long) jedis.eval(luaScript, Lists.newArrayList(key), Lists.newArrayList(limit)) == 1;
// }
//
//
// /**
// * 控制 1秒 内不超过3个
// *
// * @param key
// * @param limit 限流大小
// * @param expireTime 过期时间
// * @param scriptFile
// * @return
// * @throws IOException
// */
// public static boolean acquire(String key, String limit, String expireTime, String scriptFile) throws IOException {
//
// // 1.guava 新API
// String luaScript = Files.asCharSource(new ClassPathResource(scriptFile).getFile(), Charset.defaultCharset()).read();
// return (Long) jedis.eval(luaScript, Lists.newArrayList(key), Lists.newArrayList(limit)) == 1;
// }
}
2.3lua脚本
local key = KEYS[1] --限流KEY
local limit = tonumber(ARGV[1]) --限流大小
local expire= ARGV[2] --过期时间
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置1秒过期
current = tonumber(redis.call("INCRBY", key,"1"));
if current == 1 then -- 第一次访问,设置过期时间
redis.call("expire", key,expire)
end
return 1 -- 返回值1,表示在可控范围内,不限流
end
2.4测试
package com.kikop;
import com.kikop.util.MyJedisLuaTimeWindowLimiterUtil;
import java.io.IOException;
/**
* @author kikop
* @version 1.0
* @project Name: myluascriptdemo
* @file Name: MyLuaScriptApplication
* @desc
* @date 2021/7/31
* @time 13:00
* @by IDE: IntelliJ IDEA
*/
public class MyLuaScriptApplication {
public static void main(String[] args) throws IOException {
String scriptFile = "mytimewindowlimit.lua";
String key = "kikop:" + System.currentTimeMillis() / 1000; // 此处将当前时间戳取秒数
String limit = "10"; // 限流大小
String expireTime = "1"; // 时间窗1,单位秒
MyJedisLuaTimeWindowLimiterUtil myJedisLuaTimeWindowLimiterUtil = new MyJedisLuaTimeWindowLimiterUtil(
scriptFile, key, limit, expireTime);
for (int i = 0; i < 11; i++) {
System.out.println("lua流控结果:" + myJedisLuaTimeWindowLimiterUtil.acauire());
}
}
}
网友评论