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

相关推荐
郑州吴彦祖77211 分钟前
【java】数据类型与变量以及操作符
java·intellij-idea
程序员大金12 分钟前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
吹老师个人app编程教学17 分钟前
阿里巴巴_java开发规范手册详解
java·开发语言
天上掉下来个程小白17 分钟前
Stream流的终结方法(一)
java·windows
qq_25183645719 分钟前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端
天上掉下来个程小白39 分钟前
请求响应-08.响应-案例
java·服务器·前端·springboot
大白_dev40 分钟前
数据校验的总结
java·开发语言
失落的香蕉1 小时前
Java第二阶段---10方法带参---第三节 面向对象和面向过程的区别
java·开发语言
qq_2518364571 小时前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端
哎呀呀嗯呀呀1 小时前
class 031 位运算的骚操作
java·算法·位运算