Redis 脚本:让数据操作更高效、更安全
在日常开发中,我们常会遇到需要对 Redis 中的数据进行一系列原子性操作的场景。比如:检查某个用户是否已登录、累加积分、执行库存扣减等。如果这些操作拆分成多个命令,不仅会增加网络往返次数,还可能因为并发问题导致数据不一致。
这时候,Redis 脚本 就成了我们解决这类问题的“利器”。它允许我们将多个 Redis 命令打包成一个逻辑单元,在服务器端一次性执行,既提升了性能,又保证了操作的原子性。
想象一下,你去银行办理业务,每次取钱都要重新排队、填单子、等审核。如果能一次性把所有业务提交给柜台,一次处理完,是不是更快更安心?Redis 脚本就相当于这个“综合业务柜台”,把多个操作合并处理,避免了“多头跑”的麻烦。
什么是 Redis 脚本?它能做什么?
Redis 脚本使用 Lua 语言编写,由 Redis 服务器内部的 Lua 引擎执行。这意味着脚本不是在客户端执行,而是在 Redis 服务端完成,所有数据操作都在同一个上下文中进行,避免了网络延迟和并发干扰。
举个例子,假设我们要实现“用户积分扣减”功能,逻辑如下:
- 检查用户当前积分是否足够;
- 如果足够,扣减积分并返回成功;
- 如果不足,返回失败。
如果用普通命令,你需要先执行 GET 获取积分,再判断,再执行 DECR,整个过程需要至少两次网络通信。而使用 Redis 脚本,只需一次调用,就能完成全部逻辑。
如何使用 Redis 脚本?基础语法与执行方式
Redis 脚本通过 EVAL 和 EVALSHA 命令执行。
使用 EVAL 执行脚本
EVAL script numkeys key [key ...] arg [arg ...]
script:Lua 脚本内容,用引号包裹;numkeys:后续传入的 key 的数量;key [key ...]:要操作的 key 列表;arg [arg ...]:传给脚本的参数。
注意:Lua 脚本中,key 和 arg 是通过
KEYS和ARGV数组访问的。
示例:简单的积分扣减脚本
-- 脚本:检查并扣减用户积分
-- 参数说明:
-- KEYS[1]:用户积分的 key,如 "user:1001:score"
-- ARGV[1]:要扣减的积分值,如 10
-- 返回值:1 表示成功,0 表示失败
local current_score = redis.call("GET", KEYS[1])
if current_score == false then
-- 如果 key 不存在,返回失败
return 0
end
current_score = tonumber(current_score)
-- 检查当前积分是否足够
if current_score < tonumber(ARGV[1]) then
return 0
end
-- 扣减积分并返回结果
redis.call("DECRBY", KEYS[1], ARGV[1])
return 1
使用 EVAL 执行这个脚本:
EVAL "local current_score = redis.call('GET', KEYS[1]) if current_score == false then return 0 end current_score = tonumber(current_score) if current_score < tonumber(ARGV[1]) then return 0 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1" 1 user:1001:score 10
执行结果为 1,表示扣减成功。
✅ 注释说明:
redis.call()是 Lua 调用 Redis 命令的接口;tonumber()将字符串转为数字;KEYS[1]是第一个 key,对应user:1001:score;ARGV[1]是第一个参数,对应10。
Redis 脚本的优势:原子性、性能与安全性
1. 原子性:一个脚本,一个执行
Redis 脚本的执行是原子的。也就是说,整个脚本会作为一个整体运行,不会被其他客户端命令打断。这对于需要一致性保证的场景(如库存扣减、投票系统)至关重要。
类比:就像你在银行办理业务时,整个流程必须一气呵成,不能中途被别人插队。
2. 减少网络开销
普通方式:GET → 判断 → DECR → 返回,至少 2 次网络往返。
使用脚本:一次 EVAL,全部搞定,只用 1 次网络通信。
3. 避免并发问题
在高并发场景下,多个客户端同时读取同一数据并修改,容易出现“超卖”或“积分透支”问题。
而 Redis 脚本在服务器端执行,所有操作都在同一时刻完成,避免了“读-改-写”过程中的竞争条件。
实际案例:实现“秒杀商品库存扣减”
假设你正在开发一个秒杀系统,商品库存为 100 件,每个用户只能抢一次。
我们用 Redis 脚本实现如下逻辑:
- 检查库存是否大于 0;
- 如果有库存,扣减 1 并记录用户已抢;
- 返回结果。
脚本代码:
-- 脚本:秒杀库存扣减
-- 参数说明:
-- KEYS[1]:库存 key,如 "product:1001:stock"
-- KEYS[2]:用户抢购记录 key,如 "product:1001:users"
-- ARGV[1]:用户 ID,如 "user:1001"
-- 1. 获取当前库存
local stock = redis.call("GET", KEYS[1])
if stock == false then
-- 库存 key 不存在,视为无库存
return 0
end
stock = tonumber(stock)
-- 2. 检查库存是否足够
if stock <= 0 then
return 0
end
-- 3. 检查用户是否已抢过(避免重复抢)
local user_key = KEYS[2] .. ":" .. ARGV[1]
if redis.call("EXISTS", user_key) == 1 then
return 0 -- 已抢过,返回失败
end
-- 4. 扣减库存
redis.call("DECRBY", KEYS[1], 1)
-- 5. 记录用户抢购行为,设置过期时间(如 1 小时)
redis.call("SET", user_key, "1", "EX", 3600)
-- 6. 返回成功
return 1
执行命令:
EVAL "local stock = redis.call('GET', KEYS[1]) if stock == false then return 0 end stock = tonumber(stock) if stock <= 0 then return 0 end local user_key = KEYS[2] .. ':' .. ARGV[1] if redis.call('EXISTS', user_key) == 1 then return 0 end redis.call('DECRBY', KEYS[1], 1) redis.call('SET', user_key, '1', 'EX', 3600) return 1" 2 product:1001:stock product:1001:users user:1001
返回 1 表示抢购成功,返回 0 表示失败(库存不足或已抢)。
💡 这个脚本在高并发下依然安全,因为 Redis 会确保脚本执行期间不会被中断。
脚本缓存与复用:使用 EVALSHA
每次调用 EVAL 都要传入完整脚本内容,如果脚本很长,不仅影响性能,还容易出错。
Redis 提供了 EVALSHA 命令,通过脚本的 SHA1 哈希值来执行,实现脚本缓存。
步骤:
- 先用
SCRIPT LOAD将脚本加载到 Redis 缓存; - 然后用
EVALSHA通过哈希值执行。
-- 1. 加载脚本(返回 SHA1 值)
SCRIPT LOAD "local stock = redis.call('GET', KEYS[1]) if stock == false then return 0 end stock = tonumber(stock) if stock <= 0 then return 0 end local user_key = KEYS[2] .. ':' .. ARGV[1] if redis.call('EXISTS', user_key) == 1 then return 0 end redis.call('DECRBY', KEYS[1], 1) redis.call('SET', user_key, '1', 'EX', 3600) return 1"
返回值:a1b2c3d4e5f6...(SHA1 哈希)
-- 2. 使用哈希值执行
EVALSHA a1b2c3d4e5f6... 2 product:1001:stock product:1001:users user:1001
✅ 优势:脚本只需加载一次,后续用哈希值调用,更高效。
Redis 脚本的注意事项与最佳实践
| 事项 | 说明 |
|---|---|
| 脚本不能无限执行 | Redis 默认最大执行时间 5 秒,超时会终止脚本 |
不能使用 redis.call() 调用 SCRIPT 命令 |
脚本中无法动态加载其他脚本 |
| 避免复杂逻辑 | Lua 虽强大,但脚本应保持简洁,避免复杂计算 |
使用 KEYS 和 ARGV 传参 |
保持结构清晰,避免硬编码 key |
| 执行前验证脚本 | 可用 EVAL 本地测试,确认无误再上线 |
总结:Redis 脚本是你的数据操作“加速器”
Redis 脚本不仅让代码更简洁,更重要的是它提供了原子性、高性能和高安全性的保障。无论是用户积分管理、库存扣减,还是分布式锁、排行榜更新,Redis 脚本都能帮你把多个操作“打包处理”,避免网络延迟和并发问题。
作为开发者,掌握 Redis 脚本,就像学会了在数据库中使用存储过程一样,能让你在处理复杂业务逻辑时游刃有余。
不要再说“我只能用一条命令操作 Redis”了,现在你可以用一段 Lua 代码,完成整个业务流程。这不仅是技术的提升,更是思维的升级。
下次你在写 Redis 代码时,不妨问自己一句:这个操作能不能用脚本一次搞定?答案很可能是——能。