深入学习Redis_(四)Redis与Lua脚本

上一章

深入学习Redis_(一)五种基本数据类型、RedisTemplate、RedisCache、缓存雪崩等

深入学习Redis_(二)淘汰策略、持久化机制、主从复制、哨兵模式等

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

一、Lua介绍

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,几乎在所有操作系统和平台上都可以编译,运行。 其设计目 的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
官网:http://www.lua.org/

1. 应用场景

  1. 游戏开发
  2. 独立应用脚本
  3. Web 应用脚本
  4. 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  5. 安全系统,如入侵检测系统
  6. redis中嵌套调用实现类似事务的功能
  7. web容器中应用处理一些过滤 缓存等等的逻辑,例如nginx。

2. Linux安装Lua

yum install ‐y gcc
yum install libtermcap‐devel ncurses‐devel libevent‐devel readline‐devel curl ‐R ‐O http://www.lua.org/ftp/lua‐5.3.5.tar.gz
tar ‐zxf lua‐5.3.5.tar.gz
cd lua‐5.3.5
make linux test
make install

LUA的基本语法 lua有交互式编程和脚本式编程。
交互式编程就是直接输入语法,就能执行。
脚本式编程需要编写脚本文件,然后再执行。
一般采用脚本式编程。(例如:编写一个hello.lua的文件,输入文件内容,并执行lua hello.lua即可)

3. 简单了解Lua

创建helloworld.lua文件,内容为

print("hello");

在这里插入图片描述

保存。执行命令

lua helloworld.lua

在这里插入图片描述
单行注释:两个减号是单行注释:

--单行注释

多行注释:

--[[
多行注释 
多行注释 
--]]

Lua关键字

and、elseif、function、nil、return、while、break do、end false、if in、not or、then true、else、for、local、repeat、until

定义变量

全局变量,默认的情况下,定义一个变量都是全局变量, 如果要用局部变量 需要声明为local.例如:

‐‐ 全局变量赋值
 a=1
‐‐ 局部变量赋值 
local b=2

如果变量没有初始化:则 它的值为nil 这和java中的null不同。

Lua中的数据类型

Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作 为参数传递或结果返回。
Lua 中有 8 个基本类型分别为:nilbooleannumberstringuserdatafunctionthreadtable

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

流程控制

如下:类似于if else

if  条件表达式  then
    语句块
elseif  条件表达式 then
    语句块
else 
    语句块
end

if(0) then
	print("0 为 true") 
else
	print("0 不为true") 
end

函数

lua中也可以定义函数,类似于java中的方法。

--[[函数返回两个值的最大值--]]
function max(num1,num2)
	if(num1 > num2) then 
		result = num1
	else
		result = num2
	end
	return result
end
--调用函数
print("两值比较最大值为 ",max(10,4))
print("两值比较最大值为 ",max(5,6))

在这里插入图片描述

require 函数

require 用于 引入其他的模块,类似于java中的类要引用别的类的效果。
用法:

require "<模块名>"

例如:在demo.lua脚本中引入module.lua脚本
module.lua脚本

-- 定义一个模块
module = {}
module.username = "张三"
--定义一个全局方法
function module.fun1()
	print("fun1")
end
local function fun2()
	print("fun2")
end
function module.fun3()
	fun2()
end
return module

demo.lua脚本

-- 引入module
require("module")
print(module.username)
module.fun1()
module.fun3()

在这里插入图片描述

二、Redis整合Lua脚本

原子性

Redis执行Lua脚本是原子性的,脚本执行期间Redis不会执行其他命令。所有命令都必须等待脚本执行完成后才能执行。
为了防止某个脚本执行时间过长导致Redis无法提供服务(比如陷入死循环),Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为五秒钟。
当脚本运行时间超过这一限制后,Redis将开始接受其他命令但不会执行(保证脚本的原子性),而是会返回“BUSY”错误。
此时虽然Redis可以接受任何命令,但实际会执行的只有两个:SCRIPT KILLSHUTDOWN NOSAVE.
如果当前的脚本对Redis的数据进行了修改,则SCRIPT KILL命令不会终止脚本的运行以防止脚本只执行了一部分,会违背脚本的原子性要求。
这时候只能通过SHUTDOWN NOSAVE强行终止Redis。

EVAL方法

EVAL script numkeys key [key ...] arg [arg ...]
官方文档:

Introduction to EVAL
EVAL and EVALSHA are used to evaluate scripts using the Lua interpreter built into Redis starting from version 2.6.0.
The first argument of EVAL is a Lua 5.1 script. The script does not need to define a Lua function (and should not). It is just a Lua program that will run in the context of the Redis server.
The second argument of EVAL is the number of arguments that follows the script (starting from the third argument) that represent Redis key names. The arguments can be accessed by Lua using the KEYS global variable in the form of a one-based array (so KEYS[1], KEYS[2], …).
All the additional arguments should not represent key names and can be accessed by Lua using the ARGV global variable, very similarly to what happens with keys (so ARGV[1], ARGV[2], …).
The following example should clarify what stated above:

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

EVAL的第一个参数是一个Lua 脚本
EVAL的第二个参数是表示Redis键名的参数个数Lua可以使用KEYS全局变量以基于一个数组的形式访问这些参数(KEYS[1]KEYS[2],…)。
所有的附加参数不应该表示键名,可以通过Lua使用ARGV全局变量访问,这与使用键的情况非常相似(ARGV[1]ARGV[2],…)。
例如

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2

"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是lua脚本
KEYS[1]、KEYS[2] 相当于key的占位符,代表键:k1、k2
ARGV[1]、ARGV[2]相当于value的占位符,代表参数 v1、v2

在这里插入图片描述

lua脚本中可以使用redis.call函数调用Redis命令。

redis.call(‘set’,‘test’,1)
redis.call(‘get’,‘test’)

返回值就是Redis命令的执行结果。
Redis还提供了redis.pcall()函数,功能和上面相同,唯一区别是当命令执行出错时redis.pcall会记录错误并继续执行,而redis.call会直接返回错误,不会继续执行。

> eval "return redis.call('set','foo','bar')" 0
OK

上面的脚本将键foo设置为字符串。然而,它违背了EVAL命令的语义,因为脚本使用的所有键都应该通过keys数组传递,正确的应该是:

> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK
> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 k3 v3
OK

在这里插入图片描述
在这里插入图片描述

虽然第一种写法不规范,但是也没有错。

三、RedisTemplate整合Lua脚本

构造脚本

构造脚本使用DefaultRedisScript类的构造方法。
在这里插入图片描述

执行脚本

使用execute这个方法来执行lua脚本。
在这里插入图片描述
常用的方法有以下几个:



public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
        return this.scriptExecutor.execute(script, keys, args);
    }

  /**
     * script:要执行lua脚本,封装成RedisScript对象
     * argsSerializer:参数序列化器
     * resultSerializer:结果序列化器
     * keys:Redis的键集合
     * args:脚本所需的参数
     */  
public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) {
        return this.scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args);
    }

第一种方式:脚本直接写在代码中,String类型

 @Test
    public void testLua() {
        //设置k1 v1 并设置过期时间   最后返回 值  
        String script = "redis.call('set',KEYS[1],ARGV[1])  return redis.call('get',KEYS[1])";
        // 两个参数  分别代表 lua脚本 和 返回值的类型
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
        /**
         * List设置lua的KEYS
         */
        List<String> keyList = new ArrayList();
        keyList.add("k1");
    
        /**
         * redisCacheTemplate.execute(redisScript, keyList, argv1)
         * redisScript 代表脚本
         * keyList代表key
         * argv1 代表第一个参数 ARGV[1]
         * 如果有多个参数 往后加
         */
        String argv1 = "v1";

        String execute = redisCacheTemplate.execute(redisScript, keyList, argv1);
        System.out.println(execute);
    }
    // 使用指定参数和返回结果序列化方式的execute方法
    //获取到字符串序列化器
        RedisSerializer<String> stringSerializer = redisCacheTemplate.getStringSerializer();
        String execute1 = redisCacheTemplate.execute(redisScript, stringSerializer, stringSerializer, keyList, argv1);
        System.out.println(execute1);

第二种方式:脚本单独写在lua文件中

在resources目录下创建setExpireAndGet.lua文件

redis.call('set',KEYS[1],ARGV[1])
redis.call('expire',KEYS[1],10)
return redis.call('get',KEYS[1])

在这里插入图片描述

 @Test
    public void testLua2() {
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(String.class);
        redisScript.setLocation(new ClassPathResource("setExpireAndGet.lua"));
        /**
         * List设置lua的KEYS
         */
        List<String> keyList = new ArrayList();
        keyList.add("k2");

        String argv1 = "v2";
        String execute = redisCacheTemplate.execute(redisScript, keyList, argv1);
        System.out.println(execute);
    }

四、 Redis分布式锁结合Lua脚本

获得锁的lua脚本:

if redis.call('set',KEYS[1],ARGV[1],'NX','EX',ARGV[2]) then
    return '1'
else
    return '0'
end	

参数含义:

  1. KEYS[1] 键
  2. ARGV[1] 值 (可以设置UUID)
  3. ARGV[2] 过期时间 EX:秒 PX:毫秒

释放锁的lua脚本:

只有当值匹配是才能执行删除锁的操作,解铃还须系铃人。

if redis.call('get',KEYS[1]) == ARGV[1] then
    return tostring(redis.call('del',KEYS[1]))
else
    return '0'
end
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liu_Shihao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值