Java玩转Redis+Lua脚本:一篇让你从小白到高手的实战入门指南

可能很多人都多多少少听过这个lua脚本,但是他是干什么的?Lua 是一种轻量级、高效、可嵌入的脚本语言,设计用于扩展应用程序的功能。Lua 的核心非常小巧(仅几百KB),适合嵌入其他程序。无需编译,直接运行脚本并且跨平台。

但是作为Java开发的我们来说,最常用的莫过于将它和Redis结合起来一起为业务保驾护航,Redis 支持通过 Lua 脚本执行原子性操作 (如复杂的事务逻辑),Java 可以通过 Redis 的 EVAL 命令调用这些脚本。最经典的用法是通过 Lua 脚本实现原子性操作 ,解决 Redis 的复杂事务需求,因为Lua脚本在Redis中是原子执行的,不会被其他命令打断 要么全部执行,要么全不执行。而这篇文章会带大家一起从Java开发的角度入门一下Lua脚本,并且学会怎么能用它作为生产力来帮助我们。

环境安装

window 下我们可以使用一个叫 "SciTE" 的 IDE环 境来执行 lua 程序,下载地址为:

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

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 解释器,可以通过 EVALEVALSHA 命令执行 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 脚本?

  1. 省事儿:本来要发好几条 Redis 命令的事儿,现在一条脚本搞定,网络开销直接砍半。
  2. 稳如老狗:不用怕高并发把库存扣成负数,脚本里的操作都是原子性的,绝对靠谱。
  3. 灵活得飞起:想加判断加判断,想搞循环搞循环,Redis 直接变身编程小助手。

最后哔哔两句,其实技术没啥高大上的,能解决问题的就是好技术。Lua + Redis 这组合,说白了就是让你代码更简单、更稳当。下次遇到需要 "原子性" 或者 "复杂操作" 的时候,别犹豫,直接上脚本,真香就完事了!

至于仓库地址我给大家贴出来,大家可以自行去查看:

王富贵/SpringBoot+Redis

最后,别忘了给富贵同学一键三连意思意思一下

相关推荐
梦幻通灵18 分钟前
IDEA查看源码利器XCodeMap插件
java·intellij-idea
小满和小晨19 分钟前
Redis+Lua的分布式限流器
redis·分布式·lua
Ashlee_code29 分钟前
南太平洋金融基建革命:斐济-巴新交易所联盟的技术破局之路 ——从关税动荡到离岸红利,跨境科技如何重塑太平洋资本生态
java·开发语言·科技·金融·重构·web3·php
隐-梵30 分钟前
2025年测绘程序设计比赛--基于统计滤波的点云去噪(已获国特)
java·开发语言·windows·c#·.net
阿萨德528号1 小时前
5、生产Redis高并发分布式锁实战
数据库·redis·分布式·缓存
叉烧钵钵鸡1 小时前
Java ++i 与 i++ 底层原理
java·开发语言·后端
hqxstudying1 小时前
SpringAI的使用
java·开发语言·人工智能·springai
狐小粟同学1 小时前
JAVAEE--4.多线程案例
java·开发语言
IT小辉同学2 小时前
CentOS 7 编译 Redis 6.x 完整教程(解决 GCC 版本不支持 C11)
linux·redis·centos
the beard2 小时前
RabbitMQ:基于SpringAMQP声明队列与交换机并配置消息转换器(三)
java·开发语言·rabbitmq·intellij idea