为什么使用Lua脚本为什么能合并多个原子操作?
Redis官方文档:https://redis.io/docs/manual/programmability/eval-intro/
Redis 保证脚本的原子执行。在执行脚本时,所有服务器活动在其整个运行期间都被阻止。这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。
脚本提供了几个在许多情况下都很有价值的属性。这些包括:
通过在数据所在的地方执行逻辑来提供局部性。数据局部性减少了整体延迟并节省了网络资源。
确保脚本原子执行的阻塞语义。
启用 Redis 中缺少的或过于小众的简单功能的组合。
Lua 允许您在 Redis 中运行部分应用程序逻辑。这样的脚本可以跨多个键执行条件更新,可能以原子方式组合几种不同的数据类型。
命令行应用Lua
Redis Eval 命令使用 Lua 解释器执行脚本。
这里能帮我们实现 Redis 执行 Lua 脚本的命令有两个,一个是 EVAL,另一个是 EVALSHA。
redis Eval 命令基本语法如下:
EVAL script numkeys key [key ...] arg [arg ...]
其中 EVAL 是命令,script 是我们 Lua 脚本的字符串形式,numkeys 是我们要传入的参数数量,key 是我们的入参,可以传入多个,arg 是额外的入参。
以下尝试演示脚本KEYS和ARGV运行时全局变量之间输入参数的分布:
redis> EVAL " return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] } " 2 key1 key2 arg1 arg2 arg3
1 ) " key1 "
2 ) " key2 "
3 ) " arg1 "
4 ) " arg2 "
5 ) " arg3 "
但这种方式需要每次都传入 Lua 脚本字符串,不仅浪费网络开销,同时 Redis 需要每次重新编译 Lua 脚本,对于我们追求性能极限的系统来说,不是很完美。所以这里就要说到另一个命令 EVALSHA 了,原生语法如下:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
不同的是这里传入的不是脚本字符串,而是一个加密串 sha1。这个 sha1 是从哪来的呢?它是通过另一个命令 SCRIPT LOAD 返回的,该命令是预加载脚本用的。
从脚本与 Redis 交互
可以通过redis.call() 或从 Lua 脚本调用 Redis 命令redis.pcall() 。
两者几乎一模一样。两者都执行 Redis 命令及其提供的参数(如果这些参数表示格式正确的命令)。但是,这两个函数之间的区别在于处理运行时错误(例如语法错误)的方式。调用函数引发的错误redis.call()
直接返回给执行它的客户端。相反,调用redis.pcall()
函数时遇到的错误将返回到脚本的执行上下文,而不是进行可能的处理。
Java客户端应用Lua实例
注意Lua 脚本并不会自动帮你完成回滚操作,如果你的脚本逻辑中包含两步写操作,需要自己去做回滚。好在我们库存扣减的逻辑针对 Redis 的命令就两种,一个读一个写,并且写命令在最后,这样就不存在需要回滚的问题了。
以库存扣减核心操作为例,完成核心 Lua 脚本的编写。其主要实现的功能就是查询库存并判断库存是否充足,如果充足,则做相应的扣减操作,脚本内容如下:
-- 调用Redis的get指令,查询活动库存,其中KEYS[1 ]为传入的参数1,即库存key
local c_s = redis.call(' get ' , KEYS[1 ])
-- 判断活动库存是否充足,其中KEYS[2 ]为传入的参数2,即当前抢购数量
if not c_s or tonumber(c_s) < tonumber(KEYS[2 ]) then
return 0
end
-- 如果活动库存充足,则进行扣减操作。其中KEYS[2 ]为传入的参数2,即当前抢购数量
redis.call( ' decrby ' ,KEYS[1 ], KEYS[2 ])
然后我们将 Lua 脚本转成字符串,并添加脚本预加载机制。
预加载可以有多种实现方式,一个是外部预加载好,生成了 sha1 然后配置到配置中心,这样 Java 代码从配置中心拉取最新 sha1 即可。
另一种方式是在服务启动时,来完成脚本的预加载,并生成单机全局变量 sha1。我们这里先采取第二种方式,代码结构如下图所示:
以上是将 Lua 脚本转成字符串形式,并通过 @PostConstruct 完成脚本的预加载。然后新增 EVALSHA 方法,如下图所示:
方法入参为活动商品库存 key 以及单次抢购数量,并在内部调用 Lua 脚本执行库存扣减操作。看起来是不是很简单?在写完底层核心方法之后,我们只需要在下单之前,调用该方法即可,具体如下图所示:
脚本缓存
到目前为止,我们已经使用EVAL命令来运行我们的脚本。
每当我们调用时EVAL,我们还会在请求中包含脚本的源代码。重复调用EVAL执行同一套参数化脚本,既浪费网络带宽,也对 Redis 有一定的开销。当然,节省网络和计算资源是关键,因此,Redis 为脚本提供了一种缓存机制。
您执行的每个脚本都EVAL存储在服务器保留的专用缓存中。缓存的内容由脚本的 SHA1 摘要总和组织,因此脚本的 SHA1 摘要总和在缓存中唯一标识它。您可以通过运行EVAL并随后调用来验证此行为INFO。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2017-05-16 工程师进阶推荐十本书
2016-05-16 大型网站前端架构的设计与优化