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. 五大经典应用场景
- 分布式锁 这是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
- 原子性计数器
单纯的
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
- 限流器
在“原子计数器”的基础上,可以轻松实现一个简单的时间窗口限流器。
- 逻辑:每次请求时递增计数器。如果是这个时间窗口内的第一次请求,顺便设置一个过期时间。如果计数器超过阈值,就拒绝服务。这个逻辑全部在服务端一次完成,高效且可靠。
-- 固定窗口限流脚本
-- 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.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
- 保证数据一致性 在复杂的业务流程中,可能需要同时更新多个相关的数据,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。记住,脚本一定要写得快、准、狠。