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

文章目录

  • 上一章
  • 一、Lua介绍
    • 1. 应用场景
    • 2. Linux安装Lua
    • 3. 简单了解Lua
      • Lua关键字
      • 定义变量
      • Lua中的数据类型
      • 流程控制
      • 函数
      • require 函数
  • 二、Redis整合Lua脚本
    • 原子性
    • EVAL方法
  • 三、RedisTemplateLua整合Lua脚本
    • 构造脚本
    • 执行脚本
    • 第一种方式:脚本直接写在代码中,String类型
    • 第二种方式:脚本单独写在lua文件中
  • 四、 Redis分布式锁结合Lua脚本
    • 获得锁的lua脚本:
    • 释放锁的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

深入学习Redis_(四)Redis与Lua脚本_第1张图片
单行注释:两个减号是单行注释:

--单行注释

多行注释:

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

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 表示执行的独立线路,用于执行协同程序
table Lua 中的表(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_(四)Redis与Lua脚本_第2张图片

二、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

深入学习Redis_(四)Redis与Lua脚本_第3张图片

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

深入学习Redis_(四)Redis与Lua脚本_第4张图片
在这里插入图片描述

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

三、RedisTemplateLua整合Lua脚本

构造脚本

构造脚本使用DefaultRedisScript类的构造方法。
深入学习Redis_(四)Redis与Lua脚本_第5张图片

执行脚本

使用execute这个方法来执行lua脚本。
深入学习Redis_(四)Redis与Lua脚本_第6张图片
常用的方法有以下几个:



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])

深入学习Redis_(四)Redis与Lua脚本_第7张图片

 @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

你可能感兴趣的:(Redis,面试,redis,lua)