lua脚本在redis中的使用场景

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: lua脚本在redis中的使用场景

一、概述

1、什么是lua脚本

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。

其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因为广泛的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。

比如:Lua脚本用在很多游戏上,主要是Lua脚本可以嵌入到其他程序中运行,游戏升级的时候,可以直接升级脚本,而不用重新安装游戏。

2、Redis中为什么引入Lua脚本?

Redis是高性能的key-value内存数据库,在部分场景下,是对关系数据库的良好补充。

Redis提供了非常丰富的指令集,官网上提供了200多个命令。但是某些特定领域,需要扩充若干指令原子性执行时,仅使用原生命令便无法完成。

Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。

3、使用lua好处

在2.6版本推出了 lua 脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
  • 复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑

二、redis中的原生lua命令

命令不多,就下面这几个:

  • EVAL
  • EVALSHA
  • SCRIPT LOAD
  • SCRIPT EXISTS
  • SCRIPT FLUSH
  • SCRIPT KILL

1、EVAL命令

意义:表示执行lua脚本,需要传入lua脚本代码

格式:EVAL script numkeys key [key …] arg [arg …]

参数:

  • script:参数是一段 Lua5.1 脚本程序。脚本不必(也不应该1)定义为一个 Lua 函数
  • numkeys:指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为0
  • key [key …]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
  • arg [arg …]: 附加参数。在Lua脚本中通过ARGV[1],ARGV[2]获取。

案例:

例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
 
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"
 
 
例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
 
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"
 
 
例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second ,其实{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua语法中“使用默认索引”的table表,相当于java中的map中存放四条数据。Key分别为:1、2、3、4,而对应的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
举此例子仅为说明eval命令中参数的如何使用。项目中编写Lua脚本最好遵从key、arg的规范。
 
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 
1) "key1"
2) "key2"
3) "first"
4) "second"
 
 
例4:使用了redis为lua内置的redis.call函数,脚本内容为:先执行SET命令,在执行EXPIRE命令,numkeys=1,keys数组有一个元素userAge(代表redis的key)
,arg数组元素中有两个元素:10(代表userAge对应的value)和60(代表redis的存活时间)
 
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 44

通过上面的例4,我们可以发现,脚本中使用redis.call()去调用redis的命令。

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:

redis.call() 和 redis.pcall()

这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误

当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:

127.0.0.1:6379> lpush foo a

(integer) 1


127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0

(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value


和 redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:

127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0

(error) ERR Operation against a key holding the wrong kind of value

2、SCRIPT LOAD命令 和 EVALSHA命令

script load和evalsha

每次执行eval命令都会将脚本传入到redis中,如果脚本执行频率很高的话,就会增加网络开销

因此redis提供了缓存脚本的命令script load 再调用evalsha 执行脚本,每次执行evalsha命令都是传入脚本在redis服务器的hash值,减少了脚本传输的网络开销。两个命令的使用如下:

在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

案例:

## SCRIPT LOAD加载脚本,并得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"
 
## EVALSHA使用sha1值,并拼装和EVAL类似的numkeys和key数组、arg数组,调用脚本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 43

3、SCRIPT EXISTS 命令

命令格式:SCRIPT EXISTS sha1 [sha1 …]

作用:给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

4、SCRIPT FLUSH 命令

命令格式:SCRIPT FLUSH

作用:清除Redis服务端所有 Lua 脚本缓存

127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345

1) (integer) 1

127.0.0.1:6379> SCRIPT FLUSH

OK

127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345

1) (integer) 0

5、SCRIPT KILL 命令

命令格式:SCRIPT FLUSH

作用:杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。

这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。

假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

三、Redis执行Lua脚本文件

1、编写lua脚本

local key = KEYS[1]

local val = redis.call("GET", key);


if val == ARGV[1]

then

       redis.call('SET', KEYS[1], ARGV[2])

       return 1

else

       return 0

end

2、执行lua脚本文件

执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] , arg [arg …]

如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi

注意!!!

“–eval"而不是命令模式中的"eval”,一定要有前端的两个-

脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值

key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错

## Redis客户端执行

127.0.0.1:6379> set userName zhangsan

OK

127.0.0.1:6379> get userName

"zhangsan"


## linux服务器执行

## 第一次执行:compareAndSet成功,返回1

## 第二次执行:compareAndSet失败,返回0

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi

(integer) 1

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi

(integer) 0

四、jedis执行lua脚本命令两种API

eval命令执行lua脚本

evalsha+script load 执行事先缓存到redis服务器上的lua脚本

四、实例

1、使用Lua控制IP访问频率

需求:实现一个访问频率控制,某个IP在短时间内频繁访问页面,需要记录并检测出来,就可以通过Lua脚本高效的实现。

小声说明:本实例针对固定窗口的访问频率,而动态的非滑动窗口。即:如果规定一分钟内访问10次,记为超限。在本实例中前一分钟的最后一秒访问9次,下一分钟的第1秒又访问9次,不计为超限。

脚本如下:

local visitNum = redis.call('incr', KEYS[1])


if visitNum == 1 then

       redis.call('expire', KEYS[1], ARGV[1])

end


if visitNum > tonumber(ARGV[2]) then

       return 0

end


return 1;

演示

## LimitIP:127.0.0.1为key, 10 3表示:同一IP在10秒内最多访问三次

## 前三次返回1,代表未被限制;第四、五次返回0,代表127.0.0.1这个ip已被拦截

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3

(integer) 1

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3

(integer) 1

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3

(integer) 1

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3

(integer) 0

[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3

(integer) 0

2、红包雨

2.1、红包雨的需求分析及概要设计

如同前两年的爆款“答题抢红包”的类似需求,当一轮题目答完后会下起红包雨,我们本次分析的也是类似的需求。

题目答完前,已初始化本轮次的红包雨批次ID,并将总金额拆分成若干份放入此批次红包。题目答完后,用户可抢红包,每轮次每用户最多抢3个。红包雨结束后,需在页面展示本轮次红包雨中抢夺金额前N名。

2.2、lua脚本演示

红包id为:7758521,用户id分别为:u1、u3、u3、u4、u5

redis的key分别为:RedBagBatch:7758521、RedBagBatch:7758521:Users、RedBagBatch:7758521:Limit

给redBagId=7758521的红包,初始化进去10个红包。红包金额为1-10,随机顺序。以下为redis-cli的截图

-- 抢红包雨的lua脚本

local REDBAG_LIMIT_KEY = KEYS[1]

local REDBAG_INFO_KEY = KEYS[2]

local REDBAG_USER_KEY = KEYS[3]


local userId = ARGV[1]


-- 抢了超过3个,返回没抢到

local grabCount = redis.call('hincrby', REDBAG_LIMIT_KEY, userId, 1)

if(grabCount > 3) then

   return "-1"

end


-- pop一个红包数据

local amount = redis.call('lpop', REDBAG_INFO_KEY)


-- 没抢到返回0

if(amount == nil) then

   return "-2"

end


-- 放入结果Set

redis.call('zadd', REDBAG_USER_KEY, amount, userId.."-"..grabCount);


return amount


## 为方便演示,以下为redis客户端使用命令行操作记录

## step1:初始化红包数据

127.0.0.1:6379> lpush RedBagBatch:7758521 1 3 10 6 8 7 2 5 4 9

(integer) 10

127.0.0.1:6379> lrange RedBagBatch:7758521 0 -1

1) "9"

2) "4"

3) "5"

4) "2"

5) "7"

6) "8"

7) "6"

8) "10"

9) "3"

10) "1"


## step2:使用lua脚本抢红包,模拟用户抢夺情况

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u1

"9"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u2

"4"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u3

"5"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u4

"2"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u5

"7"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u1

"8"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u2

"6"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u1

"10"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u1

"-1"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u2

"3"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u2

"-1"

[root@vm01 learn_lua]# redis-cli -a 123456 --eval RedBagBatchGrab.lua RedBagBatch:7758521:Limit RedBagBatch:7758521 RedBagBatch:7758521:Users , u3

"1"


## step3.查看红包雨排行榜,按红包金额倒序(奇数行为:value,偶数行为:score)

## value解读:用户-本轮次红包第N次抢夺

## score解读:红包金额

127.0.0.1:6379> zrevrange RedBagBatch:7758521:Users 0 -1 WITHSCORES

1) "u1-3"

2) "10"

3) "u1-1"

4) "9"

5) "u1-2"

6) "8"

7) "u5-1"

8) "7"

9) "u2-2"

10) "6"

11) "u3-1"

12) "5"

13) "u2-1"

14) "4"

15) "u2-3"

16) "3"

17) "u4-1"

18) "2"

19) "u3-2"

20) "1"

2.3、生产环境代码

真正在项目中总不能像上面显示那样,使用命令行操作lua脚本了,下面介绍下我们在项目中是如何使用的。

项目环境:springmvc+spring+mybatis

项目redis客户端:原生Jedis

基本步骤如下:

创建一个Service类,实现ApplicationListener接口,当容器初始化完成时触发“初始化加载lua脚本”的事件

加锁加载lua脚本:使用script load方式调用Redis服务端,获取该脚本的sha值,方便后续使用。类似单例,加载一份,后续循环使用,节约资源。

使用lua脚本:后续的每次调用,均使用初始化产生的该脚本的sha值,调用redis的evalsha方法,并传入相应的keys和params,执行脚本。

看一下伪代码:

@Service

public class RedBagBatchServiceImpl implements ApplicationListener<ContextRefreshedEvent> {


   // 红包雨lua脚本script load的sha1值

   private String redBagScriptSha1 = "";

   private static final String LUA_SCRIPT_PATH = "/lua_script/";

   

   @Resource

   private RedisUtil redisUtil;

   

   @Override

   public void onApplicationEvent(ContextRefreshedEvent event) {

       try {

           logger.info("初始化LUA脚本");

           initRedBagScriptSha1();

           logger.info("成功初始化LUA脚本");

       } catch (LiveException e) {

           logger.error("初始化lua脚本出错", e);

       }

   }


   /**

    * 读取抢红包Lua脚本

    */

   private String initRedBagScriptSha1() {

       if (StringUtils.isBlank(redBagScriptSha1)) {

           synchronized (redBagScriptSha1) {

               if (StringUtils.isBlank(redBagScriptSha1)) {

                   try {

                       // 读取资源文件内容,并scriptLoad到Redis,记录sha值

                       String scriptText = readResource(LUA_SCRIPT_PATH + "/RedBagBatchGrab.lua");

                       redBagScriptSha1 = redisUtil.scriptLoad(scriptText);

                   } catch (Exception e) {

                       logger.error("初始化LUA脚本出错 - " + e.getMessage(), e);

                       throw new RunTimeException("初始化LUA脚本出错 - " + e.getMessage());

                   }

               }

           }

       }

       return grabScriptSha1;

   }

   

   /**

   * 抢红包的方法

   */

   @Override

   public BigDecimal grabRedBag(Long userId, Long redBagId) {

       

       // 判断用户是否在黑名单、红包雨时间是否已失效等业务逻辑的判断

       // ..............


       // 抢红包lua脚本使用keys,需要与脚本中顺序保持一致

       List<String> luaKey = new ArrayList<>();

       luaKey.add(receiveLimitKey); // KEYS[1]

       luaKey.add(redBagBatchKey); // KEYS[2]

       luaKey.add(redBagUserKey); // KEYS[3]


       // 抢红包lua脚本使用args,需要与脚本中顺序保持一致

       List<String> luaArgs = new ArrayList<>();

       luaArgs.add(userId.toString());  // ARGV[1]


       Object luaResult = redisUtil.evalsha(this.redBagScriptSha1, luaKey, luaArgs);


       // 没抢到返回0

       if(luaResult == null || new BigDecimal(luaResult.toString()).compareTo(BigDecimal.ZERO) == 0){

           return BigDecimal.ZERO;

       }


       // 抢到,记日志并返回结果

       BigDecimal result = new BigDecimal(luaResult.toString());

       return result;

   }

}


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
11天前
|
存储 NoSQL Java
【超长文】Redis在项目中的17种使用场景
Redis 是一个开源的高性能键值对数据库,它以其内存中数据存储、键过期策略、持久化、事务、丰富的数据类型支持以及原子操作等特性,在许多项目中扮演着关键角色。以下是V哥整理的17个Redis在项目中常见的使用场景
|
12天前
|
JSON 监控 数据格式
使用Lua代码扩展上网行为管理软件的脚本功能
本文介绍了如何使用Lua脚本增强上网行为管理,包括过滤URL、记录用户访问日志、控制带宽和自动提交监控数据到网站。Lua是一种轻量级语言,适合编写扩展脚本。文中提供多个示例代码,如URL过滤器、用户活动日志记录器和带宽控制器,帮助用户根据需求定制网络管理功能。通过这些示例,用户可以快速掌握Lua在上网行为管理中的应用。
41 4
|
1月前
|
弹性计算 NoSQL Shell
redis没设置密码,莫名被设置了4个sh脚本
阿里云ECS实例上未设密码的Redis服务被发现含有未知来源的SH脚本,这些脚本定时从外部URL下载并执行代码。这可能是服务器遭受恶意攻击的迹象。建议立即检查系统日志,确认是否被黑,并移除这些脚本。同时,为Redis设置密码,加强安全防护。若不确定,可寻求专业安全团队帮助。
95 2
|
1月前
|
存储 缓存 NoSQL
深入浅出Redis(十):Redis的Lua脚本
深入浅出Redis(十):Redis的Lua脚本
|
1月前
|
存储 SQL NoSQL
Redis入门到通关之五大基本数据类型及其使用场景
Redis入门到通关之五大基本数据类型及其使用场景
22 0
|
1月前
|
存储 NoSQL 调度
Redis Lua脚本:原子性的真相揭秘
【4月更文挑战第20天】
159 0
Redis Lua脚本:原子性的真相揭秘
|
1月前
|
存储 SQL NoSQL
Redis 入门、基础。(五种基本类型使用场景)
Redis 入门、基础。(五种基本类型使用场景)
15 0
|
存储 缓存 NoSQL
Redis进阶实践之七Redis和Lua初步整合使用
原文:Redis进阶实践之七Redis和Lua初步整合使用 一、引言        Redis学了一段时间了,基本的东西都没问题了。从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运行在任何平台上,也可以嵌入到大多数语言当中,来扩展其功能。
2345 0
|
24天前
|
NoSQL Linux Redis
Redis -- 安装客户端redis-plus-plus
Redis -- 安装客户端redis-plus-plus
48 0
|
1月前
|
NoSQL Linux 网络安全
Linux安装Redis(详细教程)
Linux安装Redis(详细教程)
93 2