赞
踩
Lua是一种轻量级、可嵌入的,自带原子性
脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。广泛应用于
游戏开发、Web开发和其他领域。``
数字、字符串、布尔值、表(数组和字典的集合)等
。这些数据类型使得Lua语言能够处理各种复杂的数据结构和算法。Lua脚本在Redis中的使用有许多优势,使其成为执行复杂操作的理想选择。以下是一些主要原因:
原生支持
Redis天生支持Lua脚本
,因此不需要额外的插件或扩展。性能
多次的客户端与服务器之间的通信
。这可以减少网络开销
,提高性能,特别是在需要执行多个Redis命令以完成一个操作时
。原子性:Redis保证Lua脚本的原子性执行,无需担心竞态条件或并发问题。事务
Lua脚本可以与Redis事务一起使用
,确保一系列命令的原子性执行
。这允许你将多个操作视为一个单一的事务,要么全部成功,要么全部失败
。复杂操作
允许你在一个脚本中组合多个Redis命令
。这对于处理复杂的业务逻辑非常有用,例计算和更新分布式计数器、实现自定义数据结构
等。实现复杂的原子锁
Lua脚本的执行是原子的,这意味着在Lua脚本执行期间,没有其他客户端可以插入其他操作
。这使得Lua脚本在实现诸如分布式锁、计数器、排行榜
等需要原子操作的情况下非常有用。
使用Lua脚本,你可以实现复杂的原子锁,而不仅仅是使用Redis的SETNX(set if not exists)
命令。这对于分布式锁的实现非常重要。
减少网络开销
大批量的数据处理
,Lua脚本可以减少客户端和服务器之间的往返次数
,从而显著减少网络开销。减少服务器负载
通过将复杂的计算移至服务器端,可以减轻客户端的负担
,降低服务器的负载。
可读性和维护性
Lua脚本在Redis中的优势在于它可以原子性地执行复杂操作、减少网络通信、提高性能、减轻服务器负载,以及提高代码的可读性
。这使得它成为执行一系列复杂操作的理想选择,尤其是在分布式系统
中需要高性能和可伸缩性的场景下。通过Lua脚本,Redis不仅成为一个键值存储
,还能执行复杂的数据操作。
在线运行lua脚本网站https://www.bejson.com/runcode/lua/
注释在Lua中用于添加说明和注解。单行注释以–开始,多行注释则使用–[[ … ]]。
-- 这是一条单行注释
--[[
这是一个多行注释
可以跨越多行
]]
2个连续
的短横线--
开始,直到该行结束方括号[[开始,并以2个方括号]]结束
变量在Lua中无需显式声明类型。使用local关键字
创建局部变量,全局变量直接声明。
local age = 30
name = "John" -- 全局变量
基本数据类型包括整数、浮点数、字符串、布尔值和nil。
{}存储键值对类型数据
,类似java的对象-- 声明不同类型的变量
local num = 42 -- 整数
local num2 = 3.14 -- 浮点数
local bool1 = true -- true
local bool2 = false -- false
local str1 = "Hello, World!" -- 双引号字符串
local str2 = 'Lua is great!' -- 单引号字符串
表是Lua的核心数据结构,用花括号{}
定义。
表可以包含键值对,键和值可以是任何数据类型。
local person = { name = "John", age = 30, hobbies = {"Reading", "Gaming"} }
print("姓名:" .. person.name)
print("年龄:" .. person.age)
local age = 1
if age < 18 then
print("未成年人")
elseif age >= 18 and age < 66 then
print("成年人")
else
print("老年人")
end
-- 未成年人
for循环
for i = 1, 5 do
print(i)
end
while循环
local count = 0
while count < 3 do
print("循环次数: " .. count)
count = count + 1
end
repeat…until循环
local count = 6
repeat
print("至少执行一次")
until count > 5
-- 至少执行一次
函数在Lua中使用function+end关键字
定义,可以接收参数并返回值
。
function functionName(arg1, arg2, ...)
-- 函数体
-- 这里是函数要执行的代码
-- 可以使用参数arg1, arg2, ...
return result -- (可选)返回结果
end
-- 定义函数
function add(a, b)
local sum = a + b
return sum
end
-- 调用add函数并打印结果
local result = add(3, 4)
print("The sum is: " .. result) -- 输出 "The sum is: 7"
-- 定义一个函数,它接受一个函数作为参数并调用它
function callFunction(func)
func() -- 调用传递进来的匿名函数
end
-- 创建一个匿名函数,并作为参数传递给callFunction
callFunction(function()
print("Hello from an anonymous function!")
end)
-- 输出:
-- Hello from an anonymous function!
local outerVariable = "I'm outside!"
-- 创建一个闭包
local function createClosure()
return function()
print(outerVariable) -- 访问外部变量
end
end
-- 获取闭包并调用它
local closure = createClosure()
closure() -- 输出:I'm outside!
Lua支持模块化编程,允许将相关功能封装在独立的模块中,并通过require关键字
加载它们。
首先,我们创建一个名为myModule.lua
的模块文件。在这个文件中,我们定义了一些函数和变量,并在文件末尾返回一个表,该表包含了模块提供的所有公开功能和数据。
-- myModule.lua
-- 私有变量
local privateVar = "This is a private variable"
-- 私有函数
local function privateFunction()
print("This is a private function")
end
-- 公开函数
function myModule.publicFunction()
print("This is a public function")
print("Private variable is: " .. privateVar) -- 可以访问私有变量
end
-- 返回公开部分
return myModule
在另一个Lua脚本中,我们可以使用require函数来加载并使用
myModule模块。
-- main.lua
-- 加载模块
local myModule = require("myModule")
-- 调用模块中的公开函数
myModule.publicFunction()
-- 尝试访问模块的私有部分(会失败)
-- print(myModule.privateVar) -- 这将引发错误,因为privateVar是私有的
-- myModule.privateFunction() -- 这也会引发错误,因为privateFunction是私有的
当使用require加载一个模块时,Lua会首先检查模块是否已经被加载过。如果是,则直接返回之前加载的模块,而不是重新加载。这有助于避免重复加载和初始化模块。
模块的路径可以是相对路径或绝对路径
。在上面的示例中,假设myModule.lua和main.lua
在同一目录下,所以只使用了模块名作为参数。如果模块在其他位置,你需要提供正确的路径
。
Lua的模块系统相对简单,没有像一些其他语言那样复杂的包管理系统
。但是,你可以使用像LuaRocks这样的第三方工具来管理和安装Lua包
。
Lua提供了丰富的字符串操作功能,包括字符串连接、查找、替换、模式匹配
字符串连接
Lua中的字符串连接操作非常简单,只需将两个字符串相邻放置即可。
local str1 = "Hello"
local str2 = "World"
local result = str1 .. str2 -- 连接字符串
print(result) -- 输出:HelloWorld
字符串长度
使用#操作符可以获取字符串的长度。
local str = "Hello"
print(#str) -- 输出:5
字符串查找
使用string.find
函数可以在字符串中查找子串。该函数返回两个值:子串开始的位置
和 结束的位置
(如果不存在则返回nil)。
local str = "Hello, World!"
local start, end_ = string.find(str, "World")
if start then
print("Found 'World' at position", start)
else
print("Not found")
end
字符串替换
使用string.gsub函数
可以全局替换字符串中的子串。第一个参数是源字符串
,第二个参数是要被替换的模式
,第三个参数是替换成的字符串
,第四个参数(可选)是一个计数器,表示替换的最大次数。
local str = "apple, apple, apple pie"
local new_str = string.gsub(str, "apple", "orange")
print(new_str) -- 输出:orange, orange, orange pie
-- 替换最多两次
local new_str = string.gsub(str, "apple", "orange", 2)
print(new_str) -- 输出:orange, orange, apple pie
字符串切分
string.find和string.sub
组合来实现。或者,你可以使用第三方库,如lua-string库中的split函数
。function split(str, delim)
local result = {}
local from = 1
local delim_from, delim_to = string.find(str, delim, from)
while delim_from do
table.insert(result, string.sub(str, from, delim_from - 1))
from = delim_to + 1
delim_from, delim_to = string.find(str, delim, from)
end
table.insert(result, string.sub(str, from))
return result
end
local str = "apple,banana,cherry"
local fruits = split(str, ",")
for _, fruit in ipairs(fruits) do
print(fruit)
end
-- 执行结果:
-- apple
-- banana
-- cherry
字符串格式化
string.format函数
来模拟这个功能。local name = "Alice"
local age = 30
local greeting = string.format("Hello, my name is %s and I'm %d years old.", name, age)
print(greeting) -- 输出:Hello, my name is Alice and I'm 30 years old.
字符串模式匹配
Lua提供了强大的模式匹配功能,通过string.match、string.gmatch和string.find
等函数可以实现复杂的字符串处理。Lua的模式匹配基于一种类似于Perl的正则表达式语法。
local str = "apple 123 orange 456"
for number in string.gmatch(str, "%d+") do
print(number) -- 输出:123 和 456
end
在Lua中,错误处理通常涉及到使用pcall(protected call)或xpcall(extended protected call)函数
来捕获和处理可能出现的错误。当在Lua代码中遇到错误时,它会抛出一个错误消息并终止当前的执行
。通过使用pcall或xpcall,你可以捕获这些错误并继续执行后续的代码。
pcall函数接收一个函数
和一个可选的参数列表
,并尝试以“保护”模式
调用该函数。
l返回true以及函数的返回值
返回false以及错误消息
。function riskyFunction()
error("Something went wrong!")
end
local status, result = pcall(riskyFunction)
if not status then
print("An error occurred: " .. result)
else
print("Function call was successful: " .. result)
end
-- An error occurred: script.lua:2: Something went wrong!
xpcall与pcall类似,但它允许你提供一个错误处理函数
,该函数将在发生错误时被调用
。这使得你可以进行更复杂的错误处理。
function myErrorHandler(err)
print("Caught an error: " .. err)
-- 这里可以进行更复杂的错误处理
return debug.traceback(err, 2) -- 返回错误跟踪信息
end
function riskyFunction()
error("Something went wrong!")
end
local status, traceback = xpcall(riskyFunction, myErrorHandler)
if not status then
print("An error occurred:")
print(traceback)
end
执行结果
Caught an error: script.lua:8: Something went wrong!
An error occurred:
script.lua:8: Something went wrong!
stack traceback:
[C]: in function 'error'
script.lua:8: in function 'riskyFunction'
[C]: in function 'xpcall'
script.lua:11: in main chunk
[C]: in ?
在Lua中,你可以使用error函数来抛出一个自定义的错误
。error函数接受一个字符串作为错误消息,并可以选择性地提供一个错误级别。
function checkNumber(n)
if type(n) ~= "number" then
error("Not a number!", 2) -- 抛出一个错误
end
print("The number is: " .. n)
end
checkNumber("hello") -- 这将抛出一个错误
执行结果
/usr/local/lua-5.3.5/lua53: script.lua:8: Not a number!
stack traceback:
[C]: in function 'error'
script.lua:3: in function 'checkNumber'
script.lua:8: in main chunk
[C]: in ?
-- 创建协程
local cdata = coroutine.create(function () print("Hello from coroutine!") end)
-- 定义函数
local function say_hello(name) print("Hello, " .. name) end
-- 调用函数,输出 "Hello, Alice"
say_hello("Alice")
-- 创建线程
local thread = coroutine.create(function () print("Hello from thread!") end)
-- 恢复线程,输出 "Hello from thread!"
coroutine.resume(thread)
-- Hello, Alice
-- Hello from thread!
string库:
table库:
提供了一系列用于操作Lua表的函数,如表的插入、删除、排序、遍历等。
例如:table.insert(), table.remove(), table.sort(), table.concat() 等。
math库:
io库:
os库:
debug库:
package库:
coroutine库:
怎么使用
使用标准库不需要使用关键字进行显式引入(import)或加载(load
)。
预编译并内置在Lua解释器中的
,因此在你的Lua脚本中可以直接调用这些库中的函数,而无需任何额外的步骤
。你只需要直接调用标准库中的函数即可,例如:
场景:在缓存中存储某些数据,但需要定期或基于条件更新
这些数据,同时确保在更新期间
不会发生并发问题
。
示例:使用Lua脚本,你可以原子性地检查数据的新鲜度
,如果需要更新,可以在一个原子性操作中重新计算数据并更新缓存
。
-- 获取键名,即从传入脚本的参数列表KEYS中获取第1个参数
local key = KEYS[1] -- 获取缓存键
-- 根据key从缓存中获取数据
local value = redis.call('GET', key ) -- 尝试从缓存获取数据
-- 如果数据不存在(即data为nil)
if not value then
-- 数据不在缓存中,重新计算并设置
--调用一个名为 calculateData 的函数(这个函数在脚本中没有定义,但应该是一个用于重新计算数据的函数)
value = calculateData()
-- 将新计算的数据设置到缓存中
redis.call('SET', key , value )
end
-- 返回数据(无论是从缓存中获取的还是新计算的)
return value
Lua脚本的执行是原子的,这意味着在Lua脚本执行期间,没有其他客户端可以插入其他操作
。这使得Lua脚本在实现诸如分布式锁、计数器、排行榜
等需要原子操作的情况下非常有用。
场景:需要执行多个Redis命令
作为一个原子操作,确保它们在多线程或多进程环境
下不会被中断。
示例:使用Lua脚本,你可以将多个命令组合成一个原子操作,如实现分布式锁、计数器、排行榜
等。
-- 获取键名,即从传入脚本的参数列表KEYS中获取第1个参数
local key = KEYS[1]
-- 获取参数值,这是你想要设置的新值 ,即从传入脚本的参数列表ARGV 中获取第1个参数
local value = ARGV[1]
-- 获取当前键对应的值
local current = redis.call('GET', key)
-- 如果当前值不存在(即current为nil),或者当前值(转换为数字后)小于给定的参数值(也转换为数字后)
if not current or tonumber(current) < tonumber(value) then
-- 如果当前值不存在或新值更大,设置新值
redis.call('SET', key, value)
end
场景:需要对Redis中的数据进行复杂的处理,如统计、筛选、聚合等。
示例:使用Lua脚本,你可以在Redis中执行复杂的数据处理
,而不必将数据传输到客户端进行处理,减少网络开销。
-- 从传入脚本的参数列表中获取第1个参数
local keyPattern = ARGV[1]
-- 调用Redis的KEYS命令,根据给定的模式keyPattern获取所有匹配的键。
-- 需要注意的是,KEYS命令在生产环境中应谨慎使用,因为它可能会阻塞Redis服务器,尤其是在有大量键存在时。
local keys = redis.call('KEYS', keyPattern)
-- 初始化一个空的Lua表result,用于存储处理后的数据
local result = {}
--遍历keys表中的每一个键
for i, key in ipairs(keys) do
-- 获取每个键对应的值
local data = redis.call('GET', key)
-- 调用一个假设存在的processData函数处理这个值(注意:在脚本中并没有给出processData函数的定义,你需要根据实际需求来实现它)。
-- 然后处理后数据并添加到result表中
table.insert(result, processData(data))
end
--返回处理后的结果表。这个表包含了所有匹配键对应的处理后的数据。
return result
场景:实现分布式系统中的锁机制,确保只有一个客户端
可以执行关键操作。
示例:使用Lua脚本,你可以原子性地尝试获取锁
,避免竞态条件
,然后在完成后释放锁
。
-- 获取锁的键名
local lockKey = KEYS[1]
-- 获取锁的值,通常是一个唯一标识符(比如 UUID),用于在释放锁时验证锁的持有者。
local lockValue = ARGV[1]
-- 获取锁的超时时间,单位是毫秒,如果在这个时间内锁没有被释放(即 lockKey 没有被删除),则 Redis 会自动删除这个键,避免死锁。
local lockTimeout = ARGV[2]
-- 尝试使用 SET 命令来设置锁,NX 表示只有在 key 不存在时设置值,PX 表示 key 的过期时间(毫秒)
-- lockKey 不存在(NX),则设置其值为 lockValue,并设置其过期时间为 lockTimeout(PX)。如果设置成功,说明客户端成功获取到了锁。
-- 如果 SET 命令执行成功,说明获取到了锁
if redis.call('SET', lockKey, lockValue, 'NX', 'PX', lockTimeout) then
-- 锁获取成功,执行关键操作(这部分逻辑在脚本中没有给出)
-- ...
-- 执行完关键操作后,删除锁键,释放锁
redis.call('DEL', lockKey) -- 释放锁
-- 返回 true,表示成功获取并释放了锁
return true
else
-- 锁获取失败,可能是因为有其他客户端已经持有了该锁
return false
通常用于在分布式系统中确保多个客户端在并发访问共享资源时能够正确地获取和释放锁
,从而避免数据不一致的问题。
原生支持
Redis天生支持Lua脚本
,因此不需要额外的插件或扩展。重点介绍通过Redis内嵌提供的调用函数,我们可以使用lua去操作redis
,又能保证他的原子性
,这里不需要大家过于精通,只需要知道他有什么作用即可。Redis的eval命令
来调用lua脚本
然后使用Lua的内置的redis模块调用redis的指令,语法如下:
redis.call('命令名称', 'key', '其它参数', ...)
执行 redis.call(‘set’, ‘name’, ‘ouyang’) 这个脚本,语法如下:
192.168.0.225:0> eval "return redis.call('set', 'name', 'ouyang')" 0
-- return redis.call('set', 'name', 'ouyang') : 脚本内容
-- 0:表示key的参数个数
如果脚本中的key、value
不想写死,可以作为参数传递
。key类型参数会放入KEYS数组
,其它参数会放入ARGV数组
,在脚本中可以从KEYS和ARGV数组获取这些参数:
192.168.0.225:0>eval "return redis.call('set', KEYS[1],ARGV[1])" 1 name 'ouyang1'
-- KEYS[1] 表示获取第一个key
-- ARGV[1] 表示获取第一个值
-- 1 表示需要一个key
执行结果
如果想执行复杂的lua命令可以这样写
lua命令如下:
# 先执行 set name jack
redis.call('set', 'name', 'ouyangjianpeng')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
转写成eval命令
eval "redis.call('set', 'name', 'ouyangjianpeng') local name = redis.call('get', 'name') return name" 0
执行结果
在Spring Boot中实现Lua脚本的执行主要涉及Spring Data Redis
和Lettuce(或Jedis)客户端
的使用。
添加Spring Data Redis和Lettuce(或Jedis)
的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce.core</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
在application.properties或application.yml中配置Redis连接属性,包括主机、端口、密码等。
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=yourPassword
创建一个Lua脚本,以执行你需要的操作。将脚本保存在Spring Boot项目
的合适位置。
假设有一个Lua脚本文件myscript.lua
,它实现了一个简单的计算:
local a = tonumber(ARGV[1])
local b = tonumber(ARGV[2])
return a + b
StringRedisTemplate
或LettuceConnectionFactory
@Service
public class LuaScriptService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 运行Lua脚本字符串
public Integer executeLuaScriptFromString() {
// Lua脚本字符串,该脚本接收两个参数(ARGV[1] 和 ARGV[2]),将它们转换为数字并相加
String luaScript = "local a = tonumber(ARGV[1])\nlocal b = tonumber(ARGV[2])\nreturn a + b";
// 创建一个Redis脚本对象,指定Lua脚本和期望的返回类型(Integer)
RedisScript<Integer> script = new DefaultRedisScript<>(luaScript, Integer.class);
// 创建一个空的keys数组,因为在这个Lua脚本中,我们不使用KEYS参数
String[] keys = new String[0]; // 通常情况下,没有KEYS部分
// 创建一个args数组,包含两个参数,这些参数将传递给Lua脚本
Object[] args = new Object[]{10, 20}; // 传递给Lua脚本的参数
// 使用stringRedisTemplate(它应该是已经配置好的Spring Data Redis的StringRedisTemplate实例)
// 执行Lua脚本,并传入keys和args数组
Integer result = stringRedisTemplate.execute(script, keys, args);
// 返回执行Lua脚本后得到的结果(两个数字的和)
return result;
}
}
将Lua脚本保存到文件,例如myscript.lua
。然后创建一个Java类来·加载和运行该脚本文件·:
@Service
public class LuaScriptService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ResourceLoader resourceLoader;
// 从文件中执行Lua脚本的方法
public Integer executeLuaScriptFromFile() {
// 加载位于类路径下的myscript.lua资源
Resource resource = resourceLoader.getResource("classpath:myscript.lua");
String luaScript;
try {
// 尝试读取资源文件内容,并将其转换为字符串
luaScript = new String(resource.getInputStream().readAllBytes());
} catch (Exception e) {
// 如果无法读取Lua脚本文件,则抛出运行时异常
throw new RuntimeException("无法读取Lua脚本文件。");
}
// 创建一个Redis脚本对象,指定Lua脚本和期望的返回类型(Integer)
RedisScript<Integer> script = new DefaultRedisScript<>(luaScript, Integer.class);
// 创建一个空的keys数组,因为在这个Lua脚本中,我们不使用KEYS参数
String[] keys = new String[0]; // 通常情况下,没有KEYS部分
// 创建一个args数组,包含两个参数,这些参数将传递给Lua脚本
Object[] args = new Object[]{10, 20}; // 传递给Lua脚本的参数
// 使用stringRedisTemplate(它应该是已经配置好的Spring Data Redis的StringRedisTemplate实例)
// 执行Lua脚本,并传入keys和args数组
Integer result = stringRedisTemplate.execute(script, keys, args);
// 返回执行Lua脚本后得到的结果(两个数字的和)
return result;
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。