Redis Lua脚本概述
1 ) 什么是Redis Lua脚本
Redis Lua脚本是一种在Redis服务器端执行的脚本语言,Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。
其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能
2 ) Redis引入Lua脚本的原因
Redis提供了非常丰富的指令集,官网上提供了200多个命令。但是某些特定领域,需要扩充若干指令原子性执行时,仅使用原生命令便无法完成。
Redis为这样的用户场景提供了lua脚本支持,用户可以向服务器发送lua脚本来执行自定义动作,获取脚本的响应数据
3 ) 使用Lua脚本的好处
减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延
原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入
复用性:客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本
复杂逻辑处理:Lua脚本可以处理复杂的逻辑,比如循环、条件判断等
Lua与数据库交互基础
1 ) Lua数据类型适配
Lua的8种基础类型(nil/boolean/number/string/table等)需转换为数据库兼容格式
示例:表结构序列化为JSON存储至Redis:
local user = {name="Alice", age=30, contact={phone="123456"}}
redis.call("SET", "user:1001", cjson.encode(user))
2 ) 连接数据库方式
Redis:通过redis.call()内置函数操作
SQL数据库:使用LuaSQL驱动(如MySQL模块):
local driver = require "luasql.mysql"
local env = driver.mysql()
local conn = env:connect('mydb', 'user', 'pwd')
3 ) 原子性保证
Redis单线程模型确保Lua脚本执行期间不被中断,避免多命令操作的竞态条件(如GET后SET的间隙被其他请求插入)
示例
-- 原子计数器递增示例
local key = KEYS[1]
local increment = tonumber(ARGV[1])
return redis.call('INCRBY', key, increment)
脚本执行期间阻塞其他命令,避免竞态条件(替代 MULTI/EXEC 事务)
示例:分布式锁的获取与释放原子操作:
--- KEYS[1]=lockkey, ARGV[1]=clientid, ARGV[2]=expire_time
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
else
return 0
end
Lua语法基础
1 ) 变量与数据类型
--- 全局变量
globalVar = "I am global"
--- 局部变量
local localVar = "I am local"
--- 数据类型
local num = 42 -- 数字
local str = "hello" -- 字符串
local bool = true -- 布尔值
local tbl = {1,2,3} -- 表(数组)
local dict = {a=1,b=2} -- 表(字典)
local nilVal = nil -- 空值
2 ) 控制结构
-- 条件语句
if condition then
-- 执行代码
elseif anotherCondition then
-- 执行代码
else
--- 执行代码
end
--- 循环语句
for i=1,10 do
print(i)
end
while condition do
--- 循环体
end
3 ) 函数定义
function add(a, b)
return a + b
end
-- 匿名函数
local multiply = function(a, b)
return a * b
end
Redis Lua脚本核心技术
1 ) 执行原理与优势
原子性:脚本整体执行,不被其他命令打断
网络优化:合并多个操作为单次请求
复用性:脚本缓存在Redis,通过SHA1摘要调用(EVALSHA)
EVAL命令
EVAL script numkeys key [key ...] arg [arg ...]
- script:Lua脚本程序
- numkeys:指定键名参数的个数
- key:键名参数,通过KEYS数组访问
(KEYS[1], KEYS[2]...) - arg:附加参数,通过ARGV数组访问
(ARGV[1], ARGV[2]...)
EVALSHA命令
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
使用脚本的SHA1值执行,避免重复传输脚本内容
SCRIPT相关命令
SCRIPT LOAD script # 加载脚本到缓存
SCRIPT EXISTS sha1 # 检查脚本是否存在
SCRIPT FLUSH # 清空所有脚本缓存
SCRIPT KILL # 终止正在执行的脚本
2 ) 关键命令详解
EVAL "return redis.call('GET', KEYS[1])" 1 user:100 # 执行脚本
SCRIPT LOAD "return redis.call('GET', KEYS[1])" # 缓存脚本返回SHA1
EVALSHA "a1b2c3d4" 1 user:100 # 通过SHA调用
3 ) 数据类型转换规则
| Redis返回类型 | Lua类型 |
|---|---|
| INTEGER | number |
| BULK REPLY | string |
| MULTI REPLY | table (数组) |
或
| Lua 类型 | Redis 返回类型 | 示例 |
|---|---|---|
| number | Integer | return 3.14 → 3 |
| table | Multi-bulk reply | return {1, ‘a’} → [1, “a”] |
| nil | Null | return nil → (nil) |
Redis Lua脚本实战案例
1 ) 分布式锁(防误删)
local key = KEYS[1]
local token = ARGV[1]
if redis.call("GET", key) == token then
return redis.call("DEL", key) -- 令牌匹配才删除
end
return 0
再举一个例子
--- 获取分布式锁
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local expireTime = tonumber(ARGV[2])
local result = redis.call('SETNX', lockKey, lockValue)
if result == 1 then
redis.call('EXPIRE', lockKey, expireTime)
return 1
else
return 0
end
2 ) 限流器(每秒限QPS)
local key = "rate_limit:"..KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or "0")
if current + 1 > limit then
return 0 -- 超限拒绝
else
redis.call("INCRBY", key, "1")
redis.call("EXPIRE", key, "1") -- 1秒过期
return 1
end
再看一个例子
--- 令牌桶限流算法
local rateKey = KEYS[1]
local limit = tonumber(ARGV[1]) -- 最大限制
local expireTime = ARGV[2] -- 过期时间
local current = redis.call('GET', rateKey)
if current == false then
redis.call('SET', rateKey, 1)
redis.call('EXPIRE', rateKey, expireTime)
return 1
else
if tonumber(current) >= limit then
return 0
else
redis.call('INCR', rateKey)
return 1
end
end
3 ) 库存扣减(原子操作)
local stock_key = KEYS[1]
local stock = tonumber(redis.call('GET', stock_key))
if stock <= 0 then
return -1 -- 库存不足
end
redis.call("DECR", stock_key)
return stock - 1 -- 返回剩余库存
4 )复杂数据处理
批量数据处理
--- 批量获取并处理数据
local keys = {'user:1', 'user:2', 'user:3'}
local values = redis.call('MGET', unpack(keys))
local results = {}
for i, value in ipairs(values) do
if value then
-- 处理每个值
table.insert(results, string.upper(value))
end
end
return results
5 )有序集合统计
-- 计算有序集合中分数范围内的元素数量
local zsetKey = KEYS[1]
local minScore = tonumber(ARGV[1])
local maxScore = tonumber(ARGV[2])
local count = 0
local members = redis.call('ZRANGE', zsetKey, 0, -1, 'WITHSCORES')
for i=1, #members, 2 do
local score = tonumber(members[i+1])
if score >= minScore and score <= maxScore then
count = count + 1
end
end
return count
四大核心场景与Lua实现
1 ) 场景1:分布式锁
--- KEYS[1]:锁键, ARGV[1]:客户端ID, ARGV[2]:过期时间(ms)
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1 -- 加锁成功
elseif redis.call('GET', KEYS[1]) == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1 -- 重入锁续期
end
return 0 -- 获取锁失败
2 ) 场景2:令牌桶限流
--- KEYS[1]:限流key, ARGV[1]:桶容量, ARGV[2]:填充速率(令牌/ms)
local tokens_key = KEYS[1]..':tokens'
local timestamp_key = KEYS[1]..':timestamp'
local now = tonumber(ARGV[3])
local tokens = tonumber(redis.call('GET', tokens_key) or ARGV[1])
local lasttime = tonumber(redis.call('GET', timestampkey) or now)
--- 计算时间差产生的令牌
local delta = math.floor((now - last_time) * tonumber(ARGV[2]))
tokens = math.min(tokens + delta, tonumber(ARGV[1]))
if tokens >= 1 then
redis.call('SET', tokens_key, tokens-1)
redis.call('SET', timestamp_key, now)
return 1 -- 允许通过
end
return 0 -- 限流
3 ) 场景3:库存扣减
--- 防止超卖的核心逻辑
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
redis.call('SADD', KEYS[2], ARGV[2]) -- 记录订单
return 1
end
return 0
NestJS企业级集成方案
1 ) 脚本预加载与SHA执行
// redis.provider.ts
@Injectable()
export class RedisService {
private scriptShas = new Map<string, string>();
async loadScript(script: string): Promise<string> {
const sha = await this.redisClient.scriptLoad(script);
this.scriptShas.set(script, sha);
return sha;
}
async executeScript<T>(script: string, keys: string[], args: any[]): Promise<T> {
const sha = this.scriptShas.get(script);
return this.redisClient.evalsha(sha, keys.length, ...keys, ...args);
}
}
2 )AOP切面实现分布式锁
// distributed-lock.decorator.ts
export const DistributedLock = (key: string, ttl: number) => {
return applyDecorators(
UseInterceptors(new LockInterceptor(key, ttl))
);
};
// LockInterceptor实现
@Injectable()
class LockInterceptor implements NestInterceptor {
constructor(
private readonly redisService: RedisService
) {}
async intercept(context: ExecutionContext, next: CallHandler) {
const lockKey = this.generateKey(context);
const clientId = uuidv4();
try {
const locked = await this.redisService.executeScript<number>(
LOCK_SCRIPT, [lockKey], [clientId, ttl]
);
if (!locked) throw new ForbiddenException('Resource locked');
return next.handle();
} finally {
await this.redisService.del(lockKey);
}
}
}
再来看一个NestJS 集成 Lua 脚本实战
1 ) 模块化架构设计
// redis-lua.module.ts
@Module({
providers: [RedisLuaService],
exports: [RedisLuaService],
})
export class RedisLuaModule {}
2 ) 服务层封装脚本执行
// redis-lua.service.ts
import { Injectable } from '@nestjs/common';
import { RedisService } from 'nestjs-redis';
@Injectable()
export class RedisLuaService {
constructor(private readonly redisService: RedisService) {}
async runScript<T>(script: string, keys: string[], args: any[]): Promise<T> {
const client = this.redisService.getClient();
// 优先使用 EVALSHA 避免重复传输脚本
const sha1 = await client.script('LOAD', script);
try {
return await client.evalsha(sha1, keys.length, ...keys, ...args);
} catch (e) {
return await client.eval(script, keys.length, ...keys, ...args);
}
}
}
3 ) 业务场景:秒杀库存扣减
Lua 脚本 (stock_deduction.lua):
local stock_key = KEYS[1] -- "product:1001:stock"
local order_key = KEYS[2] -- "product:1001:orders"
local quantity = tonumber(ARGV[1])
local user_id = ARGV[2]
local stock = tonumber(redis.call('GET', stock_key))
if stock >= quantity then
redis.call('DECRBY', stock_key, quantity)
redis.call('SADD', orderkey, userid)
return 1 -- 成功
else
return 0 -- 库存不足
end
NestJS 调用层:
@Post('seckill')
async seckill(@Body() dto: SeckillDto) {
const script = readFileSync('./scripts/stock_deduction.lua', 'utf8');
const result = await this.luaService.runScript<number>(
script,
[product:${dto.productId}:stock, product:${dto.productId}:orders],
[dto.quantity, dto.userId]
);
return result === 1 ? 'Success' : 'Out of stock';
}
实际应用案例
1 ) 秒杀系统实现
--- 秒杀库存扣减
local productId = KEYS[1]
local userId = ARGV[1]
local quantity = tonumber(ARGV[2])
--- 检查用户是否已购买
local userKey = 'seckill:user:' .. productId .. ':' .. userId
if redis.call('EXISTS', userKey) == 1 then
return 0 -- 已购买
end
--- 检查并扣减库存
local stockKey = 'seckill:stock:' .. productId
local stock = tonumber(redis.call('GET', stockKey))
if stock >= quantity then
redis.call('DECRBY', stockKey, quantity)
redis.call('SET', userKey, 1)
redis.call('EXPIRE', userKey, 3600) -- 1小时过期
return 1 -- 成功
else
return -1 -- 库存不足
end
2 ) 实时统计计算
--- 实时计算UV/PV统计
local dateKey = KEYS[1]
local userId = ARGV[1]
local type = ARGV[2] -- 'uv' or 'pv'
if type == 'pv' then
-- PV统计
redis.call('INCR', 'stats:pv:' .. dateKey)
return 1
elseif type == 'uv' then
-- UV统计
local uvSet = 'stats:uv:' .. dateKey
local isNew = redis.call('SADD', uvSet, userId)
if isNew == 1 then
return 1 -- 新用户
else
return 0 -- 重复用户
end
end
调试与监控
1 ) 脚本调试技巧
-- 使用日志输出调试信息
redis.log(redis.LOG_WARNING, 'Debug: key=' .. KEYS[1] .. ', value=' .. ARGV[1])
-- 返回调试信息
local debugInfo = {
key = KEYS[1],
arg = ARGV[1],
result = redis.call('GET', KEYS[1])
}
return debugInfo
2 ) 性能监控
--- 记录脚本执行时间
local startTime = redis.call('TIME')[1] * 1000000 + redis.call('TIME')[2]
--- 执行业务逻辑
local result = redis.call('GET', KEYS[1])
local endTime = redis.call('TIME')[1] * 1000000 + redis.call('TIME')[2]
local duration = endTime - startTime
redis.call('LPUSH', 'script:performance:log', duration)
redis.call('LTRIM', 'script:performance:log', 0, 999) -- 保留最近1000条记录
return result
高级技巧与生产实践
1 ) 脚本调试与优化
避免KEYS *:用SCAN替代防阻塞
参数化键名:通过KEYS数组传递所有键,兼容Redis集群
添加脚本幂等标识:通过请求ID避免崩溃后重试导致重复执行
使用EVALSHA避免重复传输脚本
--- 首次加载脚本
local script = 'return redis.call("GET", KEYS[1])'
local sha1 = redis.call('SCRIPT', 'LOAD', script)
--- 后续使用SHA1执行
redis.call('EVALSHA', sha1, 1, 'mykey')
使用 SCRIPT KILL 终止长时运行脚本(默认超时 5 秒)
NestJS 超时处理
const result = await Promise.race([
this.luaService.runScript(script, keys, args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Script timeout')), 5000)
)
]);
3 ) 错误处理
使用pcall()捕获异常:
local ok, result = pcall(redis.call, "GET", "key")
if not ok then
return {err = result} -- 返回错误信息
end
再来看一个例子
--- 使用redis.pcall进行错误处理
local success, result = pcall(function()
return redis.call('INCR', KEYS[1])
end)
if success then
return result
else
return redis.call('SET', KEYS[1], 1)
end
4 ) 性能监控体系
// 注入Prometheus指标
@Counter({ name: 'luascriptexec_count', help: 'Lua脚本执行计数' })
execCounter: Counter<string>;
async executeScript(script: string, ...args: any[]) {
const endTimer = this.execTimer.startTimer();
try {
const result = await this.client.evalsha(...);
this.execCounter.inc();
return result;
} catch (e) {
this.errorCounter.inc();
throw e;
} finally {
endTimer();
}
}
5 ) 键的规范使用
--- 正确的键使用方式
--- 所有键都应该通过KEYS数组传递
local key1 = KEYS[1]
local key2 = KEYS[2]
--- 错误的方式(在阿里云Redis中会报错)
local keyName = 'user:' .. ARGV[1]
redis.call('GET', keyName)
6 )大Key分片策略
超过10KB的Hash拆分为多个子Key
使用HSCAN迭代处理替代全量获取
7 )集群环境下的脚本处理
所有节点需缓存相同脚本(通过 SCRIPT LOAD 预分发)
8 ) 安全最佳实践
禁用全局变量防止污染:local log = redis.log
参数校验避免注入:tonumber(ARGV[1]) 显式类型转换
典型应用场景扩展
| 场景 | Lua优势 | NestJS集成范例 |
|---|---|---|
| 秒杀库存扣减 | 原子性避免超卖 | 网关层限流+库存服务 |
| API配额管理 | 滑动窗口精准计数 | 全局请求拦截器 |
| 分布式ID生成 | 避免重复ID的原子递增 | 微服务公共模块 |
| 实时排行榜更新 | ZSET操作与业务逻辑结合 | WebSocket事件推送 |
性能与安全
1 ) 超时控制
设置lua-time-limit(默认5秒),超时脚本可通过 SCRIPT KILL 终止
2 ) 沙箱环境限制
禁用危险函数(如os.execute());
输入校验防注入:过滤非预期参数
安全考虑与漏洞防护
1 ) 常见安全漏洞
CVE-2025-49844:Redis Lua脚本远程代码执行漏洞,允许经过认证的攻击者通过精心构造的恶意Lua脚本触发内存损坏,导致任意代码执行
释放后重用(Use-After-Free):攻击者可操控垃圾回收机制触发该漏洞
2 )安全最佳实践
--- 输入验证
local input = ARGV[1]
if not input or input == '' then
return error('Invalid input')
end
---限制执行时间
Redis默认lua-time-limit为5000毫秒
--- 避免编写耗时过长的脚本
--- 避免使用全局变量
---所有变量都应使用local声明
local safeVar = 'safe'
3 )版本更新建议
受影响版本及修复版本:
Redis 6.2.20 → 升级至最新版本
Redis 7.2.0-7.2.11 → 升级至7.2.12+
Redis 7.4.0-7.4.6 → 升级至7.4.7+
Redis 8.0.0-8.0.4 → 升级至8.0.5+
Redis 8.2.0-8.2.2 → 升级至8.2.3+
总结
Redis Lua 脚本通过原子性+高性能成为分布式系统核心组件,结合 NestJS 的模块化与 TypeScript 类型安全,可构建高可靠服务。关键点:
- 生产级代码结构:封装 RedisLuaService 统一管理脚本;
- 性能瓶颈规避:使用 EVALSHA 并监控脚本执行时间;
- 扩展性设计:适配 Redis 集群与 Pipeline 优化
Redis Lua脚本通过将计算逻辑下沉至数据层,解决了分布式环境下的原子性与一致性问题
结合NestJS的依赖注入、AOP等特性,可构建出高并发、易维护的云原生应用
建议在复杂事务、高频计数等场景优先采用此方案,性能提升可达3-5倍
- 完整代码参考:NestJS+Redis 集成示例
- 深度原理拓展:《Redis 设计与实现》
- Redis Lua脚本库

被折叠的 条评论
为什么被折叠?



