游乐游手机版
首页/数据库/文章详情

Redis Lua脚本从入门到避坑完整实战案例操作指南

时间:2026-07-26 20:27
Redis内置Lua解释器,通过EVAL命令原子执行多个操作,参数使用KEYS和ARGV区分,用redis call pcall执行命令。脚本需短小避免阻塞,集群下所有key必须在同一slot,并设置超时保护,同时避免使用全局变量以保证原子性。

一、为什么是Lua?

从Redis 2.6.0版本开始,它就内置了Lua解释器,这带来了几个非常直接的好处:

Redis Lua脚本从入门到避坑指南

  • 零依赖:不像Ja va要加Ma ven依赖,也不像Python要pip install各种库,什么都不用装,拿来就用。
  • 开箱即用:只要Redis版本是2.6及以上,直接用EVAL命令就能跑起来。

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

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

1. 大小写敏感

Lua严格区分大小写。return写成Return是绝对不行的,KEYS也不能写成keys。这一点和Ja va完全一致。

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. 逻辑运算符

和Ja va的对照关系:

Ja vaLua
!anot a
a && ba and b
a || ba 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 <脚本内容>   

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

它实际是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.call vs redis.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实例的主线程,不影响你的Ja va应用、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被回滚),无法多抢。

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

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


    org.springframework.boot
    spring-boot-starter-data-redis

这自带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 SECKILL = 
    new DefaultRedisScript<>(SECKILL_SCRIPT, Long.class);

3. 执行脚本

@Autowired
private StringRedisTemplate stringRedisTemplate;

public Long doSeckill(String stockKey, String usersKey, String userId) {
    List 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 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 NOSA VE 是最后手段
end

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

5. 合理拆分脚本

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

6. 充分测试边界条件

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

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

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

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

来源:https://www.jb51.net/database/365550o0w.htm
上一篇三款主流数据库真实性能对比详细总结:PostgreSQL、MySQL与SQLite 下一篇MySQL连接数爆满排查与永久优化方案
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Redis是什么:核心特性、架构与应用场景解析
数据库 · 2026-09-01

Redis是什么:核心特性、架构与应用场景解析

Redis是一款基于内存的键值型NoSQL数据库,以超高读写速度和丰富的数据结构著称。本文系统梳理Redis的核心特性、架构组成、性能优势及典型应用场景,并通过与Memcached、MySQL、MongoDB的对比,帮助开发者快速判断Redis是否适合当前业务需求。

Windows 安装 MongoDB 完整图文教程
数据库 · 2026-09-01

Windows 安装 MongoDB 完整图文教程

本文详细介绍在 Windows 系统上安装 MongoDB 的完整流程。从官网下载 MSI 安装包开始,逐步演示自定义安装路径、配置 Windows 服务、跳过 MongoDB Compass 等关键选项,并提供通过系统服务列表验证安装是否成功的方法,帮助开发者快速搭建本地 MongoDB 环境。

Linux 安装 MongoDB 完整指南:依赖配置、环境变量与服务启动
数据库 · 2026-09-01

Linux 安装 MongoDB 完整指南:依赖配置、环境变量与服务启动

本文详解在 Linux 系统下安装 MongoDB 的完整流程,涵盖依赖包安装、二进制包下载解压、环境变量配置、数据与日志目录创建及服务启动验证。通过标准化命令与路径说明,帮助开发者快速完成部署并确认服务状态。

MacOS安装MongoDB完整教程
数据库 · 2026-09-01

MacOS安装MongoDB完整教程

本文介绍在MacOS系统下安装MongoDB的完整流程,涵盖下载、解压、目录配置、环境变量设置及服务启动。通过明确的命令与参数说明,帮助开发者快速完成环境搭建并验证安装结果。

Ubuntu系统安装与配置Redis完整指南
数据库 · 2026-09-01

Ubuntu系统安装与配置Redis完整指南

本文详解在Ubuntu系统中安装Redis的两种主流方式:apt在线安装与源码编译安装。涵盖版本选择逻辑、服务启停与状态检查、连接验证方法,以及在线练习工具与桌面GUI客户端的对比与使用建议,帮助开发者快速搭建并验证Redis运行环境。