[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);
}
在hash和zset 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 参考文献
本文作者:
千千寰宇
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号