2555 字
约 8 分钟
7
简单的 Hello World 脚本

Redis 脚本概述

Redis 从 2.6 版本开始支持 Lua 脚本,允许用户在 Redis 服务器端执行复杂的逻辑操作。Lua 脚本在 Redis 中具有原子性,整个脚本作为一个整体执行,不会被其他命令打断。这使得 Lua 脚本成为实现复杂业务逻辑、保证数据一致性的强大工具。

Redis Lua 脚本特性

⚛️

原子性执行

整个脚本作为一个原子操作执行,不会被其他命令中断
🚀

高性能

在服务器端执行,减少网络往返,提高执行效率
🔒

数据一致性

保证复杂操作的数据一致性,避免竞态条件
💾

脚本缓存

脚本会被缓存,重复执行时只需传递 SHA1 值
🔧

灵活性

支持复杂的条件判断和循环逻辑
🌐

跨平台

Lua 语言简单易学,跨平台兼容性好

1. 编写脚本

使用 Lua 语言编写业务逻辑
→

2. 执行脚本

使用 EVAL 命令执行脚本
→

3. 脚本缓存

Redis 自动缓存脚本
→

4. 重复执行

使用 EVALSHA 快速执行

🛠️ 脚本基本命令

Redis 脚本的核心命令及其使用方法:

命令 语法 描述 返回值
EVAL EVAL script numkeys key [key ...] arg [arg ...] 执行 Lua 脚本 脚本返回值
EVALSHA EVALSHA sha1 numkeys key [key ...] arg [arg ...] 通过 SHA1 值执行缓存的脚本 脚本返回值
SCRIPT LOAD SCRIPT LOAD script 加载脚本到缓存,返回 SHA1 脚本的 SHA1 值
SCRIPT EXISTS SCRIPT EXISTS sha1 [sha1 ...] 检查脚本是否存在于缓存中 存在状态数组
SCRIPT FLUSH SCRIPT FLUSH 清空脚本缓存 OK
SCRIPT KILL SCRIPT KILL 终止正在执行的脚本 OK

基本脚本示例:

# 简单的 Hello World 脚本
EVAL "return 'Hello, Redis Lua!'" 0
# 返回:"Hello, Redis Lua!"

# 获取键值的脚本
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# 返回:mykey 的值

# 设置键值的脚本
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "Hello World"
# 返回:OK

# 条件设置脚本
local script = [[
    local key = KEYS[1]
    local value = ARGV[1]
    local current = redis.call('GET', key)
    if current == false then
        redis.call('SET', key, value)
        return 1
    else
        return 0
    end
]]

EVAL script 1 "newkey" "newvalue"
# 返回:1 (设置成功) 或 0 (键已存在)

脚本缓存示例:

# 加载脚本到缓存
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 返回:"4e6d8fc8bb01276962cce5371fa795a7763657ae"

# 检查脚本是否存在
SCRIPT EXISTS "4e6d8fc8bb01276962cce5371fa795a7763657ae"
# 返回:1) (integer) 1

# 使用 SHA1 执行脚本
EVALSHA "4e6d8fc8bb01276962cce5371fa795a7763657ae" 1 mykey
# 返回:mykey 的值

# 清空脚本缓存
SCRIPT FLUSH
# 返回:OK

# 再次检查脚本
SCRIPT EXISTS "4e6d8fc8bb01276962cce5371fa795a7763657ae"
# 返回:1) (integer) 0

📚 Lua 语法基础

在 Redis 中使用的 Lua 语法要点:

变量和数据类型:

-- 变量声明local name = "Redis"local count = 100
local flag = truelocal arr = {1, 2, 3, 4, 5}
local obj = {name="Redis", version="7.0"}

-- 字符串操作local str1 = "Hello"local str2 = "World"local result = str1 .. " " .. str2 -- 字符串连接-- 数值操作local a = 10
local b = 20
local sum = a + b
local product = a * b

条件判断和循环:

-- 条件判断if count > 50 thenreturn"High"elseif count > 20 thenreturn"Medium"elsereturn"Low"end-- for 循环for i = 1, 10 do
redis.call('INCR', 'counter')
end-- while 循环local i = 1
while i <= 5 do
redis.call('LPUSH', 'list', i)
i = i + 1
end-- 遍历数组for index, value inipairs(arr) do
redis.call('SET', 'key' .. index, value)
end

Redis 命令调用:

-- redis.call() - 严格模式,出错时停止执行local value = redis.call('GET', 'mykey')
if value == falsethen
redis.call('SET', 'mykey', 'default')
end-- redis.pcall() - 保护模式,出错时返回错误信息local result = redis.pcall('INCR', 'string_key')
iftype(result) == 'table' and result.err thenreturn"Error: " .. result.err
end-- 批量操作
redis.call('MULTI')
redis.call('SET', 'key1', 'value1')
redis.call('SET', 'key2', 'value2')
redis.call('EXEC')

KEYS 和 ARGV 参数:

-- KEYS[1], KEYS[2], ... 访问键参数-- ARGV[1], ARGV[2], ... 访问值参数local key1 = KEYS[1] -- 第一个键local key2 = KEYS[2] -- 第二个键local value1 = ARGV[1] -- 第一个参数local value2 = ARGV[2] -- 第二个参数-- 获取参数数量local key_count = #KEYS
local arg_count = #ARGV

-- 遍历所有键for i = 1, #KEYS do
redis.call('DEL', KEYS[i])
end

🎯 实际应用案例

Redis Lua 脚本在实际项目中的典型应用:

🔒 分布式锁

-- 获取锁脚本
local lock_script = [[
    local key = KEYS[1]
    local identifier = ARGV[1]
    local expire = ARGV[2]
    
    local result = redis.call('SET', key, identifier, 'EX', expire, 'NX')
    if result then
        return 1
    else
        return 0
    end
]]

-- 释放锁脚本
local unlock_script = [[
    local key = KEYS[1]
    local identifier = ARGV[1]
    
    local current = redis.call('GET', key)
    if current == identifier then
        return redis.call('DEL', key)
    else
        return 0
    end
]]

-- 使用示例
EVAL lock_script 1 "lock:resource" "client123" 30
EVAL unlock_script 1 "lock:resource" "client123"

**优势:**原子性操作,避免锁的误删除

📊 限流器

-- 滑动窗口限流脚本
local rate_limit_script = [[
    local key = KEYS[1]
    local window = tonumber(ARGV[1])  -- 时间窗口(秒)
    local limit = tonumber(ARGV[2])   -- 限制次数
    local current_time = tonumber(ARGV[3])
    
    -- 清理过期记录
    redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)
    
    -- 获取当前窗口内的请求数
    local current_requests = redis.call('ZCARD', key)
    
    if current_requests < limit then
        -- 添加当前请求
        redis.call('ZADD', key, current_time, current_time)
        redis.call('EXPIRE', key, window)
        return {1, limit - current_requests - 1}
    else
        return {0, 0}
    end
]]

-- 使用示例
local current_time = os.time()
EVAL rate_limit_script 1 "rate_limit:user123" 60 10 current_time

**优势:**精确的滑动窗口限流,高性能

💰 库存扣减

-- 原子性库存扣减脚本
local stock_deduct_script = [[
    local stock_key = KEYS[1]
    local order_key = KEYS[2]
    local quantity = tonumber(ARGV[1])
    local order_id = ARGV[2]
    
    -- 获取当前库存
    local current_stock = tonumber(redis.call('GET', stock_key) or 0)
    
    -- 检查库存是否足够
    if current_stock >= quantity then
        -- 扣减库存
        redis.call('DECRBY', stock_key, quantity)
        -- 记录订单
        redis.call('SADD', order_key, order_id)
        -- 返回成功和剩余库存
        return {1, current_stock - quantity}
    else
        -- 返回失败和当前库存
        return {0, current_stock}
    end
]]

-- 使用示例
EVAL stock_deduct_script 2 "stock:product123" "orders:product123" 5 "order456"

**优势:**防止超卖,保证库存准确性

🎲 抽奖系统

-- 加权随机抽奖脚本
local lottery_script = [[
    local user_key = KEYS[1]
    local prizes_key = KEYS[2]
    local user_id = ARGV[1]
    local max_draws = tonumber(ARGV[2])
    
    -- 检查用户抽奖次数
    local user_draws = tonumber(redis.call('GET', user_key) or 0)
    if user_draws >= max_draws then
        return {0, "已达到最大抽奖次数"}
    end
    
    -- 获取奖品配置
    local prizes = redis.call('HGETALL', prizes_key)
    local total_weight = 0
    local prize_list = {}
    
    -- 计算总权重
    for i = 1, #prizes, 2 do
        local prize_name = prizes[i]
        local weight = tonumber(prizes[i + 1])
        total_weight = total_weight + weight
        table.insert(prize_list, {prize_name, weight})
    end
    
    -- 生成随机数
    math.randomseed(os.time() + user_draws)
    local random_num = math.random(1, total_weight)
    
    -- 确定中奖奖品
    local current_weight = 0
    for _, prize in ipairs(prize_list) do
        current_weight = current_weight + prize[2]
        if random_num <= current_weight then
            -- 增加用户抽奖次数
            redis.call('INCR', user_key)
            redis.call('EXPIRE', user_key, 86400)  -- 24小时过期
            return {1, prize[1]}
        end
    end
    
    return {0, "抽奖失败"}
]]

-- 使用示例
EVAL lottery_script 2 "draws:user123" "prizes:config" "user123" 3

**优势:**公平抽奖,防止作弊,支持加权随机

📈 排行榜更新

-- 排行榜更新脚本
local leaderboard_script = [[
    local board_key = KEYS[1]
    local user_key = KEYS[2]
    local user_id = ARGV[1]
    local score = tonumber(ARGV[2])
    local max_size = tonumber(ARGV[3])
    
    -- 获取用户当前分数
    local current_score = redis.call('ZSCORE', board_key, user_id)
    
    -- 只有分数更高时才更新
    if not current_score or score > tonumber(current_score) then
        -- 更新排行榜
        redis.call('ZADD', board_key, score, user_id)
        
        -- 限制排行榜大小
        local board_size = redis.call('ZCARD', board_key)
        if board_size > max_size then
            redis.call('ZREMRANGEBYRANK', board_key, 0, board_size - max_size - 1)
        end
        
        -- 更新用户最高分
        redis.call('SET', user_key, score)
        
        -- 获取用户排名
        local rank = redis.call('ZREVRANK', board_key, user_id)
        return {1, rank + 1, score}
    else
        local rank = redis.call('ZREVRANK', board_key, user_id)
        return {0, rank and (rank + 1) or -1, current_score}
    end
]]

-- 使用示例
EVAL leaderboard_script 2 "leaderboard:game" "user:123:best" "user123" 9500 100

**优势:**高效的排行榜维护,支持分数比较

🔄 缓存更新

-- 缓存更新脚本
local cache_update_script = [[
    local cache_key = KEYS[1]
    local lock_key = KEYS[2]
    local data = ARGV[1]
    local ttl = tonumber(ARGV[2])
    local lock_ttl = tonumber(ARGV[3])
    
    -- 尝试获取更新锁
    local lock_result = redis.call('SET', lock_key, '1', 'EX', lock_ttl, 'NX')
    
    if lock_result then
        -- 获取锁成功,更新缓存
        redis.call('SET', cache_key, data, 'EX', ttl)
        redis.call('DEL', lock_key)
        return {1, "缓存更新成功"}
    else
        -- 获取锁失败,检查缓存是否存在
        local cache_exists = redis.call('EXISTS', cache_key)
        if cache_exists == 1 then
            return {0, "缓存已存在,跳过更新"}
        else
            return {-1, "等待其他进程更新缓存"}
        end
    end
]]

-- 使用示例
EVAL cache_update_script 2 "cache:user:123" "lock:cache:user:123" "user_data" 3600 10

**优势:**防止缓存击穿,避免重复更新

脚本性能和最佳实践

方面 优点 缺点 最佳实践
原子性 完全原子性执行 长脚本会阻塞服务器 控制脚本执行时间
性能 减少网络往返 复杂逻辑影响性能 优化算法复杂度
缓存 脚本自动缓存 内存占用增加 定期清理无用脚本
调试 支持错误返回 调试相对困难 充分测试和日志记录

⚠️ 注意事项:执行时间:避免编写执行时间过长的脚本内存使用:注意脚本中的内存分配,避免内存泄漏随机性:脚本中的随机数生成需要注意种子设置错误处理:使用 pcall 处理可能出错的操作键的访问:只能访问通过 KEYS 参数传递的键

✅ 最佳实践建议:

  • 脚本设计:保持脚本简洁,单一职责
  • 参数验证:在脚本开始时验证输入参数
  • 错误处理:合理使用 redis.call 和 redis.pcall
  • 性能优化:避免不必要的循环和复杂计算
  • 版本管理:为脚本建立版本管理机制
  • 测试覆盖:编写充分的测试用例
  • 监控告警:监控脚本执行时间和错误率

💡 使用建议:适用场景:复杂的原子性操作、条件判断、批量处理不适用场景:简单的单命令操作、长时间运行的任务替代方案:简单操作使用事务,复杂业务逻辑考虑应用层处理调试技巧:使用 redis.log 输出调试信息

← 上一章:Redis 事务 下一章:Redis 连接 →

简单的 Hello World 脚本
https://clxhxhhr.top/posts/2836/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。