Lua语言基础

什么是Lua

Lua是一种轻量级、高效的脚本语言,设计初衷是作为嵌入式语言与其他程序配合使用,语法简单,容易学习,功能强大

基本语法

变量与数据类型

Lua中的变量不需要声明类型,主要有以下几种数据类型

-- 数字
local num = 10
​
-- 字符串
local str = "Hello"
​
-- 布尔值
local is_valid = true
​
-- 表(Lua中的数组和对象)
local arr = {1, 2, 3, 4}
local obj = {name = "John", age = 30}
​
-- nil(表示没有值)
local empty = nil
​

注意, -- 是Lua中的注释符号

条件语句

if condition then
    -- 代码块
elseif another_condition then
    -- 代码块
else
    -- 代码块
end
​

循环

-- for循环
for i = 1, 10 do
    -- 代码块
end
​
-- while循环
while condition do
    -- 代码块
end
​
-- 遍历表
for key, value in pairs(table_name) do
    -- 代码块
end
​

函数

-- 函数定义
function add(a, b)
    return a + b
end
​
-- 调用函数
local result = add(5, 3)  -- result = 8
​

Lua中的表(Table)

表是Lua中最重要也是唯一的数据结构,可以用来表示数组、字典、对象等

-- 创建表
local my_table = {}
​
-- 作为数组使用
my_table[1] = "first"
my_table[2] = "second"
​
-- 作为字典使用
my_table["name"] = "John"
my_table["age"] = 30
​
-- 简写形式
local person = {
    name = "John",
    age = 30,
    ["favorite color"] = "blue"  -- 带空格的键需要用方括号
}
​
-- 遍历表
for key, value in pairs(person) do
    print(key, value)
end
​

Redis中的Lua脚本

为什么在Redis中使用Lua呢

Redis中使用Lua脚本有以下几个主要优势

  • 原子性执行:Lua脚本在Redis中是原子执行的,不会被其他命令打断
  • 减少网络往返:多个操作可以合并成一个脚本,减少客户端和服务器之间的通信
  • 可复用性:可以将常用的操作封装成脚本,方便重复使用
  • 条件判断:可以在脚本中进行逻辑判断,实现更复杂的功能

Redis中执行Lua脚本的基本命令

EVAL script numkeys key [key ...] arg [arg ...]
​
  • script:Lua脚本内容
  • numkeys:键名参数的数量
  • key:键名参数列表
  • arg:附加参数列表

示例

EVAL "return redis.call('GET', KEYS[1])" 1 mykey
​

在Lua脚本中访问Redis

在Lua脚本中,可以通过redis.call()或redis.pcall()函数调用Redis命令

-- 获取键值
local value = redis.call("GET", KEYS[1])
​
-- 设置键值
redis.call("SET", KEYS[1], ARGV[1])
​

区别

  • redis.call():如果Redis命令执行出错,脚本会终止并返回错误
  • redis.pcall():如果Redis命令执行出错,脚本会继续执行,返回错误信息

传递参数给Lua脚本

在Redis中执行Lua脚本时,可以传递两种参数

  • KEYS参数:用于传递键名,在脚本中通过KEYS[index]访问
  • ARGV参数:用于传递其他值,在脚本中通过ARGV[index]访问
EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
​

在上面的例子中

  • KEYS[1] 是 “key1”
  • KEYS[2] 是 “key2”
  • ARGV[1] 是 “arg1”
  • ARGV[2] 是 “arg2”

EVAALSHA命令

为了避免每次都传输完整的脚本内容,Redis提供了EVALSHA命令,它通过脚本的SHA1摘要来执行已经加载的脚本

SCRIPT LOAD "return redis.call('GET', KEYS[1])"
​

常见应用场景

原子性计数器和限流器

使用Lua脚本可以实现复杂的计数逻辑,确保在高并发情况下的正确性

-- 限流器:每个用户每分钟最多请求N次
local user_id = KEYS[1]
local max_requests = tonumber(ARGV[1])
local window_size = tonumber(ARGV[2]) -- 时间窗口,单位:秒
​
local key = "rate_limit:" .. user_id
local current = redis.call("GET", key)
​
if current == false then
    -- 第一次请求
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key, window_size)
    return 1
elseif tonumber(current) < max_request then
    -- 为超过限制
    redis.call("INCR", key)
    return tonumber(current) + 1
else
    -- 超过限制
    return 0
end

防止超卖

在高并发电商场景中,使用Lua脚本可以确保库存不会出现负数

-- 检查并减少库存
local product_id = KEYS[1]
local quantity = tonumber(ARGV[1])
​
local stock_key = "stock:" .. product_id
local current_stock = redis.call("GET", stock_key)
​
if current_stock == false then
    retuen -1 -- 商品不存在
end
​
current_stock = tonumber(current_stock)
​
if current_stock >= quantity then
    -- 库存充足,减少库存
    redis.call("DECRBY", stock_key, quantity)
    return current_stock - quantity
else
    -- 库存不足
    return -2
end

分布式锁

使用Lua脚本实现分布式锁的获取和释放

-- 获取锁
local lock_key = KEYS[1]
local lock_value = ARGV[1]  -- 通常是唯一的客户端ID
local ttl = tonumber(ARGV[2])
​
-- NX表示键不存在时才设置,PX设置过期时间(毫秒)
local result = redis.call("SET", lock_key, lock_value, "NX", "PX", ttl)
​
if result == false then
    return 0  -- 获取锁失败
else
    return 1  -- 获取锁成功
end
​
-- 释放锁(确保只有锁的持有者才能释放)
local lock_key = KEYS[1]
local lock_value = ARGV[1]  -- 客户端ID
​
local current_value = redis.call("GET", lock_key)
if current_value == lock_value then
    return redis.call("DEL", lock_key)
else
    return 0
end
​

缓存操作

使用Lua脚本实现复杂的缓存逻辑

-- 获取缓存,如果不存在则从指定位置加载
local cache_key = KEYS[1]
local source_key = KEYS[2]
​
local cached_value = redis.call("GET", cache_key)
if cached_value then
    -- 更新访问时间
    redis.call("EXPIRE", cache_key, 3600)  -- 一小时过期
    return cached_value
else
    -- 从源加载
    local source_value = redis.call("GET", source_key)
    if source_value then
        redis.call("SET", cache_key, source_value, "EX", 3600)
        return source_value
    else
        return nil
    end
end
​

Lua脚本示例

购物车管理

-- 添加商品到购物车并检查限购
-- KEYS[1]: 用户购物车的key(例如:cart:123)
-- KEYS[2]: 商品限购信息的key(例如:product:limit:456)
-- ARGV[1]: 商品ID
-- ARGV[2]: 添加数量
​
local cart_key = KEYS[1]
local limit_key = KEYS[2]
local product_id = ARGV[1]
local quantity = tonumber(ARGV[2])
​
-- 检查商品是否有购买限制
local limit = redis.call("GET", limit_key)
if limit then
    limit = tonumber(limit)
    
    -- 获取当前购物车中该商品数量
    local current = redis.call("HGET", cart_key, product_id)
    current = current and tonumber(current) or 0
    
    -- 检查是否超出限购
    if current + quantity > limit then
        return {err = "LIMIT_EXCEEDED", current = current, limit = limit}
    end
end
​
-- 添加到购物车
redis.call("HINCRBY", cart_key, product_id, quantity)
-- 设置购物车过期时间(24小时)
redis.call("EXPIRE", cart_key, 86400)
​
-- 返回更新后的数量
return redis.call("HGET", cart_key, product_id)
​

排行榜更新与查询

-- 更新用户得分并返回排名
-- KEYS[1]: 排行榜key(例如:leaderboard:game:123)
-- ARGV[1]: 用户ID
-- ARGV[2]: 得分
​
local leaderboard_key = KEYS[1]
local user_id = ARGV[1]
local score = tonumber(ARGV[2])
​
-- 获取用户当前得分
local current_score = redis.call("ZSCORE", leaderboard_key, user_id)
current_score = current_score and tonumber(current_score) or 0
​
-- 只在新分数更高时更新
if score > current_score then
    redis.call("ZADD", leaderboard_key, score, user_id)
    
    -- 获取用户新排名(从高到低)
    local rank = redis.call("ZREVRANK", leaderboard_key, user_id)
    
    -- 返回结果
    return {
        old_score = current_score,
        new_score = score,
        rank = rank + 1,  -- Redis排名从0开始
        updated = 1
    }
else
    -- 获取用户当前排名
    local rank = redis.call("ZREVRANK", leaderboard_key, user_id)
    
    return {
        old_score = current_score,
        new_score = current_score,
        rank = rank and (rank + 1) or nil,
        updated = 0
    }
end
​

会话管理

-- 更新用户会话并检查是否在线
-- KEYS[1]: 用户会话key(例如:session:user:123)
-- KEYS[2]: 在线用户集合key(例如:online:users)
-- ARGV[1]: 用户ID
-- ARGV[2]: 会话数据(JSON字符串)
-- ARGV[3]: 会话过期时间(秒)
​
local session_key = KEYS[1]
local online_key = KEYS[2]
local user_id = ARGV[1]
local session_data = ARGV[2]
local expire_time = tonumber(ARGV[3])
​
-- 检查会话是否存在
local exists = redis.call("EXISTS", session_key)
​
-- 更新会话数据
redis.call("SET", session_key, session_data, "EX", expire_time)
​
-- 如果是新会话,将用户添加到在线集合
if exists == 0 then
    redis.call("SADD", online_key, user_id)
end
​
-- 更新在线用户集合的过期时间
redis.call("EXPIRE", online_key, expire_time * 2)
​
-- 返回在线用户数
return redis.call("SCARD", online_key)
​

最佳实践与性能考虑

最佳实践

保持脚本简短

  • 复杂逻辑应分解为多个小脚本
  • 避免在脚本中执行耗时操作

使用KEYS和ARGV

  • 将所有键名放在KEYS中,其他参数放在ARGV中
  • 不要在脚本中拼接键名,而应作为参数传入

预加载脚本

  • 使用SCRIPT LOAD预加载脚本,然后通过EVALSHA执行
  • 这样可以减少网络传输开销

错误处理

  • 尽量使用redis.pcall()而不是redis.call(),以便更好地处理错误
  • 返回结构化错误信息,便于客户端理解

避免无限循环

  • Lua脚本有执行时间限制,默认为5秒
  • 避免编写可能陷入无限循环的脚本

结构化返回值

返回表或嵌套表,便于客户端解析结果

return {success = 1, data = {count = 10, items = {...}}}
​

性能考虑

避免大量数据操作

  • Lua脚本执行期间,Redis会阻塞其他命令
  • 避免在脚本中处理大量数据

使用合适的数据结构

  • 根据场景选择合适的Redis数据结构
  • 例如排行榜用有序集合,计数器用字符串

控制时间复杂度

  • 注意Redis命令的时间复杂度
  • 避免在脚本中使用O(N)复杂度的命令,如KEYS、SMEMBERS等
  • 可以使用SCAN系列命令替代

批量处理

  • 使用批量命令如MGET、HMGET减少脚本中的命令次数
  • 例如,使用HMGET一次获取多个哈希字段,而不是多次HGET

脚本复用

  • 相似功能的脚本应考虑合并并通过参数控制行为
  • 这样可以减少服务器上缓存的脚本数量

常见问题与解答

Lua脚本执行超时怎么办?

Redis默认限制Lua脚本的执行时间为5秒,如果脚本执行超过这个时间,Redis将

  • 记录警告日志
  • 开始接受CLIENT KILL和SHUTDOWN NOSAVE命令
  • 继续执行脚本直到完成或被手动终止

解决方案:

  • 优化脚本,减少操作数量
  • 分解成多个小脚本分步执行
  • 使用KEYS的替代方案如SCAN来处理大量数据

脚本中如何处理Redis返回的不同数据类型?

-- 字符串转数字
local str = redis.call("GET", "mykey")
local num = tonumber(str) or 0  -- 如果str为nil或转换失败,使用默认值0
​
-- 处理列表
local list = redis.call("LRANGE", "mylist", 0, -1)
for i, v in ipairs(list) do
    -- 处理每个元素
end
​
-- 处理哈希表
local hash = redis.call("HGETALL", "myhash")
-- Redis返回的是扁平数组: {key1, value1, key2, value2, ...}
local result = {}
for i = 1, #hash, 2 do
    result[hash[i]] = hash[i+1]
end
​

如何在Lua脚本中生成唯一ID?

-- 使用时间戳加随机数
local timestamp = redis.call("TIME")
local seconds = timestamp[1]
local microseconds = timestamp[2]
local random = math.random(1000, 9999)
return seconds .. microseconds .. random
​
-- 或者使用Redis的自增计数器
local counter = redis.call("INCR", "global:counter")
return counter
​

如何在脚本中处理JSON数据?

-- 假设我们已经从客户端获取了解析后的值
local user_id = ARGV[1]
local product_id = ARGV[2]
local quantity = tonumber(ARGV[3])
​
-- 构建简单的JSON响应
local json_result = '{"status":"success","user_id":"' .. user_id .. '","quantity":' .. quantity .. '}'
return json_result
​

脚本中能否调用外部API或执行系统命令?

不能

出于安全考虑,Redis中的Lua脚本环境是沙箱化的,不允许

  • 调用外部API
  • 执行系统命令
  • 加载外部Lua模块
  • 访问文件系统
  • 访问网络

脚本只能通过redis.call()或redis.pcall()与Redis交互

来源:

最后修改:2025 年 08 月 07 日
如果觉得我的文章对你有用,请随意赞赏