[Redis] Redis (6) Lua 脚本 [转]

概述: Redis Lua 脚本

介绍

  • lua脚本解决什么问题?

(1)与多次发送redis指令相比,lua脚本可以合并指令、减少网络开销
(2)原子操作。在Lua脚本中会将整个脚本做一个一个整体执行,不会被打断。
(3)复用和封装。对于一些通用的功能,通过lua脚本封装能很好的复用。

lua脚本与事务

Lua脚本本身也可以看作为一种事务,而且使用脚本起来更简单,并且可控制的流程更灵活。

案例

lua脚本使用举例

在hash和zset两个key中删除指定条数的数据项

zset_key:删除从小到大排序的前count条数据;
hash_key:删除count个字段,字段名由前面zset获取。

  • 对应lua脚本及解释:
if KEYS[1] == nil then return {err='[PARAM ERR] hash KEYS[1] empty error'} end  //hash_key
if KEYS[2] == nil then return {err='[PARAM ERR] zset KEYS[2] empty error'} end  //zset_key
if ARGV[1] == nil then return {err='[PARAM ERR] hash ARGV[1] empty error'} end  //删除条数
local key_exists = tonumber(redis.call('EXISTS', KEYS[1], KEYS[2]))             //看看这个key有几个key实际存在
if key_exists ~= 2 then return {err=string.format('[NOTEXIST ERR] KEYS size(%d) is not even', #KEYS)}  //如果不是两个key,则直接报错
if ((#KEYS % 2) ~= 0) then return {err=string.format('KEYS size(%d) is not even', #KEYS)} end          //再校验下key的个数,如果不是偶数也直接返回错误提示
local not_empty = function(x) return (type(x) == "table") and (not x.err) and (#x ~= 0) end            //这里定义了一个判空的函数,函数名是 not_empty().
local need_del_count = tonumber(ARGV[1])  //要删除的条数
local need_del_ret = redis.call('zrangebyscore', KEYS[2], '-inf', '+inf', 'LIMIT', 0, need_del_count)  //获取排序(时间戳)最小的count条需要删除的数据
if not_empty(need_del_ret) then
    local del_hash_count = redis.call('hdel', KEYS[1], unpack(need_del_ret)) //删除hash
    local del_zset_count = redis.call('zrem', KEYS[2], unpack(need_del_ret)) //删除zset
end
return need_del_ret

调用代码

    std::vector<std::string> keys;
    keys.push_back(str_hash_key);  
    keys.push_back(str_zset_key);
 
    std::vector<std::string> argv;
    argv.push_back(std::to_string(del_amount));
 
    //std::vector<std::string> result;
    int ret = EvalSha(g_evalsha_delete_last_list_item, keys, argv, &deleted_item, retry);
    if (ERR_REDIS_NO_SCRIPT == ret)
    {
        // 如果lua脚本未加载,则进行懒加载
        LOGSYS_WATER(_LC_WARNING_, "no script, load again, ret:%d, evalsha:%s", ret, g_evalsha_delete_last_list_item.c_str());
        ret = ScriptLoad(g_lua_delete_last_list_item, g_evalsha_delete_last_list_item, NET_HELPER_RPC_DEFAULT_RETRYTIME_TWO);
        ret |= EvalSha(g_evalsha_delete_last_list_item, keys, argv, &deleted_item,  retry);
    }

hashzset 2个key中删除指定field/member的数据

在hash_key中的删除指定的field值的项; —— 删除特定{channel}{cid}列表。
在zset_key中删除特定member值的项;—— 删除特定{channel}
列表。

if KEYS[1] == nil then return {err='[PARAM ERR] hash KEYS[1] empty error'} end   // 参数校验, 如果没有hash_key报错
if KEYS[2] == nil then return {err='[PARAM ERR] zset KEYS[2] empty error'} end   // 参数校验, 如果没有zset_key也报错
if #ARGV == 0 then return {err='[PARAM ERR] field empty error'} end             // 要删除的 {channel}_{cid};此处 #ARGV 应该就是数组size。
local key_exists = tonumber(redis.call('EXISTS', KEYS[1], KEYS[2]))             // 查询这个key,究竟存在几个key
if key_exists ~= 2 then return {err=string.format('[NOTEXIST ERR] KEYS size(%d) is not even', #KEYS)}  //如果不是两个key都存在那就报错
local bulk = {}             //table是lua易总数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。此处初始化表 bulk。
for i=1, #ARGV do table.insert(bulk, ARGV[i]) end   //遍历 {chanel}_{cid} 列表,并插入 bulk。
local del_hash_count = redis.call('hdel', KEYS[1], unpack(bulk))  //unpack接受一个数组(table)作为参数,并默认从下标1开始返回数组的所有元素。
local del_zset_count = redis.call('zrem', KEYS[2], unpack(bulk))  //lua5.1中,unpack是全局函数 可以直接使用;5.2中unpack被移动到table.unpack。
if del_hash_count ~= del_zset_count then return {err='[DATA NOT CONSISTENE ERR] field empty error'} end
return del_hash_count

调用函数:

int RedisMyListHelper::DelListItemByKey(const std::string& str_hash_key, const std::string& str_zset_key, const std::vector<std::string>& subkeys,  std::vector<std::string>& result, int retry)
{
    if(str_hash_key.empty() || str_zset_key.empty() || subkeys.empty())
    {
        return ERR_REDIS_INVALID_PARAM;
    }
 
    std::vector<std::string> keys;
    keys.push_back(str_hash_key);
    keys.push_back(str_zset_key);
 
    // std::vector<std::string> argv;
    // argv.push_back(std::to_string(size));
    // if(!str_last_subkey.empty())
    // {
    //     argv.push_back(str_last_subkey);
    // }
 
    int ret = EvalSha(g_evalsha_delete_list_item_by_key, keys, subkeys, &result, retry);
    if (ERR_REDIS_NO_SCRIPT == ret)
    {
        // 如果lua脚本未加载,则进行懒加载
        LOGSYS_WATER(_LC_WARNING_, "no script, load again, ret:%d, evalsha:%s", ret, g_evalsha_delete_last_list_item.c_str());
        ret = ScriptLoad(g_lua_del_list_item_by_key, g_evalsha_delete_list_item_by_key, NET_HELPER_RPC_DEFAULT_RETRYTIME_TWO);
        ret |= EvalSha(g_evalsha_delete_list_item_by_key, keys, subkeys, &result,  retry);
    }
 
    if (ERR_REDIS_KEY_NOT_EXIST == ret || ERR_REDIS_REPLY_IS_NULL == ret)
    {
        LOGSYS_WATER(_LC_ERROR_, "DelListItemByKey  not exist, hash_key:%s, zset_key:%s , ret:%d", str_hash_key.c_str(), str_zset_key.c_str(),  ret);
        return ret;
    }
    else if (0 != ret)
    {
        LOGSYS_WATER(_LC_ERROR_, "DelListItemByKey failed, hash_key:%s, zset_key:%s, ret:%d", str_hash_key.c_str(), str_zset_key.c_str(), ret);
        return ret;
    }
    LOGSYS_WATER(_LC_DEBUG_, "DelListItemByKey succeed,  hash_key:%s, zset_key:%s, ", str_hash_key.c_str(), str_zset_key.c_str());
    return 0;
}

redis插入数据指定版本号

  • redis本身不直接支持在插入数据时指定版本号,但可以通过一些方法实现类似的功能。

以下提供lua脚本获取key版本号及插入数据指定版本号的例子。

a. redis版本号匹配才更新

  • 使用Lua脚本来检查键key的当前版本号是否与期望的版本号expected_version相匹配。

如果匹配,则更新这个键的值为value;
如果不匹配或键不存在,脚本将不执行更新操作,返回false。

import redis
 
# 连接到Redis
r = redis.Redis(host='localhost', port=6379, db=0)
 
# Lua脚本
script = """
local key = KEYS[1]
local value = ARGV[1]
local expected_version = ARGV[2]
local current_version = redis.call('GET', key)
if current_version == expected_version then
    redis.call('SET', key, value)
    return true
else
    return false
end
"""

# 需要插入的键和值
key = 'mykey'
value = 'myvalue'
expected_version = '1'  # 假设期望的版本号是1
 
# 使用Lua脚本
result = r.eval(script, 1, key, value, expected_version)
 
print(result)  # 如果版本号匹配,返回true;如果不匹配或出现错误,返回false。

b. redis通过lua脚本插入数据、并指定版本号

  • 在这个脚本中,我们首先检查键是否已经存在。

如果不存在,我们使用SET命令来插入数据,并且使用HSET命令来设置版本号。
如果键已经存在,我们不做任何操作,并返回0。

-- Lua script to insert data with a specific version number
-- KEYS[1] is the key to insert the data
-- ARGV[1] is the value to insert
-- ARGV[2] is the version number
 
-- Check if the key already exists
if (redis.call('exists', KEYS[1]) == 1) then
    -- Key exists, don't overwrite
    return 0
else
    -- Key does not exist, insert the data with SET command
    redis.call('set', KEYS[1], ARGV[1])
    -- Set the version number with HSET command
    redis.call('hset', KEYS[1], 'version', ARGV[2])
    -- Return 1 to indicate the insertion was successful
    return 1
end

在Redis中执行这个Lua脚本的示例代码:

redis-cli --eval script.lua , mykey myvalue 1

注:上述 script.lua 是包含上述Lua脚本的文件,mkkey是要插入的键,myuvalue是要插入的值,1是版本号。
如果键已存在,脚本返回0;
如果键不存在,脚本插入数据并返回1。

Y 推荐文献

  • lua-redis

X 参考文献

posted @ 2025-05-25 07:29  千千寰宇  阅读(50)  评论(0)    收藏  举报