Redis 事务 vs Lua,区别以及如何选择

Redis 事务 vs Lua

前言

Lua 脚本也能保证一定的原子性,这与 Redis 事务机制十分类似,为啥 Redis 会支持两种类似的机制呢?

另外,在 stackoverflow 十几年前的远古帖子上还能看到有人引用官网的话如下:

"we may deprecate and finally remove transactions"

但直至 Redis 7.0 版本,Redis 事务功能依然健在,而新版本的官网也找不到这句话了。

所以,事务和 Lua 到底有何区别?我们应该如何做选择呢?

我们先分别介绍一下事务 与 Lua 机制。

Redis 事务简介

Redis 事务基本使用

Redis 事务基本可以分为如下几个步骤:

1、开启事务:multi

让客户端Client变为事务模式状态

2、命令入列

输入multi后的所有命令会被存储在一个命令队列中,作为事务的一部分

此时,所有命令返回值均为 QUEUED

3、执行事务:exec

执行exec,执行事务

4、放弃事务:discard

不会执行事务

Redis 事务有ACID吗

1、原子性

原子性的描述是:一个事务的操作要么全部成功,要么全部失败。Redis 事务是否有原子性这个问题,需要分类讨论,但我们需要先明确一点:Redis 事务不支持回滚。

不支持回滚

Redis 事务并不支持回滚,这与许多关系型数据库截然相反,关于这个问题我找到了一篇官方回答:《为什么 Redis 不支持事务回滚》,非常推荐。简单说就是,Redis 事务的所有命令会被一起执行,并且事务的执行期间,别的客户端发送的命令需要等待事务执行完毕才去执行。加之 Redis 单线程的特性,可以理解为事务获取到了Redis 全局锁。换句话说,只要编码正确,事务总会成功,不会失败

现在再回到Redis 事务原子性的讨论中:

  • 当事务存在「语法错误」,能够被 Redis 在命令入队时检测出,执行EXEC会失败,有原子性
  • 当事务存在「运行时错误」,比如命令和操作的数据类型不匹配,此时事务中其它正确的命令仍然会被按序执行,此时无原子性,即上述编码错误的情况
  • 当事务正常执行,无任何错误时,有原子性

因此,只要编码正确,Redis 事务总能保证原子性。

2、隔离性

既然 Redis 单线程执行命令,并且「事务的所有命令会被一起执行」,其它客户端的命令此时会被阻塞,因此 Redis 事务执行过程中能保证天然的隔离性。

但是,一个 Redis 事务的生命周期包含两部分:开启到执行前,和执行过程中。

事务开启到正式执行前,可能会存在隔离性问题,Redis 提供了 watch 机制来解决这个问题。

监控 watch:乐观锁

watch使用在 multi 事务开启之前

watch 可以保证 EXEC 前的隔离性,可以理解为一把乐观锁。

使用watch监控若干个key,如果在事务 EXEC 前,某个监控项被修改了,那么在执行 EXEC 命令时,会放弃事务

css 复制代码
WATCH [key]

使用unwatch可以清除所有监控。

因此,合理地使用 watch 机制,Redis 事务也能保证隔离性。

3、持久性

这取决于 Redis 的持久化配置模式。

但不管 Redis 采用什么持久化模式,都可能出现数据丢失的情况,所以无法保证事务的持久性。

4、一致性

Redis 事务机制对一致性属性是有保证的。

Redis 事务的不足

一旦事务需要依赖于一个 get 操作的结果,就非常不方便。因为事务是一起执行的,我们没办法将一个操作的结果作为命令的一部分进行执行。

这么说可能有些抽象,举个例子,假如没有 incrby 命令,要使用事务保证原子性,需要这样执行:

sql 复制代码
WATCH counter
GET counter
MULTI
SET counter <the value obtained from GET + any increment>
EXEC

即在事务开始前,先获取事务依赖的操作结果,然后执行事务。那要保证线程安全,就必须引入乐观锁机制,使用 WATCH 命令,而当多个客户端都并发执行这段逻辑时,事务执行失败的概率就非常高,容易不断再次 get ,再次执行事务,再次事务被放弃,陷入循环。

Lua 脚本简介

Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。

Lua 脚本的基本使用

执行脚本:EVAL

css 复制代码
# [script] 是要执行的脚本,用""包裹
# [numkeys] 代表 key 的数量 
# [key ...] key 的名称 [numkeys]个 Lua操作的 Key 的名称必须来自于此。
# [arg ...] 其它参数
EVAL "[script]" [numkeys] [key ...] [arg ...]

EVAL 不仅仅会执行脚本,还会载入脚本,后续会介绍 Redis 的 Lua 缓存

Lua 操作 Key:必须来自 KEYS

Lua 脚本操作的 Key 的来源,必须来自于 KEYS[x]

arduino 复制代码
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 命令:redis.call

arduino 复制代码
  EVAL "return redis.call('GET','mystring')" 0
 "hello world"

redis.call 如果发生错误,会立即返回错误,并终止 Lua 脚本的执行

不同的错误处理:redis.pcall

redis.pcallredis.call 唯一的区别是出错时的处理。

redis.pcall 并不会终止,而是返回一个带err域的Lua表?

脚本缓存:SHA1

单纯的 EVAL 命令存在一个问题,即每次都需要将完整的 Lua 脚本传送给 Redis。

为了解决这个问题,Redis 专门有一个缓存区,存放 Lua 脚本的内容以及 SHA1的值。

EVAL 命令既会执行命令,也会载入缓存,SCRIPT LOAD 是纯粹的载入缓存,在 Redis 持有缓存后,只需要EVALSHA 命令,即可不传输完整的 Lua 脚本,只传输 SHA1 的值来执行脚本。

bash 复制代码
#  加载脚本
redis> SCRIPT LOAD "return 'Immabe a cached script'"
"c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
#  执行脚本
redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
"Immabe a cached script"

脚本缓存本身不会持久化,重启 Redis,主从故障转移等等情况,脚本缓存都会丢失,此时 EVALSHA 命令会返回

go 复制代码
(error) NOSCRIPT No matching script

在客户端层面需要对缓存丢失的情况做进一步保证,如 EVALSHA 报错后执行 EVAL 命令 。

Lua 常见命令

载入并执行脚本:EVAL

css 复制代码
# [script] 是要执行的脚本,用""包裹
# [numkeys] 代表 key 的数量 
# [key ...] key 的名称 [numkeys]个 Lua操作的 Key 的名称必须来自于此。
# [arg ...] 其它参数
EVAL "[script]" [numkeys] [key ...] [arg ...]

执行载入的脚本:EVALSHA

复制代码
EVALSHA sha1

查询脚本是否已载入:EXISTS

ini 复制代码
SCRIPT EXISTS [SHA1 ...]
# 0 不存在 1 存在

载入脚本:LOAD

bash 复制代码
# 不会执行脚本
SCRIPT LOAD

清除载入的脚本:FLUSH

复制代码
SCRIPT FLUSH

终止脚本的执行:SCRIPT KILL

bash 复制代码
# 一旦 lua 脚本存在写入操作,就无法终止
SCRIPT KILL
bash 复制代码
#!lua flags=no-writes,allow-stale
local x = redis.call('get','x')
return x

Redis 7.0 新特性:Eval flags

Redis 7.0 前,Redis 假设所有的脚本都会进行数据的读和写操作。但 Redis 7.0 提供了 Eval flags 可以告知 Redis 脚本的行为。

只要 Redis 发现 #! ,就会获得一些默认的 lua flags,这和没有 #! 的 Lua 脚本不同

没有 #! 的 Lua 脚本可以访问集群中的所有节点,但是由于 #! 默认继承了一些flags,所以不能。

Redis 默认脚本有如下特性:

  1. 既有读操作,又有写操作
  2. 可以运行在集群模式下,但不能跨 slots 访问数据
  3. 从节点数据可能过时,会被拒绝操作从节点
  4. 内存空间小时,会拒绝执行来避免内存溢出

下面列出部分参数及含义:

参数 含义
allow-cross-slot-keys 允许脚本访问来自不同slots的keys(尽管应该尽量避免这样做)
no-cluster 如果Redis在集群模式下运行,执行脚本会返回错误
no-writes 脚本只有读操作,没有写操作

执行时长限制

lua 脚本如果执行时长超过 5 s,Redis 会自动终止该脚本

主从模式下 Lua 的数据同步

Redis 7.0 只有一种同步方案:Effects replication。

主节点会将 Lua 脚本中,实际修改数据的命令封装成「MULTI/EXEC 的事务」并同步给从节点(以及AOF)。

从节点无需将这些命令当作脚本执行,便可以保证主从节点的数据一致。

在 Redis 5.0 之后默认的方案即为 Effects replication。

另一个方案叫 Verbatim replication,需要从节点与主节点完全执行相同的工作,在 Redis 7.0 被废弃。

Lua vs 事务

最后,我们来对比 Lua 和 事务的主要区别。

性能比较

根据官方文档,Lua 的理论效率要高于事务,但实际运行过程中也可能比较接近。Lua 略胜。

使用成本

尽管 Redis 事务相关的命令非常少,但 Lua 脚本的编写也十分简单,入门门槛低。Redis 事务略胜,但不关键。

应用场景

Lua 更灵活,在 Lua 脚本内,可以通过 if then 语句做逻辑控制,而事务不行。

另外一旦事务需要依赖于一个 get 操作的结果,Redis 事务就很难做到了。在灵活度上 Lua 完胜。

什么时候该用事务

总结是,能用 Lua ,就用 Lua,实在用不了才用事务。

  • 您的事务所依赖的键不会被频繁修改,这意味着您确信乐观锁定几乎永远不会中止事务。
  • 可能是,第三方服务编写的大量逻辑,因此没有简单的方法可以将该逻辑移动到 Lua 脚本。

除非这两点同时满足,否则都更推荐使用 Lua。

参考文档

《Redis 核心技术与实战》

Redis 官网

You Don't Need Transaction Rollbacks in Redis

Error: crossslot keys in request don't hash to the same slot

CROSSSLOT error on single-shard cluster

相关推荐
無限進步D3 小时前
Java 运行原理
java·开发语言·入门
難釋懷3 小时前
安装Canal
java
是苏浙3 小时前
JDK17新增特性
java·开发语言
不光头强3 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp6 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多6 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood7 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员7 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai