可能很多人都多多少少听过这个lua脚本,但是他是干什么的?Lua 是一种轻量级、高效、可嵌入的脚本语言,设计用于扩展应用程序的功能。Lua 的核心非常小巧(仅几百KB),适合嵌入其他程序。无需编译,直接运行脚本并且跨平台。
但是作为Java开发的我们来说,最常用的莫过于将它和Redis结合起来一起为业务保驾护航,Redis 支持通过 Lua 脚本执行原子性操作 (如复杂的事务逻辑),Java 可以通过 Redis 的 EVAL
命令调用这些脚本。最经典的用法是通过 Lua 脚本实现原子性操作 ,解决 Redis 的复杂事务需求,因为Lua脚本在Redis中是原子执行的,不会被其他命令打断 要么全部执行,要么全不执行。而这篇文章会带大家一起从Java开发的角度入门一下Lua脚本,并且学会怎么能用它作为生产力来帮助我们。
环境安装
window 下我们可以使用一个叫 "SciTE" 的 IDE环 境来执行 lua 程序,下载地址为:
- Github 下载地址:github.com/rjpcomputin...

下载安装之后你就多了这么两个东西:

Lua是lua的命令行,而SciTE是Lua脚本的IDE编辑器。新建一个文件,文件后缀为 .lua
即可点击蓝色三角形运行lua脚本:

认识Lua
变量与打印
在 Lua 中,变量无需声明类型 ,直接使用 local
定义局部变量(推荐),或省略 local
使用全局变量。字符串拼接使用 ..
运算符。
lua
-- 1. 变量与打印
local name = "王富贵" -- local 表示局部变量
local age = 25
age = 25 -- 全局变量(不推荐)
print("Hello, " .. name .. "! You are " .. age .. " years old.") -- .. 是字符串连接符
local
限制变量作用域,避免污染全局环境。- 字符串连接用
..
,数字会自动转字符串 (如age
)。

条件判断
Lua 的 if-then-else
结构使用 ==
、<
、>
等比较运算符,不支持单 =
判断 。elseif
连写,不能写成 else if
。
lua
-- 2. 条件判断
local age = 25
if age == 18 then
print("刚满十八岁")
elseif age < 18 then
print("回家写作业")
else
print("出来上班")
end

循环(for 和 while)
Lua 支持 for
数值循环和 while
条件循环
lua
-- for 循环(1到5,步长1)
for i = 1, 5 do
print("现在是:", i)
end
-- while 循环
local j = 1
while j <= 3 do
print("我嘎嘎循环:", j)
j = j + 1 -- 手动递增
end

数据结构
定义函数
Lua 使用 function
关键字定义函数,支持多返回值:
lua
-- 计算矩形面积和周长
function rectangle(a, b)
local area = a * b
local perimeter = 2 * (a + b)
return area, perimeter -- 返回多个值
end
-- 调用函数
local area, perimeter = rectangle(3, 4)
print("面积:", area) -- 输出: 面积: 12
print("周长:", perimeter) -- 输出: 周长: 14

表(Table)
表是 Lua 唯一的复合数据类型,可作数组、字典或对象:
lua
-- 创建表(混合数组和键值对)
local person = {
name = "王富贵",
age = 18,
hobbies = {"游泳", "编程"}, -- 数组部分
score = {math=90, english=85} -- 字典部分
}
-- 访问元素
print(person.name) -- 输出: 王富贵
print(person.hobbies[1]) -- 输出: 游泳(数组下标从1开始)
print(person.score["english"]) -- 输出: 85

表遍历
pairs
:遍历所有键值对(顺序不确定)ipairs
:按顺序遍历数组部分(遇到nil停止)
lua
-- 创建表(混合数组和键值对)
local person = {
name = "王富贵",
age = 18,
hobbies = {"游泳", "编程"}, -- 数组部分
score = {math=90, english=85} -- 字典部分
}
-- 遍历键值对
for k, v in pairs(person) do
if type(v) == "table" then
print(k..": [table]")
else
print(k..": "..tostring(v))
end
end
-- 遍历数组部分(hobbies)
for i, hobby in ipairs(person.hobbies) do
print("爱好"..i..": "..hobby)
end

函数与表的结合
lua
-- 为表添加方法
local car = {
speed = 10,
accelerate = function(self, delta)
self.speed = self.speed + delta
end
}
car:accelerate(20) -- 冒号调用自动传递self
print("车速:", car.speed) -- 输出: 车速: 30

上面就是我们的Lua脚本入门了,怎么样是不是挺简单的?其实还有很多复杂的操作,但是作为我们以及学习后面的知识来说完全是够了,我们没有必要深究。那么我们仅仅认识它还不够,我们再来看一下如何用Lua脚本结合Redis呢?
Redis操作Lua
如果你的电脑没有安装redis,可以参考博主的文章进行安装:Linux安装Redis(图文解说详细版)
Redis 内置了 Lua 解释器,可以通过 EVAL
或 EVALSHA
命令执行 Lua 脚本,实现原子性操作 、复杂计算 和减少网络开销。
lua
-- 语法:EVAL "脚本内容" key数量 [key1 key2...] [arg1 arg2...]
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
例如我们使用lua脚本设置一个key的值为 王富贵
,我们就可以这样写
lua
-- 语法:EVAL "脚本内容" key数量 [key1] [value1]
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 username 王富贵

KEYS[1]
:存储的键名(username
)。ARGV[1]
:要设置的值(王富贵
)。redis.call('SET', ...)
:调用 Redis 的SET
命令。
那么我们在看看如何查看这个key的值:
lua
-- 语法:EVAL "脚本内容" key数量 [key1]
EVAL "return redis.call('GET', KEYS[1])" 1 username

KEYS[1]
:要查询的 Key(如username
)。redis.call('GET', ...)
:调用 Redis 的GET
命令。

- 如果
username
存在,返回"王富贵"
。 - 如果 Key 不存在,返回
null
。
那么这就有意思了,我们知道了如何在redis中执行lua脚本,那么我们可以写一个稍微比上面的案例复杂一点的demo试一下:
用 Lua 脚本让 Redis 读取一个 key 的值并加 1(如果没有 key,就初始化为 1)。
lua
local key = KEYS[1] -- 获取传入的 key(如 "counter")
local current = redis.call("GET", key) -- 尝试获取当前值
if current == false then -- 如果 key 不存在
redis.call("SET", key, 1) -- 初始化成 1
return 1 -- 返回 1
else -- 如果 key 存在
redis.call("INCR", key) -- 值 +1
return tonumber(current) + 1 -- 返回新值
end
我们可以使用EVAL
命令执行一下试试(注意这里要记得压缩为一行执行):
lua
EVAL "local key=KEYS[1] local current=redis.call('GET',key) if current==false then redis.call('SET',key,1) return 1 else redis.call('INCR',key) return tonumber(current)+1 end" 1 wangfuguiAge
1
表示KEYS
的数量(这里只有wangfuguiAge
一个 key)。wangfuguiAge
是要操作的 Redis key。

可以看到随着我们每次执行 wangfuguiAge
的值就加了一个1,这样我们就可以通过Lua结合Redis实现一个简单的累加器了。
Java操作Lua
我们学会了使用redis调用lua脚本,那么我们可以自己试一些复杂的脚本,大家可以动手试一下,那么我们前面也说过,我们作为java开发如何使用Lua呢,这里我们就可以来演示一下如何使用Java调用Lua脚本。
作者之前写过一篇springboot版本的redis教学,我们就在这个基础上面加几个方法来完成我们这次的操作:SpringBoot集成Redis
首先我们在RedisUtil类
要加一个方法:
java
/**
* 调用lua脚本
*/
public Long executeLua(String luaScript, List<String> keys, Object... values) {
// 指定 lua 脚本,并且指定返回值类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
// 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
return stringRedisTemplate.execute(redisScript, keys, values);
}
这个是执行lua
脚本的核心方法,其他的脚本都需要经过这个方法,之后我们再把上面我们案例中的lua脚本压缩为一行:
java
private static final String INCREMENT_LUA_SCRIPT = "local key=KEYS[1] local current=redis.call('GET',key) if current==false then redis.call('SET',key,1) return 1 else redis.call('INCR',key) return tonumber(current)+1 end";
然后我们再封装一个方法将脚本传给executeLua方法:
java
/**
* 调用executeLua方法实现计数器
* local key = KEYS[1] -- 获取传入的 key(如 "counter")
* local current = redis.call("GET", key) -- 尝试获取当前值
* if current == false then -- 如果 key 不存在
* redis.call("SET", key, 1) -- 初始化成 1
* return 1 -- 返回 1
* else -- 如果 key 存在
* redis.call("INCR", key) -- 值 +1
* return tonumber(current) + 1 -- 返回新值
* end
*/
public Long increment(String key) {
return executeLua(INCREMENT_LUA_SCRIPT, Arrays.asList(key), "");
}
注意这里的 values
值如果你没有value的话就要写成空字符串,否则会报错
现在我们的RedisUtil里面已经有了这个两个方法,我们就可以从controller层暴露出去:
java
package com.wangfugui.apprentice.controller;
import com.wangfugui.apprentice.common.util.RedisUtils;
import com.wangfugui.apprentice.common.util.ResponseUtils;
import com.wangfugui.apprentice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author MaSiyi
* @version 1.0.0 2021/10/23
* @since JDK 1.8.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private RedisUtils redisUtils;
@GetMapping("/listUser")
public ResponseUtils listUser() {
return userService.listUser();
}
@GetMapping("/test-increment")
public ResponseEntity<Long> testIncrement(@RequestParam String key) {
Long count = redisUtils.increment(key);
return ResponseEntity.ok(count);
}
}
这样,我们的一个demo就写好了,此时我们只需要调用这个接口方法即可拿到我们累加之后的值了:


每调用一次值就加一,那么不出意料的我们去redis客户端看到的 ssss
的值也是 17

那么如果是其他的lua脚本内容呢,熟悉并发的估计对超卖
这个词不陌生了吧,我们来弄一个防止超卖
的案例:
lua
local product_id = KEYS[1] -- 商品ID(如 "1001")
local quantity = tonumber(ARGV[1]) -- 购买数量(转为数字)
local stock_key = "stock:" .. product_id -- 拼接库存Key(如 "stock:1001")
local current_stock = redis.call("GET", stock_key) -- 查询当前库存
if current_stock == false then
return -1 -- 商品不存在
end
current_stock = tonumber(current_stock) -- 转为数字
if current_stock >= quantity then
-- 库存充足,原子性扣减
redis.call("DECRBY", stock_key, quantity)
return current_stock - quantity -- 返回剩余库存
else
-- 库存不足
return -2 -- 自定义错误码
end
那么这段lua脚本的流程图就像现在这样:

那么在开始之前呢,我们给1001
这个商品的库存设置为 100

接下里我们开始写我们的java方法:
java
private static final String OVERSOLD_SCRIPT = "local product_id = KEYS[1] " +
"local quantity = tonumber(ARGV[1]) " +
"local stock_key = 'stock:' .. product_id " +
"local current_stock = redis.call('GET', stock_key) " +
"if current_stock == false then return -1 end " +
"current_stock = tonumber(current_stock) " +
"if current_stock >= quantity then " +
" redis.call('DECRBY', stock_key, quantity) " +
" return current_stock - quantity " +
"else return -2 end";
/**
* 调用executeLua方法实现库存扣减
*
* @param productId 商品ID
* @param quantity 购买的商品数量
*
* @return 返回的错误码或者现在的库存
*/
public Long oversold(String productId, Long quantity) {
return executeLua(OVERSOLD_SCRIPT, Arrays.asList(productId), quantity.toString());
}
controller方法:
java
@GetMapping("/test-oversold")
public ResponseEntity<Long> testOversold(@RequestParam String key, @RequestParam Long quantity) {
Long count = redisUtils.oversold(key, quantity);
return ResponseEntity.ok(count);
}
然后我们就可以调用接口方法测试一下:


买了两次,每次2件商品,就会发现接口返回给我们的就是剩余的库存数量,那么如果我们开始没有设置该商品的库存(即stock:1001
无值)接口则会返回-1,如果我们一直买,那么没库存的时候就会返回 -2
ok啊大家,上面呢就是我们这篇文章所有的内容了,聊了这么多,咱也算是把 Lua + Redis 这套组合拳入门了。从最开始的 "这啥玩意儿?" 到现在的 "真香!",总结起来就一句话:这玩意儿能让你少掉好多头发!
为啥非要用 Lua 脚本?
- 省事儿:本来要发好几条 Redis 命令的事儿,现在一条脚本搞定,网络开销直接砍半。
- 稳如老狗:不用怕高并发把库存扣成负数,脚本里的操作都是原子性的,绝对靠谱。
- 灵活得飞起:想加判断加判断,想搞循环搞循环,Redis 直接变身编程小助手。
最后哔哔两句,其实技术没啥高大上的,能解决问题的就是好技术。Lua + Redis 这组合,说白了就是让你代码更简单、更稳当。下次遇到需要 "原子性" 或者 "复杂操作" 的时候,别犹豫,直接上脚本,真香就完事了!
至于仓库地址我给大家贴出来,大家可以自行去查看:
最后,别忘了给富贵同学一键三连意思意思一下
