Lua: 在Redis数据库操作中的核心应用与实战技巧

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类型
INTEGERnumber
BULK REPLYstring
MULTI REPLYtable (数组)

Lua 类型Redis 返回类型示例
numberIntegerreturn 3.14 → 3
tableMulti-bulk replyreturn {1, ‘a’} → [1, “a”]
nilNullreturn 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 类型安全,可构建高可靠服务。关键点:

  1. 生产级代码结构:封装 RedisLuaService 统一管理脚本;
  2. 性能瓶颈规避:使用 EVALSHA 并监控脚本执行时间;
  3. 扩展性设计:适配 Redis 集群与 Pipeline 优化

Redis Lua脚本通过将计算逻辑下沉至数据层,解决了分布式环境下的原子性与一致性问题
结合NestJS的依赖注入、AOP等特性,可构建出高并发、易维护的云原生应用
建议在复杂事务、高频计数等场景优先采用此方案,性能提升可达3-5倍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值