在学习本文之前,我是强烈建议大家了解一下我的其他两篇博客
Redis通过嵌入Lua解释器,实现了对Lua脚本的执行。在执行过程中,Redis保证了脚本的原子性和阻塞性,同时通过脚本缓存和访问限制来提高性能和安全性。这使得Lua脚本成为在Redis中实现复杂逻辑的有效手段
嵌入式Lua解释器:Redis内部集成了Lua解释器,这使得Redis可以直接执行Lua脚本。Lua是一种轻量级的脚本语言,性能较高,且易于嵌入到其他程序中。通过嵌入Lua解释器,Redis可以实现更复杂的逻辑,同时保持高性能。
原子性:Redis执行Lua脚本时,会保证脚本的原子性。这意味着在执行脚本期间,Redis不会处理其他客户端的请求。这样可以确保脚本执行过程中的数据一致性,避免并发问题。这个很关键,他保证了,我们执行命令的时候是原子执行,这个是实现分布式锁的前提。
阻塞性:由于Redis是单线程模型,执行Lua脚本时会阻塞其他客户端的请求。因此,为了保持Redis的高性能,需要确保Lua脚本的执行时间尽量短。如果脚本执行时间过长,可能会导致Redis性能下降。
脚本缓存:为了提高执行效率,Redis会缓存已执行过的Lua脚本。当客户端再次请求执行相同的脚本时,Redis可以直接从缓存中获取脚本,而无需重新加载和编译。这有助于减少脚本执行的开销。
限制访问:出于安全和性能的考虑,Redis对Lua脚本的访问权限进行了限制。在Lua脚本中,只能访问到传入的键和参数,不能访问Redis的全局状态。此外,脚本中不能执行一些可能导致阻塞或影响性能的命令,如BLPOP
、SUBSCRIBE
等。
Redis 执行Lua脚本的核心源码位于src/scripting.c
地址:https://github.com/redis/redis/blob/6.0/src/scripting.c
Redis嵌入Lua解释器是通过将Lua源码与Redis源码结合,进行封装和适配实现的。Redis通过注册特殊的Lua命令,并调用Lua解释器的API函数,实现了Lua脚本在Redis中的执行和与Redis的交互。这样,用户可以在Redis中编写复杂的Lua脚本,利用Lua的强大功能进行数据处理和业务逻辑的实现。
我们通过官网和源码浅析一下Redis可以执行Lua脚本的原理,其实和nginx 可以执行lua脚本的原理类似。
Lua源码和头文件:Redis使用的是Lua编程语言的官方实现,即LuaJIT或Lua。Redis的源代码中包含了Lua源码文件和相关的头文件,这些文件位于src/scripting/
目录下。
Redis对Lua的封装:Redis通过封装Lua的API,提供了一套与Redis命令交互的接口。这些封装函数位于src/scripting.c
文件中。
Redis命令的注册:Redis将Lua脚本作为一种特殊类型的命令进行处理。在src/scripting.c
文件中,Redis通过redisCommandTable
数组注册了与Lua相关的命令,例如EVAL
、EVALSHA
等。这些命令与Lua脚本的执行和管理相关。
Redis命令的执行:当执行Lua脚本相关的命令时,Redis首先会解析命令参数,获取Lua脚本的内容和参数。然后,Redis将Lua脚本传递给Lua解释器进行解析和执行。
Lua解释器的初始化:在Redis的服务器初始化过程中,会调用luaInit()
函数进行Lua解释器的初始化。该函数位于src/scripting.c
文件中。在初始化过程中,Redis会创建Lua解释器实例,并进行相关的设置和配置。
Lua脚本的执行:当执行Lua脚本相关的命令时,Redis会调用evalGenericCommand()
函数来处理命令。该函数位于src/scripting.c
文件中。在该函数中,Redis会将Lua脚本和参数传递给Lua解释器,并调用Lua的API执行脚本。
Redis与Lua的交互:在Lua脚本执行期间,Redis与Lua之间可以进行数据的交互。Redis提供了一些API函数,例如redis.call()
和redis.pcall()
,用于在Lua脚本中调用Redis命令。这些API函数允许Lua脚本直接访问Redis的键值存储和其他功能。
evalGenericCommand()
:执行Lua脚本相关的命令(如EVAL
和EVALSHA
)。该方法接收Lua脚本和参数,并将它们传递给Lua解释器进行执行。
luaCreateScriptingCommand()
:注册Lua脚本相关的命令。该方法通过调用Redis的命令注册函数,将Lua脚本命令添加到Redis的命令表中。
evalCommand()
:解析和执行EVAL
命令。该方法会解析命令参数,获取Lua脚本和参数,并将它们传递给evalGenericCommand()
方法进行执行。
loadCachedScript()
:加载缓存的Lua脚本。在执行EVALSHA
命令时,Redis会尝试从缓存中获取已加载的Lua脚本,以提高执行效率。
Redis的Lua脚本缓存机制主要包含以下步骤:
当一个Lua脚本被发送到Redis服务器并执行时,Redis不仅会执行这个脚本,同时它还会计算该脚本的SHA1哈希值,并将这个哈希值和对应的Lua脚本内容添加至缓存中。这个过程通常是在使用EVAL
命令执行脚本时自动完成的。
当我们再次执行相同的Lua脚本时,可以使用EVALSHA
命令,只需要发送之前计算出的SHA1哈希值,而无需再次发送整个脚本内容。
Redis会根据EVALSHA
命令中的哈希值在缓存中搜索对应的Lua脚本,如果找到则直接执行,如果未找到则返回错误。
通过这种方式,我们可以避免重复发送大型的Lua脚本,从而减少网络传输的开销。同时,因为Redis直接执行缓存中的脚本,也无需再次解析脚本,从而提高了执行效率。
注意,虽然Redis的缓存可以提高Lua脚本的执行效率,但是它并不会缓存脚本的执行结果。这意味着,即使是相同的Lua脚本,每次执行时都会根据当前的数据重新计算结果。当前Redis并没有提供直接的方式来查看缓存的Lua脚本,也没有提供直接的方式来清除脚本缓存。当我们需要修改脚本时,只能重新计算修改后的脚本的哈希值,并使用新的哈希值来执行脚本。
Lua脚本是在Redis服务器上执行的脚本,它可以通过使用Redis提供的特定命令和库函数来与Redis进行交互。以下是一些常见的Redis Lua脚本示例:
使用Lua脚本完成原子递增并设置过期时间,用于API请求限流。
local current = tonumber(redis.call('get', KEYS[1]) or "0") -- 获取当前计数
local max_limit = tonumber(ARGV[1]) -- 最大计数限制
if current + 1 > max_limit then -- 如果超过最大限制则返回0
return 0
else
redis.call('incrby', KEYS[1], ARGV[2]) -- 否则计数+1
redis.call('expire', KEYS[1], ARGV[3]) -- 设置过期时间
return current + 1
end
local element = redis.call('rpop', KEYS[1]) -- 从list1右侧pop出元素
if element then
redis.call('lpush', KEYS[2], element) -- 如果元素存在则push到list2
end
return element -- 返回被移动的元素
local old_val = redis.call('hget', KEYS[1], ARGV[1]) -- 获取旧值
redis.call('hset', KEYS[1], ARGV[1], ARGV[2]) -- 设置新值
return old_val -- 返回旧值
local exists = redis.call('exists', KEYS[1]) -- 检查键是否存在
if exists == 0 then
redis.call('set', KEYS[1], ARGV[1]) -- 如果不存在则设置键值
end
return exists -- 返回键是否存在
local exists = redis.call('sismember', KEYS[1], ARGV[1]) -- 检查值是否在set中
if exists == 0 then
redis.call('sadd', KEYS[1], ARGV[1]) -- 如果不存在则添加
end
return exists -- 返回值是否在set中
local count = 0
for i, member in ipairs(ARGV) do
count = count + redis.call('zrem', KEYS[1], member) -- 删除Sorted Set中的元素
end
return count -- 返回被删除元素的数量
for i = 1, #ARGV, 2 do
redis.call('hset', KEYS[1], ARGV[i], ARGV[i + 1]) -- 设置多个hash字段的值
end
return true
local keys = redis.call('keys', ARGV[1]) -- 列出所有具有特定前缀的键
for i, key in ipairs(keys) do
redis.call('del', key) -- 删除这些键
end
return keys -- 返回被删除的键
local size = redis.call('llen', KEYS[1]) -- 获取list的长度
if size < tonumber(ARGV[2]) then
redis.call('rpush', KEYS[1], ARGV[1]) -- 如果未超过最大数量则添加元素
end
return size -- 返回list的长度
redis.call('zadd', KEYS[1], ARGV[2], ARGV[1]) -- 在Sorted Set中添加元素
if redis.call('zcard', KEYS[1]) > tonumber(ARGV[3]) then -- 如果元素的数量超过最大数量
return redis.call('zremrangebyrank', KEYS[1], 0, 0) -- 则删除分数最低的元素
end
return true
local keysValues = {'key1', 'value1', 'key2', 'value2', 'key3', 'value3'}
redis.call('MSET', unpack(keysValues))
local result = math.sqrt(16)
local members = redis.call('SMEMBERS', 'set')
for i, member in ipairs(members) do
-- 处理元素
end
local substring = string.sub('hello world', 1, 5)
local rank = redis.call('ZRANK', 'zset', 'member')
local myTable = {key1 = 'value1', key2 = 'value2', key3 = 'value3'}
table.insert(myTable, 'value4')
local members = redis.call('ZREVRANGE', 'zset', 0, -1, 'WITHSCORES')
for i = 1, #members, 2 do
local member = members[i]
local score = members[i + 1]
-- 处理成员和分数
end
local count = redis.call('HLEN', 'hash')
local difference = redis.call('SDIFF', 'set1', 'set2')
local currentTime = os.date('%Y-%m-%d %H:%M:%S')
local keys = redis.call('KEYS', 'pattern*')
for i, key in ipairs(keys) do
-- 处理匹配到的键
end
local members = redis.call('ZREVRANGEBYSCORE', 'zset', maxScore, minScore, 'WITHSCORES')
redis.call('HSETNX', 'hash', 'field', 'value')
local members = redis.call('ZREVRANGE', 'zset', 0, 10, 'WITHSCORES')
redis.call('EVAL', 'local bf = redis.call("BFINSERT", "filter", "item"); local result = redis.call("BFCHECK", "filter", "item"); return result;', 0)
local count = redis.call('INCR', 'counter')
local info = redis.call('INFO')
local values = {'value1', 'value2', 'value3'}
redis.call('RPUSH', 'list', unpack(values))
local co = coroutine.create(function()
-- 执行协程操作
end)
coroutine.resume(co)
local key = KEYS[1]
local value = ARGV[1]
redis.call('SET', key, value)
local fieldsCount = 0
local fields = redis.call('HGETALL', 'hash')
for i = 1, #fields, 2 do
fieldsCount = fieldsCount + 1
end
return fieldsCount
local file = io.open('/path/to/file', 'r')
local content = file:read('*a')
file:close()
local union = redis.call('SUNION', 'set1', 'set2')
return #union
local info = debug.getinfo(2)
local count = redis.call('ZCARD', 'zset')
local result = os.execute('command')
redis.call('ZADD', 'zset', score, 'member')
local str = 'hello world'
local parts = {}
for part in string.gmatch(str, '%S+') do
table.insert(parts, part)
end
redis.call('PEXPIRE', 'key', 1000)
local count = redis.call('ZCOUNT', 'zset', minScore, maxScore)
local result = bit.band(7, 3)
local fieldsValues = redis.call('HGETALL', 'hash')
for i = 1, #fieldsValues, 2 do
local field = fieldsValues[i]
local value = fieldsValues[i + 1]
-- 处理字段和值
end
local members = redis.call('ZREVRANGE', 'zset', 0, 10)
local random = math.random(1, 100)
local length = redis.call('LLEN', 'list')
local index = string.find('hello world', 'world')
redis.call('ZREMRANGEBYSCORE', 'zset', minScore, maxScore)
local tableCopy = table.deepcopy(originalTable)
local exists = redis.call('EXISTS', 'key')
local timestamp = os.time()
local date = os.date('%Y-%m-%d %H:%M:%S', timestamp)
local elements = redis.call('LRANGE', 'list', start, stop)
local info = debug.getinfo(1, 'n')
local isNumber = tonumber('123')
local score = redis.call('ZSCORE', 'zset', 'member')
local replacedString = string.gsub('hello world', 'world', 'redis')
local fields = redis.call('HKEYS', 'hash')
for i, field in ipairs(fields) do
-- 处理字段
end
local cardinality = redis.call('SCARD', 'set')
local file = io.open('/path/to/file', 'w')
file:write('content')
file:close()
local members = redis.call('ZRANGEBYSCORE', 'zset', minScore, maxScore, 'WITHSCORES')
local values = redis.call('HVALS', 'hash')
local lowercase = string.lower('HELLO')
local uppercase = string.upper('world')
local members = redis.call('ZRANGE', 'zset', minScore, maxScore)
local myTable = {key1 = 'value1', key2 = 'value2', key3 = 'value3'}
for key, value in pairs(myTable) do
-- 处理键和值
end
redis.call('LREM', 'list', count, 'element')
local concatenatedString = string.concat('hello', 'world')
local members = redis.call('ZRANGE', 'zset', 0, -1, 'WITHSCORES')
for i = 1, #members, 2 do
local member = members[i]
local score = members[i + 1]
-- 处理成员和分数
end
local element = redis.call('LINDEX', 'list', index)
local co = coroutine.create(function()
-- 执行协程操作
end)
coroutine.resume(co)
local fieldsValues = redis.call('HGETALL', 'hash')
local result = bit.lshift(7, 2)
redis.call('MULTI')
redis.call('SET', 'key1', 'value1')
redis.call('SET', 'key2', 'value2')
redis.call('EXEC')
local rank = redis.call('ZRANK', 'zset', 'member')
local currentTime = os.time()
local sum = 0
local elements = redis.call('LRANGE', 'list', 0, -1)
for i, element in ipairs(elements) do
sum = sum + tonumber(element)
end
return sum
local substring = string.sub('hello world', 7)
local members = redis.call('ZRANGEBYRANK', 'zset', startRank, endRank)
local roundedNumber = math.round(3.7)
redis.call('HDEL', 'hash', 'field')
local result = string.compare('hello', 'world')
local rank = redis.call('ZREVRANK', 'zset', 'member')
不同的语言和类库已经基于Redis实现了分布式锁,大家可以用作参考。
Redlock-rb(Ruby实现)。还有一个Redlock-rb的分支,增加了一个gem以便于分发。
Redlock-py(Python实现)。
Pottery(Python实现)。
Aioredlock(Asyncio Python实现)。
Redlock-php(PHP实现)。
PHPRedisMutex(进一步的PHP实现)。
cheprasov/php-redis-lock(PHP锁库)。
rtckit/react-redlock(异步PHP实现)。
Redsync(Go实现)。
Redisson(Java实现)。
Redis::DistLock(Perl实现)。
Redlock-cpp(C++实现)。
Redis-plus-plus(C++实现)。
Redlock-cs(C#/.NET实现)。
RedLock.net(C#/.NET实现)。包括异步和锁扩展支持。
ScarletLock(C# .NET实现,具有可配置的数据存储)。
Redlock4Net(C# .NET实现)。
node-redlock(NodeJS实现)。包括对锁扩展的支持。
Deno DLM(Deno实现)
Rslock(Rust实现)。包括异步和锁扩展支持。
大家好,我是冰点,今天的Redis Lua脚本执行原理和语法示例,全部内容就是这些。如果你有疑问或见解可以在评论区留言。