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交互
来源: