欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 旅游 > 【redis】lua脚本

【redis】lua脚本

2025/3/14 16:11:55 来源:https://blog.csdn.net/u022812849/article/details/146238040  浏览:    关键词:【redis】lua脚本

在分布式系统与高并发架构的战场中,开发者们始终在与两个永恒的命题博弈:数据一致性系统性能。当我们试图用Redis构建高速缓存、实现分布式锁或设计秒杀系统时,往往会陷入这样的困境——如何在保证原子性的同时,避免网络往返带来的性能损耗?如何让复杂的多命令操作像单一指令般高效执行?

这正是Redis Lua脚本闪耀的舞台。作为Redis的"核武器级"特性,Lua脚本不仅实现了原子性、隔离性的操作保障,更能将复杂的业务逻辑压缩成服务端的高性能执行单元。

为什么要选择Lua脚本?

在Redis中使用Lua脚本能带来以下核心优势:

原子性保证:Lua脚本在Redis中以单线程方式原子执行,避免多命令操作时的竞态条件。例如库存扣减、分布式锁等场景必须依赖这种特性。

减少网络开销:将多个Redis命令合并为一个脚本执行,减少客户端与服务端之间的网络往返次数(RTT)。对于高频操作性能提升显著。

逻辑复用与版本控制:脚本上传后可通过SHA摘要重复调用,结合SCRIPT LOAD/EVALSHA实现服务端逻辑复用,避免重复传输代码。

服务端计算能力:利用Redis服务端的计算资源处理数据,减少客户端计算压力。例如实现复杂统计、数据过滤等操作。

事务增强版:相比MULTI事务,Lua脚本提供更灵活的逻辑控制(支持if/else、循环等),且执行期间不会被其他命令打断。

Lua脚本命令的使用

有关事务的命令可以通过help @scripting命令来查看。有关命令的使用可以通过help 命令来查看,例如help eval

EVAL

eval:执行脚本。

语法:

EVAL script numkeys key [key ...] arg [arg ...]

参数说明:

  • script:Lua脚本代码
  • numkeys:后面key数组的数量
  • key:脚本代码中所需要操作的redis中的key数组
  • arg:脚本代码中所需要用到的变量参数数组

使用:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2
1) "k1"
2) "k2"
3) "v1"
4) "v2"

SCRIPT系列命令

语法:

# 设置执行脚本为调试模式
SCRIPT DEBUG YES|SYNC|NO# 验证脚本是否存在
SCRIPT EXISTS sha1 [sha1 ...]# 清空脚本缓存
SCRIPT FLUSH [ASYNC|SYNC]# 终止运行中的脚本
SCRIPT KILL -# 缓存脚本,并返回SHA摘要
SCRIPT LOAD script

使用:

127.0.0.1:6379> script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"a42059b356c875f0717db19a51f6aaca9ae659ea"127.0.0.1:6379> script exists "a42059b356c875f0717db19a51f6aaca9ae659ea"
1) (integer) 1127.0.0.1:6379> script flush
OK127.0.0.1:6379> script exists "a42059b356c875f0717db19a51f6aaca9ae659ea"
1) (integer) 0

EVALSHA

evalsha:通过脚本的SHA1摘要执行已缓存的脚本。

语法:

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

使用:

127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 k1 k2 v1 v2
(error) NOSCRIPT No matching script. Please use EVAL.127.0.0.1:6379> script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"a42059b356c875f0717db19a51f6aaca9ae659ea"127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 k1 k2 v1 v2
1) "k1"
2) "k2"
3) "v1"
4) "v2"

EVAL命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗,Redis实现了EVALSHA命令,它的作用和EVAL一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。

如果服务器还记得给定的SHA1校验和所指定的脚本,那么执行这个脚本,如果服务器不记得给定的SHA1校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用EVAL代替EVALSHA。

Lua中执行redis命令

在Lua中,可以通过内置的函数redis.call()和redis.pcall()来执行redis命令。

redis.call()和redis.pcall()两个函数的参数可以是任意的Redis命令:

127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
OK

需要注意的是,上面这段脚本的确实现了将键foo的值设为bar的目的,但是,它违反了EVAL命令的语义,因为脚本里使用的所有键都应该由KEYS数组来传递,就像这样:

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK

要求使用正确的形式来传递键(key)是有原因的,因为不仅仅是EVAL这个命令,所有的Redis命令,在执行之前都会被分析,借此来确定命令会对哪些键进行操作。

因此,对于EVAL命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行。除此之外,使用正确的形式来传递键还有很多其他好处,它的一个特别重要的用途就是确保Redis集群可以将你的请求发送到正确的集群节点。

redis.call()与redis.pcall()很类似,他们唯一的区别是当redis命令执行结果返回错误时,redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以Lua表的形式返回。

下面的例子演示了redis.call()与redis.pcall()的区别:

127.0.0.1:6379> eval "return redis.call('set1',KEYS[1],'bar')" 1 foo
(error) ERR Error running script (call to f_d968406ee98123006fa91fd2ee764d4f7f859dd7): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script127.0.0.1:6379> eval "return redis.pcall('set1',KEYS[1],'bar')" 1 foo
(error) @user_script: 1: Unknown Redis command called from Lua script127.0.0.1:6379> eval "return type(redis.call('set1',KEYS[1],'bar'))" 1 foo
(error) ERR Error running script (call to f_c62b83c8313fd8f2557865e37d2bb5133f1789af): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script127.0.0.1:6379> eval "return type(redis.pcall('set1',KEYS[1],'bar'))" 1 foo
"table"

Lua数据类型和Redis数据类型之间转换

当Lua通过call()或pcall()函数执行Redis命令的时候,命令的返回值会被转换成Lua数据结构。

同样地,当Lua脚本在Redis内置的解释器里运行时,Lua脚本的返回值也会被转换成Redis协议(protocol),然后由EVAL将值返回给客户端。

数据类型之间的转换遵循这样一个设计原则:如果将一个Redis值转换成Lua值,之后再将转换所得的Lua值转换回Redis值,那么这个转换所得的Redis 值应该和最初时的Redis值一样。

换句话说,Lua类型和Redis类型之间存在着一一对应的转换关系。

LuaRedis示例
number(整型)integer3 → 3
number(浮点型)bulk string3.3 -> “3.3”
stringbulk string“value” → “value”
table (数组形式)multi-bulk{1,2,3} → [“1”,“2”,“3”]
table(键值对形式)multi-bulk{name=“Bob”} → (empty array),无法转换需使用json库序列化为字符串
table(只带一个ok的键值对)status{ok=‘success’} -> success
table(只带一个err的键值对)error{err=‘My Error’} -> (error) My Error
booleanintegertrue → 1, false → (nil)
nilnilnil -> 无返回

Lua中整数和浮点数之间没有什么区别。因此,我们始终将Lua的数字转换成整数的回复,这样将舍去小数部分。如果你想从Lua返回一个浮点数,你应该将它作为一个字符串,比如ZSCORE命令。

以下是几个类型转换的例子:

127.0.0.1:6379> eval "return 10" 0
(integer) 10127.0.0.1:6379> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 32) "Hello World!"127.0.0.1:6379> eval "return redis.call('get','foo')" 0
"bar"

最后一个例子展示如果是Lua直接命令调用它是如何可以从redis.call()或redis.pcall()接收到准确的返回值。

下面的例子我们可以看到浮点数和nil将怎么样处理:

127.0.0.1:6379> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"

正如你看到的3.333被转换成了3,并且nil后面的字符串bar没有被返回回来。

可以使用tostring()函数将数字转字符串:

127.0.0.1:6379> eval "return tostring(3.3333)" 0
"3.3333"

有两个辅助函数从Lua返回Redis的类型:

  • redis.error_reply(error_string):returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
  • redis.status_reply(status_string):returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.

使用redis.error_reply()函数与直接返回一个table效果一样:

127.0.0.1:6379> eval "return {err='My Error'}" 0
(error) My Error127.0.0.1:6379> eval "return redis.error_reply('My Error')" 0
(error) My Error

可用库

Redis Lua解释器可用加载以下Lua库:

  • base lib.
  • table lib.
  • string lib.
  • math lib.
  • debug lib.
  • struct lib.
  • cjson lib.
  • cmsgpack lib.
  • bitop lib.
  • redis.sha1hex function.

每一个Redis实例都拥有以上的所有类库,以确保您使用脚本的环境都是一样的。

struct,CJSON和cmsgpack都是外部库,所有其他库都是标准Lua库。

CJSON库为Lua提供极快的JSON处理:

127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0
"{\"foo\":\"bar\"}"127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}"
"bar"127.0.0.1:6379> eval "local table = {} table['foo']='bar' table['hello']='world' return cjson.encode(table)" 0
"{\"hello\":\"world\",\"foo\":\"bar\"}"

在java中的使用

lua脚本:

-- 库存扣减脚本
local key = KEYS[1]
local quantity = tonumber(ARGV[1])local stock = tonumber(redis.call('GET', key))
if not stock thenreturn -1
endif stock >= quantity thenreturn redis.call('DECRBY', key, quantity)
elsereturn 0
end

在java中使用lua脚本:

package com.morris.redis.demo.lua;import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.Collections;/*** RedisTemplate中lua脚本的使用*/
@Service
public class RedisTemplateLuaDemo {@Resourceprivate RedisTemplate redisTemplate;public void testLua() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/deduct_stock.lua")));redisScript.setResultType(Long.class);Object object = redisTemplate.execute(redisScript,Collections.singletonList("stock:10086"),Integer.toString(1));System.out.println(object);}}

RedisTemplate底层源码已经实现了evalsha+eval,无需手动加载脚本,源码如下:

org.springframework.data.redis.core.script.DefaultScriptExecutor#eval

	protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys,byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {Object result;try {result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);} catch (Exception e) {if (!ScriptUtils.exceptionContainsNoScriptError(e)) {throw e instanceof RuntimeException ? (RuntimeException) e : new RedisSystemException(e.getMessage(), e);}result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);}if (script.getResultType() == null) {return null;}return deserializeResult(resultSerializer, result);}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词