Redis中的Lua

一. Redis中的Lua脚本的作用

Redis中的Lua脚本主要解决复杂操作的原子性和性能优化两大核心问题。它允许你在Redis服务器端直接执行自定义的逻辑,而不是把数据拉到客户端处理后再写回去。

核心作用

1. 保证原子性操作

Redis执行Lua脚本时,脚本中的所有命令会作为一个整体被执行,中间不会被其他命令插入。这完美解决了多命令组合操作时的并发安全问题。

经典场景: 实现分布式锁、库存扣减、限流计数器等。比如秒杀场景下,检查库存、扣减库存、记录订单这三个操作必须原子执行,用Lua脚本就能确保数据一致性。

2. 减少网络开销

将多个Redis命令合并到一个Lua脚本中,原本需要多次网络请求的操作变成一次请求完成。数据在Redis服务器本地处理,避免了大量数据在客户端和服务器之间来回传输。

3.扩展Redis指令集

你可以用Lua实现Redis原生不支持的复杂业务逻辑。比如自定义数据结构操作、复杂条件判断、循环处理等,让Redis变成一个具备计算能力的"小数据库"。

4. 提高代码复用率

脚本可以被缓存到Redis服务器,客户端通过SHA1摘要重复调用,无需每次都发送完整的脚本代码。多个应用可以共享同一套脚本逻辑

总结

Redis Lua脚本让你能把多个操作打包成一个原子性的、在服务器本地执行的任务,既保证了数据一致性,又提升了性能。特别适合秒杀、分布式锁、计数器、复杂条件更新等需要"读-改-写"原子操作的场景。

二、Lua安装

windows下载地址 :https://luabinaries.sourceforge.net/

三 、 Lua在Redis中最常用、最经典的几个应用场景

1. 在Redis中操作Lua脚本的常用命令

命令作用一句话解释
EVAL执行给定的Lua脚本直接把脚本和参数传给Redis运行一次。
SCRIPT LOAD将脚本缓存到Redis把脚本存到Redis里,会返回一个SHA1值作为“脚本ID”。
EVALSHA根据SHA1值执行缓存的脚本拿着“脚本ID”去执行之前存好的脚本,省流量,效率更高。
SCRIPT EXISTS检查脚本是否已缓存看看某个“脚本ID”对应的脚本还在不在Redis里。
SCRIPT KILL终止正在运行的脚本如果脚本执行太久卡住了,可以用这个命令尝试杀掉它。
SCRIPT FLUSH清空所有缓存的脚本一键清除Redis里缓存的所有脚本,操作要小心\-1

2. 五大经典应用场景

  1. 分布式锁 这是Lua脚本在Redis中最经典的应用。获取锁和释放锁的多个步骤必须是一个原子操作,否则会出现并发问题。
  • 获取锁:使用 SET NX PX 命令,一步到位地尝试设置一个带过期时间的键,保证只有一个客户端能拿到锁-8
  • 释放锁:释放锁时,需要先检查当前持有锁的客户端是否就是自己(通过一个唯一值比如UUID判断),然后再删除锁。这两个动作必须通过Lua脚本一起执行,才能避免误删别人的锁-2-8
-- 安全的释放锁脚本
-- KEYS[1]: 锁的名称
-- ARGV[1]: 客户端的唯一标识
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
  1. 原子性计数器 单纯的INCR命令是原子的,但如果计数逻辑稍微复杂一点,比如要限制计数上限,就需要Lua了-2
  • 场景:限制某个用户每分钟只能调用某个接口100次。
  • 实现:在Lua脚本里,你可以先GET当前的计数,判断是否超过阈值,没超过再用INCR增加。这整个“读-判断-写”的过程由Lua保证原子性,避免了在高并发下计数超限的风险。
-- 带阈值的原子计数器
-- KEYS[1]: 计数器键名
-- ARGV[1]: 递增步长
-- ARGV[2]: 最大阈值
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
local new_value = current + tonumber(ARGV[1])

if new_value > tonumber(ARGV[2]) then
    return 0  -- 超过阈值,返回失败
else
    redis.call('SET', KEYS[1], new_value)
    return new_value
end
  1. 限流器

在“原子计数器”的基础上,可以轻松实现一个简单的时间窗口限流器。

  • 逻辑:每次请求时递增计数器。如果是这个时间窗口内的第一次请求,顺便设置一个过期时间。如果计数器超过阈值,就拒绝服务。这个逻辑全部在服务端一次完成,高效且可靠。
-- 固定窗口限流脚本
-- KEYS[1]: 限流键名 (例如 "rate_limit:user123")
-- ARGV[1]: 阈值 (例如 100)
-- ARGV[2]: 时间窗口 (秒)
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
if current >= tonumber(ARGV[1]) then
    return 0  -- 被限流
end
redis.call('INCR', KEYS[1])
if current == 0 then
    redis.call('EXPIRE', KEYS[1], ARGV[2])
end
return 1  -- 请求通过
  1. 条件更新(CAS) 实现“比较并更新”操作,这也是数据库一致性中的常见需求-2-9
  • 场景:只有当前版本号是1.0时,才更新数据到2.0
  • 实现:在Lua脚本里用GET拿到当前值,与期望值比较,如果相等则执行更新。这能防止在“读”和“写”之间数据被其他客户端修改。
-- 比较并更新(CAS)脚本
-- KEYS[1]: 目标键名
-- ARGV[1]: 预期值
-- ARGV[2]: 新值
if redis.call('GET', KEYS[1]) == ARGV[1] then
    redis.call('SET', KEYS[1], ARGV[2])
    return 1  -- 更新成功
else
    return 0  -- 更新失败
end
  1. 保证数据一致性 在复杂的业务流程中,可能需要同时更新多个相关的数据,Lua脚本可以确保它们要么全部成功,要么全部失败(原子性)。 场景:在订单系统中,将一个“临时订单”转换为“支付订单”。你需要读取临时订单数据、创建支付订单、再删除临时订单。用Lua脚本包裹这三个步骤,就能保证数据转换过程的完整性,不会出现中间状态-4

3.核心实践与注意事项

  • 使用redis.call()redis.pcall():在Lua脚本里,通过这两个函数来调用Redis命令。pcall会捕获命令执行中的错误并返回,而call则会直接抛出错误导致脚本停止-5
  • 参数规范与命名空间:所有要操作的Redis键,必须通过KEYS数组参数传递,业务参数通过ARGV传递。切忌在Lua代码里对键名硬编码,这在大集群环境下会引发严重问题-1-3-9
  • 最佳实践:SCRIPT LOAD + EVALSHA:对于频繁使用的脚本,先用SCRIPT LOAD缓存它,得到一个唯一的SHA1值。之后执行时,都用EVALSHA加这个SHA1值来调用。这样做能极大减少网络传输的数据量,提升性能-1-3-5
  • 警惕长脚本:Redis是单线程的,Lua脚本在执行时会阻塞所有其他命令。如果脚本里有耗时的循环或操作,会导致Redis卡顿。默认脚本执行超过5秒,就会被强制杀死-1-3。记住,脚本一定要写得快、准、狠。