Redis(四):Redis的脚本语言Lua及限流策略

Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大。所以许多应用都选用它作为脚本语言:

  1. 在游戏领域,暴雪公司的"魔兽世界","愤怒的小鸟"
  2. Nginx 可以将 Lua 作为 嵌入式脚本语言,用于扩展其处理逻辑,特别是在高性能、高并发的场景下。
  3. Redis 将 Lua 脚本作为一种原子操作机制 ,用于在服务端一次性执行多个命令封装复杂逻辑,避免多条命令之间的非原子性问题。。

Redis 2.6 版本通过内嵌支持 Lua 环境。也就是说一般的运用,是不需要单独安装Lua的。

通过使用LUA脚本:

  1. 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行;
  2. 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入(Redis执行命令是单线程)。
  3. 复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑。

不过为了我们方便学习Lua语言,我们还是单独安装一个Lua。

Lua入门

安装Lua

Lua在linux中的安装
bash 复制代码
1、wget http://www.lua.org/ftp/lua-5.3.6.tar.gz
2、tar -zxvf lua-5.3.6.tar.gz
# 进入解压的目录:
3、cd lua-5.3.6
4、make linux
5、make install(需要在root用户下)
Lua在Windows中的安装

Lua 5.3(发布于 2015)之后,Lua 官网停止提供 Windows 平台的预编译二进制包(.exe 安装器), 只提供源码,可以用如下方式编译源码:

  • mingw32-make mingw(MinGW)
  • cl /Fe:lua.exe lua.c(Visual Studio)

也可以到Github 下载5.3之前的版本:https://github.com/rjpcomputing/luaforwindows/releases

Lua基本语法

https://www.cainiaojc.com/lua/lua-basic-syntax.html
lua 教程https://www.cainiaojc.com/lua/lua-tutorial.html

Lua的数据类型

Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。

数据类型 描述
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:false和true。
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。
Lua 中的函数
bash 复制代码
fuinction
fun1(c1,c2)
return
c1..c2 ## c1+c2
end # 结束

fuinction
fun2(c1,c2)
return
c1+c2 ## c1+c2
end # 结束

#调用函数
> print(fun1("1","2"))
12
> print(fun2("1","2"))
3
Lua 变量

Lua 中的变量全是全局变量,那怕是语句块或是函数里,除非用 local 显式声明为局部变量。局部变量的作用域为从声明位置开始到所在语句块结束。

变量的默认值均为 nil。

bash 复制代码
> a = 5
> local b = 5
> function fun()
>> c=5
>> local d = 5
>> end
> fun()
> print(a,b,c,d)
5       nil     5       nil
循环控制

for 循环

bash 复制代码
> a = {1,2,3}
> for i,v in ipairs(a) do
>> print(i,v)
>> end
1       1
2       2
3       3

if 语句

bash 复制代码
> a =10;
> if(a<20)
>> then
>> print("a less than 20")
>> end
a less than 20
>

if嵌套

bash 复制代码
> a = 10
> b = 20
> if(a==10)
>> then
>> if(b==20)
>> then
>> print("a=10,b=20")
>> end
>> end
a=10,b=20

垃圾回收

Lua 提供了以下函数collectgarbage ([opt [, arg]])用来控制自动内存管理:

  • collectgarbage("collect"): 做一次完整的垃圾收集循环。通过参数 opt 它提供了一组不同的功能:
  • collectgarbage("count"): 以 K 字节数为单位返回 Lua 使用的总内存数。 这个值有小数部分,所以只需要乘上 1024 就能得到 Lua 使用的准确字节数(除非溢出)。
  • collectgarbage("restart"): 重启垃圾收集器的自动运行。
  • collectgarbage("setpause"): 将 arg 设为收集器的 间歇率。 返回 间歇率 的前一个值。
  • collectgarbage("setstepmul"): 返回 步进倍率 的前一个值。
  • collectgarbage("step"): 单步运行垃圾收集器。 步长"大小"由 arg 控制。 传入 0 时,收集器步进(不可分割的)一步。 传入非 0 值, 收集器收集相当于 Lua 分配这些多(K 字节)内存的工作。 如果收集器结束一个循环将返回 true 。
  • collectgarbage("stop"): 停止垃圾收集器的运行。 在调用重启前,收集器只会因显式的调用运行。

连接数据库

Lua 数据库的操作库:LuaSQL。他是开源的,支持的数据库有:ODBC, ADO, Oracle, MySQL, SQLite 和 PostgreSQL

LuaSQL 可以使用 LuaRocks 来安装可以根据需要安装你需要的数据库驱动。

Redis中的Lua

使用LUA脚本的好处
  • 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
  • 原子操作,Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。(Redis执行命令是单线程)
  • 复用性,客户端发送的脚本会存储在Redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑
eval 命令
bash 复制代码
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379> eval "return redis.call('mset',KEYS[1],KEYS[2],ARGV[1],ARGV[2])" 2 key1 key2 first second
OK
127.0.0.1:6379> keys *
1) "key1"
2) "first"
127.0.0.1:6379> get key1
"key2"
127.0.0.1:6379>
evalsha 命令

但是eval命令要求你在每次执行脚本的时候都发送一次脚本,所以Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗, Redis 提供了evalsha 命令,它的作用和 EVAL一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 摘要。

bash 复制代码
127.0.0.1:6379> script load "return redis.call('set',KEYS[1],ARGV[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
127.0.0.1:6379> evalsha "c686f316aaf1eb01d5a4de1b0b63cd233010e63d" 1 key1 keytest
OK
127.0.0.1:6379> get key1
"keytest"
127.0.0.1:6379>

Redis与限流

java 复制代码
@Service
public class IsAcquire {
    //引入一个Redis的Lua脚本的支持
    private DefaultRedisScript<Long> getRedisScript;



    //判断限流方法---类似于RateLimiter
    public boolean acquire(String limitKey,int limit,int expire) throws  Exception{
        //连接Redis
        Jedis jedis =  new Jedis("127.0.0.1",6379);
        getRedisScript =new  DefaultRedisScript<>();
        getRedisScript.setResultType(Long.class);//脚本执行返回值 long
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
        Long result = (Long)jedis.eval(getRedisScript.getScriptAsString(),
                1,limitKey,String.valueOf(limit),String.valueOf(expire));
        if(result ==0){
            return false;
        }
        return true;
    }

    public static void main(String[] args) throws Exception {
        IsAcquire isAcquire = new IsAcquire();
        for (int i = 0;i<10;i++){
            Thread.sleep(1000);
            if(isAcquire.acquire("iphone",2,5)){ //10秒只能进行2次
                System.out.println("恭喜你,抢到iphone!");
            }else{
                System.out.println("-----------业务被限流");
            }
        }
    }
}

rateLimiter.lua

bash 复制代码
--java端送入三个参数(1个key,2个param  )string
--limitKey(redis中key的值)
local key =KEYS[1];
--limit(次数)
local times = ARGV[1];
--expire(秒S)
local expire = ARGV[2];
--对key-value中的 value +1的操作  返回一个结果

local afterval=  redis.call('incr',key);
if afterval ==1 then --第一次
    redis.call('expire',key,tonumber(expire) )  --失效时间(1S)  TLL 1S
    return 1; --第一次不会进行限制
end
--不是第一次,进行判断
if afterval > tonumber(times) then
    --限制了
    return 0;
end

return 1;

限流算法

  1. 固定窗口

    简单粗暴,但是有临界值问题
  2. 滑动窗口

动态演示:https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html

存在瞬时陡增、资源开销大等问题。

  1. 漏桶
    恒定输出速率,平滑处理请求,实现简单开销小,抗突发能力强。
    但是也存在一定的弊端:响应性差,不适合实时性要求高的场景;突发请求会被丢弃或排队,无法及时处理;无法动态适应流量变化。
  2. 令牌桶
    基于漏桶的弊端,出现了令牌桶:支持突发请求(有弹性)、处理速率可控且灵活、实时性更好、更符合真实系统流量模型、适合统计型限流 + 节流。
    其实令牌桶也有一定的弊端:实现复杂度略高、可能瞬间处理大量请求、无法精准控制请求间隔、
限流算法对比
特性 / 算法 固定窗口 Fixed Window 滑动窗口 Sliding Window 漏桶 Leaky Bucket 令牌桶 Token Bucket
🧠 核心机制 时间窗口计数,窗口跳变 时间窗口滑动,实时统计 固定速率出水 请求排队/丢弃 固定速率生成令牌 请求消耗令牌
🟢 优点 ✔ 实现简单 ✔ 性能高 ✔ 精度高 ✔ 限流更平滑 ✔ 输出速率恒定 ✔ 防突发、防过载 ✔ 实现简单 ✔ 支持突发流量 ✔ 响应更快 ✔ 兼顾限流与节流 ✔ 灵活配置
🔴 缺点 ✘ 临界突发可能超限 ✘ 限流不平滑 ✘ 实现较复杂 ✘ 占用内存高 ✘ 不支持突发 ✘ 实时性差 ✘ 处理速率固定 ✘ 不能动态调节 ✘ 实现略复杂 ✘ 突发放行可能冲击系统
📈 限流平滑性 ❌ 差(容易突发) ✅ 高 ✅ 极高(匀速) ✅ 中高(灵活)
🚀 支持突发流量 ❌ 不支持 ❌ 部分支持 ❌ 不支持 ✅ 支持(可积累令牌)
📉 实时响应性 ✅ 高(立即判断) ✅ 高 ❌ 低(需等待出水) ✅ 高(有令牌立即通过)
🛠️ 实现复杂度 ✅ 最简单 ❌ 中等偏高(需时间队列) ✅ 简单(计数器+时间戳) ❌ 中等(需时间计算+桶状态)
💾 资源占用 ✅ 低 ❌ 中/高 ✅ 低 ❌ 中等(维护令牌)
🌐 分布式实现 ✅ 易实现 ❌ 较复杂(需队列/窗口同步) ✅ 易实现(Redis计数器) ❌ 略复杂(需原子令牌生成)
🎯 适用场景 基础限流,单机 QPS 控制 如简单接口、控制台按钮 高精度限流 如频率限制、反爬虫 强保护系统,匀速节流 如数据库、第三方服务 实际生产高频限流 如 API 网关、用户请求处理
相关推荐
Cult Of1 小时前
内存、硬盘与缓存的技术原理及特性解析
缓存
bing_1582 小时前
如何保护 Redis 实例的安全?
数据库·redis·安全
江湖中的阿龙2 小时前
SpringBoot:基于 Redis 自定义注解实现后端接口防重复提交校验(幂等操作)
spring boot·redis·后端·幂等操作
Miraitowa_cheems4 小时前
Redis 核心概念、命令详解与应用实践:从基础到分布式集成
数据库·redis·缓存
bing_15812 小时前
在多租户或多服务共享 Redis 时,如何做逻辑隔离或权限控制?
数据库·redis·缓存
bing_15814 小时前
如何将 Redis 监控集成到微服务整体的监控体系中( 如 Prometheus + Grafana)
redis·微服务·prometheus
专业软件系统开发15 小时前
黑名单举报查询系统源码
redis·骗子查询系统·黑名单查询系统·举报查询系统源码
zfoo-framework19 小时前
线上redis的使用
数据库·redis·缓存
典孝赢麻崩乐急19 小时前
Redis学习-----Redis的基本数据类型
数据库·redis·学习