2021-08-06_lua脚本学习笔记

2021-08-07  本文已影响0人  kikop

20210806_lua脚本学习笔记

1概述

1.1为什么lua脚本具有原子性

Redis保证以原子方式执行脚本,

redis会为lua脚本执行创建伪客户端模拟客户端调用redis执行命令,伪客户端执行lua脚本是排他的。

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());
        }

    }

}

参考

上一篇 下一篇

猜你喜欢

热点阅读