首页 > 数据库    Redis日期:2026-06-12 / 浏览

一、为什么是 Lua?

Redis 从 2.6.0 版本开始内置了 Lua 解释器。这意味着:

  • 零依赖:不需要像 Java 加 Maven 依赖、Python pip 安装库那样做任何前置准备。
  • 开箱即用:只要 Redis 版本 ≥ 2.6,直接用 EVAL 命令就能跑。

Lua 在 Redis 里的角色是胶水语言,用它把多个 Redis 命令和业务判断逻辑打包成一个整体,交给 Redis 服务端一次性执行。

二、核心语法速查(Java 开发者视角)

1. 大小写敏感

Lua 严格区分大小写return 不能写成 ReturnKEYS 不能写成 keys。这和 Java 一致。

2. 变量与local

local count = 10          -- 局部变量,强烈推荐
global_var = "hello"      -- 全局变量,危险,会污染环境

铁律:所有变量都用 local 声明。 这既是为了作用域隔离,也是为了性能。

3. 常用类型

Lua 是动态类型语言,变量没类型,值有类型。

类型 写法 备注
数字 local n = 10  
字符串 local s = "hello" 拼接用 ..
布尔 local ok = true 只有 false 和 nil 为假,0 和空字符串都为真!
local x = nil  
数组 local arr = {"a", "b"} 索引从 1 开始
字典 local map = {name="Tom"} 访问:map.name 或 map["name"]

4. 逻辑运算符

和 Java 的对照:

Java Lua
!a not a
a && b a and b
a || b a or b

Lua 的 and/or 是短路求值,返回值本身而不是强制转 boolean。常用于设默认值:

local count = redis.call('GET', KEYS[1]) or 0

5. 常用内置函数

函数 作用 示例
tonumber(str) 字符串转数字 tonumber("123") → 123
tostring(val) 转字符串 tostring(100) → "100"
type(val) 判断类型 type(nil) → "nil"
string.sub(s, i, j) 截取子串 string.sub("hello", 1, 3) → "hel"
string.len(s) 字符串长度 string.len("hi") → 2
math.abs(n) 绝对值 math.abs(-5) → 5
math.floor(n) 向下取整 math.floor(3.9) → 3
math.ceil(n) 向上取整 math.ceil(3.1) → 4
math.max(a,b) 最大值 math.max(1,5) → 5

重要:ARGV 里的参数全部是字符串,需要用 tonumber() 转成数字才能做算术运算。

三、EVAL 命令详解

命令格式

EVAL <脚本内容> <key的数量> <key列表...> <arg列表...>

中间那个数字是干什么的?

它是 KEYS 和 ARGV 的分割标识,告诉 Redis “前面 N 个参数是 KEY,剩下的全是 ARGV”。

示例:

EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
  • 2 表示前 2 个是 KEY:key1, key2
  • 剩下的全是 ARGV:arg1, arg2

为什么必须写这个数字?

  1. 集群路由:Redis Cluster 需要知道哪些是 key,才能校验它们是否在同一个 slot,并正确路由。
  2. 代码可读性:显式声明让脚本意图清晰,不用猜。
  3. 安全校验:Redis 可在执行前做静态分析。

即使脚本完全不用 KEYS,也必须写 0:

EVAL "return 'hello'" 0

脚本里只有 KEYS 和 ARGV 两个预置数组吗?

是的。 Redis 的 Lua 沙箱里,除了 KEYS 和 ARGV,没有任何其他外部数据来源。你需要的一切数据,都得在脚本里通过 redis.call 获取。

四、Redis 与 Lua 的联动机制

执行流程(四个步骤)

  1. 解析参数:Redis 根据 numkeys 把参数切分成 KEYS 和 ARGV。
  2. 注入沙箱:启动 Lua 解释器,创建隔离环境,注入 KEYS 和 ARGV。
  3. Lua 执行,回调 Redis:脚本通过 redis.call 或 redis.pcall 向宿主 Redis 发请求。脚本暂停→Redis 执行命令→返回结果给脚本→脚本继续。
  4. 返回结果:Lua 脚本执行完,结果转成 Redis 协议返回给客户端。

redis.callvsredis.pcall

  • redis.call('GET', KEYS[1]):执行 Redis 命令,出错会中断脚本并抛错。
  • redis.pcall('GET', KEYS[1]):执行 Redis 命令,出错不中断,以 table 形式返回错误信息。

用 pcall 可以让你在脚本内部做错误处理:

local res = redis.pcall('GET', KEYS[1])
if type(res) == 'table' and res['err'] then
    return "出错了"
end

整个脚本执行期间,其他命令必须排队

这是真的,而且是所有命令都排队。 原因是 Redis 的单线程模型:主线程在跑 Lua 时,不可能同时处理其他请求。

后果:脚本必须快(毫秒级),否则整个 Redis 会被阻塞,导致服务雪崩。

边界:这只阻塞这一个 Redis 实例的主线程,不影响你的 Java 应用、MySQL、Nginx 等其他进程。

五、原子性:最大的优势与最大的陷阱

能保证什么

绝对的执行原子性:脚本一旦开始,绝不会被其他命令插队。Redis 把它当成一个不可分割的“超级命令”。

不能保证什么

没有事务回滚。脚本执行中途如果服务器崩溃,已执行的写操作不会自动回滚。

示例:

redis.call('SET', 'key1', 'a')  -- 执行了
-- 此时断电
redis.call('SET', 'key2', 'b')  -- 没执行

结果:key1 是新值,key2 还是老样子,数据处于不一致状态。

解决方案

把所有校验逻辑放在脚本最前面,写入操作集中在最后几行,缩小风险窗口:

-- 先做全部检查(不改变数据)
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) <= 0 then
    return -1
end
-- 检查通过后再集中写入
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1

六、经典实战:秒杀场景的并发控制

不用 Lua(错误方案)

stock = redis.get("stock:001")
if int(stock) > 0:
    redis.decr("stock:001")

问题:A 和 B 同时 get 到 1,各自判断 >0 后各自执行 decr,结果库存变 -1,超卖

用 Lua(正确方案)

-- KEYS[1]: 库存key, KEYS[2]: 已抢用户set, ARGV[1]: 用户ID
local stock = redis.call('DECR', KEYS[1])
if stock < 0 then
    redis.call('INCR', KEYS[1])  -- 回滚
    return -1  -- 卖完
end
redis.call('SADD', KEYS[2], ARGV[1])  -- 记录用户
return stock

效果:A 的脚本执行期间,B 的脚本完全进不来。A 执行完,B 再执行时看到的库存已经是 0(或 -1 被回滚),无法多抢。

七、Java 实战:怎么在代码里用 Lua

1. 引入依赖(Spring Boot 项目)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

这自带 Lettuce 客户端,原生支持 Lua。

2. 定义脚本(推荐用DefaultRedisScript)

import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
// 放在静态变量里,避免重复创建
private static final String SECKILL_SCRIPT = 
    "local stock = redis.call('DECR', KEYS[1]) " +
    "if stock < 0 then " +
    "    redis.call('INCR', KEYS[1]) " +
    "    return -1 " +
    "end " +
    "redis.call('SADD', KEYS[2], ARGV[1]) " +
    "return stock";
private static final RedisScript<Long> SECKILL = 
    new DefaultRedisScript<>(SECKILL_SCRIPT, Long.class);

3. 执行脚本

@Autowired
private StringRedisTemplate stringRedisTemplate;
public Long doSeckill(String stockKey, String usersKey, String userId) {
    List<String> keys = Arrays.asList(stockKey, usersKey);
    Object[] args = { userId };
    return stringRedisTemplate.execute(
        SECKILL,   // 脚本对象
        keys,      // KEYS 列表
        args       // ARGV 列表
    );
}

4. 用EVALSHA优化性能

每次传完整脚本有网络开销。可以提前加载脚本到 Redis,后续用 SHA1 哈希调用:

// 加载脚本
String sha1 = stringRedisTemplate.getConnectionFactory()
    .getConnection().scriptLoad(SECKILL_SCRIPT.getBytes());
// 用 SHA1 执行
DefaultRedisScript<Long> cachedScript = new DefaultRedisScript<>();
cachedScript.setSha1(sha1);
cachedScript.setResultType(Long.class);
stringRedisTemplate.execute(cachedScript, keys, args);

Spring 的 DefaultRedisScript 默认行为:第一次执行时用 EVAL,失败则尝试 EVALSHA,透明处理缓存,一般无需手动干预。

八、工程化避坑清单(重要!)

1. 脚本必须短小精悍

  • 禁止:大循环、复杂正则、遍历大集合(如 SMEMBERS 百万级 Set)。
  • 原则:只做快速判断 + 少量原子操作,耗时控制在毫秒级。

2. 永远设置超时

// 应用层设置网络超时
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<?, ?> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    // 关键:设置命令超时,避免被慢脚本卡死
    template.setDefaultTimeout(Duration.ofSeconds(2));
    return template;
}

3. 集群模式下,所有 KEY 必须在同一个 slot

Redis Cluster 要求 Lua 脚本操作的所有 key 必须在同一节点。常用的技巧是用 hash tag

EVAL "..." 2 {order}:stock {order}:users

{order} 告诉 Redis,只对花括号里的部分做哈希计算,保证这两个 key 落在同一个 slot。

4. 脚本里不要有死循环

while true do
    -- Redis 主线程会卡死,只能靠 `SCRIPT KILL` 强制中断
    -- 如果脚本已经执行过写命令,SHUTDOWN NOSAVE 是最后手段
end

Redis 7.0+ 有 lua-time-limit 保护(默认 5 秒),但仍然不建议测试这个底线。

5. 合理拆分脚本

不要把一个大而全的脚本用于所有场景。按职责拆分:扣库存一个脚本、发优惠券一个脚本、记录日志一个脚本。保持原子性边界最小化。

6. 充分测试边界条件

  • 库存为 0 时
  • 参数为空时
  • 参数非法时(传了非数字)
  • 并发压测验证原子性

九、总结:Lua 在 Redis 中的独特定位

维度 常规编程语言(Java/Python) Redis Lua 脚本
执行位置 客户端 服务端(靠近数据)
网络开销 N 次往返 1 次
原子性 需借助事务/分布式锁 天然原子性
逻辑能力 无限 受限(但够用)
复杂度 灵活,但容易出错 简单,脚本短小即安全

一句话总结:Redis Lua 脚本解决的是“需要在一次往返中,原子性地执行多条 Redis 命令且夹杂业务判断”这个特定问题。它不是用来取代 Java 的,而是让你的 Java 应用在处理某些 Redis 操作时,更安全、更高效。

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章