首页 > 数据库 >redis 事务

redis 事务

时间:2023-04-20 11:25:41浏览次数:33  
标签:脚本 事务 调用 Lua redis Redis 命令

redis 事务

MULTI,EXEC,DISCARD和WATCH是Redis事务的基础。它们允许在一个步骤中执行一组命令,并有两个重要的保证:

  1. 事务中的所有命令都被序列化并按顺序执行。在执行Redis事务的过程中,不会发生由另一个客户端发出的请求。这保证了命令作为一个单独的操作被执行。
  2. 要么所有的命令都没有被处理、要么没有命令被执行,所以Redis事务也是原子的。

    EXEC命令,触发事务中的所有命令的执行,因此,如果客户端在执行MULTI命令前,在事务的上下文中丢失与服务器的连接,没有命令会被执行,然而如果EXEC命令被调用时,所有的操作都被执行。使用 append-only file ,Redis确保使用单个写操作,系统调用将事务写入磁盘。但是,如果Redis服务器崩溃或被系统管理员以某种方式杀死,则可能只有部分命令被执行。Redis会在重新启动时检测到这种情况,会退出并显示错误信息。使用该redis-check-aof工具,可以修复将删除部分事务的append only file,以便服务器可以重新启动。

从版本2.2开始,Redis允许为上述两个提供额外的保证,采用与 check-and-set (CAS) 操作非常相似的乐观锁定形式

Redis事务命令


MULTI:
	使用MULTI命令开启Redis事务。该命令总是回复OK。此时用户可以发出多个命令。Redis不会执行这些命令,而是将它们排队。一旦EXEC被调用,所有的命令被执行。
	类似于mysql中的BEGIN; 标记一个事务块的开始。
	当Redis连接处于MULTI请求的上下文中时,所有命令都将回复该字符串QUEUED
	

EXEC:
	类似于COMMIT; 执行所有事务块内的命令。
	EXEC返回一个响应数组,其中每个元素都是事务中单个命令的回复,这与命令发出的顺序相同。
	**当Redis连接处于MULTI请求的上下文中时,所有命令都将回复该字符串QUEUED**
	**即使命令失败,队列中的所有其他命令也会被处理**
 
DISCARD:
	调用DISCARD刷新事务队列,并且退出事务。
	类似于ROLLBACK;取消事务,放弃执行事务块内的所有命令。


WATCH key [key ...] : 
	则是用于来实现mysql中类似锁的功能。 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
	WATCH用于为Redis事务提供检查和设置(check-and-set(乐观锁定))(CAS)行为
	- 这是一个使EXEC有条件的命令:只有在任何被WATCH的键没有被修改的情况下,我们才会要求Redis执行事务。(但是,他们可能会被事务内相同的客户端没有放弃它而改变。)否则事务不会进入的。(请注意:如果您WATCH一个存在有效期的键<volatile key>,键过期后,EXEC仍然可以工作。)
	- WATCH可以被多次调用。简单地说,所有的WATCH调用,都将具有监视从调用开始,直到EXEC被调用期间变化的效果。您也可以将任意数量的key发送给一个WATCH调用。
	- 当EXEC被调用时,不管事务是否中止,所有的key都被UNWATCH。另外,当一个客户端连接关闭,所有的key也都被UNWATCH。
	- 也可以使用UNWATCH命令(不带参数)来刷新所有watched keys。有时候,我们乐观地锁定了几个键,这是非常有用的,因为可能我们需要执行一个事务来改变这些键,但是在读完这些键的当前内容之后,我们不想继续。发生这种情况时,我们只需调用UNWATCH,以便连接可以自由地用于新的事务。
	
	

UNWATCH:  
取消 WATCH 命令对所有 key 的监视。

事务中的错误

  • 命令可能无法进入队列,所以在调用EXEC之前可能会出现错误

    命令可能在语法上是错误的(参数数量错误,错误的命令名称...),或者可能存在一些严重的情况,例如内存不足(如果服务器被配置为使用该maxmemory指令,具有内存限制)。

  • 在 调用EXEC 命令之后,命令可能会失败

    我们对具有错误值的键执行操作(例如,针对string值调用list 操作)。在EXEC调用之前,客户端通过检查排队命令的返回值,来感知第一类错误:如果命令使用QUEUED进行响应,则排队正确,否则Redis返回错误。如果排队命令时发生错误,大多数客户端将中止放弃该事务。

  • 在 EXEC 之后发生的错误不是以一种特殊的方式处理的:即使某些命令在事务中失败,所有其他的命令也会被执行

为什么Redis不支持回滚(roll backs)

  • 如果使用错误的语法调用Redis命令(并且在命令排队期间无法检测到问题),或者针对保存错误数据类型的键,则Redis命令可能会失败:这意味着,实际上,失败的命令是编程错误的结果,以及在开发过程中很可能被检测到的一种错误,而不是在生产中。
  • Redis的内部简化和更快,因为它不需要回滚的能力。
  • 反对Redis观点的一个观点是错误发生了,但是应该注意的是一般情况下回滚并不能避免编程错误。例如,如果一个查询增加了一个键而不是1,或者增加了错误的键,那么回滚机制就没有办法提供帮助。鉴于没有人能够挽救程序员的错误,并且Redis命令失败所需的错误类型不太可能进入生产环境,所以我们选择了不支持错误回滚的更简单快捷的方法。

悲观锁(Pessimistic Lock),

顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,
这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock),

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,
不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

Redis脚本和事务

EVAL执行Lua脚本

  • 自Redis2.6.0版本起可用。
  • 时间复杂度:取决于执行的脚本。

EVAL介绍

  • EVAL和EVALSHA用于从Redis2.6.0版本,开始使用内置在Redis中的Lua解释器来评估脚本。
  • EVAL的第一个参数是一个Lua 5.1脚本。脚本不需要定义一个Lua函数(不应该)。这只是一个将在Redis服务器上下文中运行的Lua程序。
  • EVAL的第二个参数是脚本后面的参数个数,(从第三个参数开始)代表Redis键名称。参数可以通过Lua中使用来访问KEYS全局变量在基于一个数组(这样的形式KEYS[1],KEYS[2]...)。

所有其他参数不应该代表键名称,可以通过ARGV全局变量被Lua使用,非常类似于键做了什么(如:ARGV[1],ARGV[2]...)。

下面的例子应该阐明上面的内容:

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

注意:正如你所看到的,Lua数组是以Redis多个批量回复的形式返回的,这是一个Redis返回类型,你的客户端类库可能会用你的编程语言转换成一个Array类型。

  • 可以使用两个不同的Lua函数从Lua脚本调用Redis命令:
redis.call()
redis.pcall()
  1. redis.call()类似于redis.pcall(),唯一的区别是,
  2. 如果一个Redis命令调用会导致一个错误,redis.call()将引发一个Lua错误,反过来会强制EVAL向命令调用者返回一个错误,
  3. 然而redis.pcall将捕捉错误,并返回一个Lua table代表错误。
  • redis.call()和redis.pcall()函数的参数,是Redis命令格式的所有参数:
> eval "return redis.call('set','foo','bar')" 0
OK

上面的脚本将键foo设置为字符串bar。然而,它违反了EVAL命令的语义,因为脚本使用的所有键都应该使用KEYS数组传递:

> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK
  • 在执行之前,必须先分析所有Redis命令,以确定命令将在哪些键上运行。为了使EVAL成为真,键必须明确地传递。这在许多方面都很有用,但特别是要确保Redis群集可以将请求转发到适当的群集节点。
  • 请注意:为了向用户提供使用Redis单实例配置的机会,不会执行此规则,但要以编写与Redis群集不兼容的脚本为代价。

Lua脚本可以使用一组转换规则,返回从Lua类型转换为Redis协议的值。

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

  • 当Lua使用call()or 调用Redis命令时,Redis返回值被转换为Lua数据类型pcall()。同样,在调用Redis命令和Lua脚本返回值时,Lua数据类型转换为Redis协议,以便脚本可以控制EVAL返回给客户端的值。
  • 数据类型之间的这种转换的设计方式是,如果将Redis类型转换为Lua类型,然后将结果转换回Redis类型,则结果与初始值相同。
  • 换句话说,Lua和Redis类型之间存在一对一的转换。下表显示了所有的转换规则:
Redis到Lua转换表。
Redis integer reply -> Lua number
Redis bulk reply -> Lua string
Redis multi bulk reply -> Lua table (may have other Redis data types nested)
Redis status reply -> Lua table with a single ok field containing the status
Redis error reply -> Lua table with a single err field containing the error
Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type
Lua到Redis转换表。
Lua number -> Redis integer reply (the number is converted into an integer)
Lua string -> Redis bulk reply
Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
Lua table with a single ok field -> Redis status reply
Lua table with a single err field -> Redis error reply
Lua boolean false -> Redis Nil bulk reply.

还有一个额外的Lua-to-Redis转换规则,它没有对应的Redis到Lua转换规则:

Lua boolean true -> Redis integer reply with value of 1.

还有两个重要的规则要注意:

  • Lua有一个数字类型,Lua numbers。整数和浮点数没有区别。所以我们总是把Lua数字转换成整数回复,如果有小数部分的话去掉数字的小数部分。如果你想从Lua返回一个浮点数,你应该像字符串一样返回它,就像Redis本身一样(例如见ZSCORE命令)。
  • 没有简单的方法,在Lua数组中有nils(There is no simple way to have nils inside Lua arrays),这是的Lua表语义的结果,所以当Redis的一个Lua数组转换成Redis的协议,如果遇到nil的转换停止。

以下是几个转换示例:

> eval "return 10" 0
(integer) 10


> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 3
2) "Hello World!"

> eval "return redis.call('get','foo')" 0
"bar"
  • 最后一个例子展示它是如何从Lua获得,确切redis.call()或者redis.pcall()的返回值 ,如果命令直接调用将被返回。
  • 在下面的例子中,我们可以看到如何处理带有nil的浮点数组和数组:
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"

正如你可以看到3.333转化成3,bar 永远不会返回的字符串,因为前面是零。

Helper函数返回Redis类型

有两个helper 函数可以从Lua返回Redis类型。

  • redis.error_reply(error_string)返回错误回复。这个函数只是简单的返回a single field table ,其中的err字段设置为指定的字符串。
  • redis.status_reply(status_string)返回状态回复。这个函数只是简单的返回a single field table ,其中的ok字段设置为指定的字符串。
    使用helper 函数或直接以指定的格式返回table 没有区别,所以以下两种形式是等价的:
return {err="My Error"}
return redis.error_reply("My Error")

脚本的原子性

  • Redis使用相同的Lua解释器来运行所有的命令。另外,Redis保证以原子方式执行脚本:执行脚本时不会执行其他脚本或Redis命令。这个语义类似于MULTI / EXEC。从所有其他客户的角度来看,脚本的效果要么不可见,要么已经完成。
  • 但是这也意味着执行缓慢的脚本不是一个好主意。创建快速脚本并不难,因为脚本开销非常低,但是如果要使用慢速脚本,则应该意识到在脚本运行时,没有其他客户端可以执行命令。

错误处理

  • 如前所述,redis.call()导致Redis命令错误的调用会停止脚本的执行并返回一个错误,使得脚本错误显而易见:
> del foo
(integer) 1
> lpush foo a
(integer) 1
> eval "return redis.call('get','foo')" 0
(error) ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e38c0e791): ERR Operation against a key holding the wrong kind of value
  • 使用redis.pcall()不会引发错误,但错误对象是在上述规定的格式返回(作为一个 Lua table 与err字段)。该脚本可以通过返回redis.pcall()的错误对象,将确切的错误传递给用户。

Bandwidth和EVALSHA

EVAL命令迫使你一次又一次地发送脚本主体。Redis不需要每次重新编译脚本,因为它使用内部缓存机制,但是在许多情况下支付额外带宽(bandwidth )的代价可能不是最佳的。

另一方面,使用特殊命令或通过定义命令redis.conf 将是一个问题,原因如下:

  • 不同的实例可能有不同的命令实现。

  • 如果我们必须确保所有实例都包含给定的命令,特别是在分布式环境中,则部署非常困难。

  • 阅读应用程序代码,完整的语义可能不清楚,因为应用程序调用命令定义的服务器端。

为了避免这些问题,避免带宽损失,Redis实现了EVALSHA命令。

EVALSHA的工作方式与EVAL完全相同,但不是将脚本作为第一个参数,而是使用脚本的SHA1 digest 。

行为如下:

  • 如果服务器仍然记住具有匹配的SHA1摘要的脚本,则执行该脚本。

  • 如果服务器不记得具有此SHA1 digest的脚本,则会返回一个特殊错误,告诉客户端使用EVAL。

例:

> set foo bar
OK
 
> eval "return redis.call('get','foo')" 0
"bar"
 
> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
"bar"
 
> evalsha ffffffffffffffffffffffffffffffffffffffff 0
(error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).
  • 即使客户端实际调用了EVAL,客户端库实现也总能乐观地发送EVALSHA,希望脚本已经被服务器看到了。如果错误返回,将使用EVAL。
  • NOSCRIPT将键和参数作为额外的EVAL参数传递在这个上下文中也是非常有用的,因为脚本字符串保持不变并且可以被Redis有效地缓存。

脚本缓存语义

  • 执行的脚本被保证永远在Redis实例的给定执行的脚本缓存中。这意味着如果对Redis实例执行EVAL,所有后续的EVALSHA调用都将成功。
  • 脚本可以长时间缓存的原因是,编写良好的应用程序不可能有足够的不同脚本来引起内存问题。每个脚本在概念上都像执行一个新的命令,即使是一个大的应用程序也可能只有几百个。即使应用程序被多次修改,脚本也会改变,所使用的内存可以忽略不计。
  • 刷新脚本缓存的唯一方法是显式调用SCRIPT FLUSH命令,这将彻底刷新脚本缓存,删除到目前为止执行的所有脚本。
  • 这通常仅在实例将被实例化为云环境中的另一个客户或应用程序时才需要。

另外,如前所述,重新启动Redis实例刷新脚本缓存,这是不持久的。但是从客户端的角度来看,只有两种方法可以确保Redis实例在两个不同的命令之间不重新启动。

  • 我们与服务器的连接是持久的,并且从来没有关闭过。
  • 客户端显式检查INFO命令中的runid字段,以确保服务器未重新启动,并且仍然是相同的进程。

实际上,对于客户端来说,简单地假设在给定连接的上下文中,保证缓存脚本在那里,除非管理员明确地调用了SCRIPT FLUSH命令。

用户可以指望Redis不删除脚本的事实在流水线上下文中是语义上有用的。

例如,一个与Redis持久连接的应用程序可以确定,如果脚本一旦被发送,它仍然在内存中,那么EVALSHA就可以在管道中用于这些脚本,而不会由于未知的脚本而产生错误(我们稍后会详细看到这个问题)。

一个常见的模式是调用SCRIPT LOAD来加载将出现在管道中的所有脚本,然后直接在管道中使用EVALSHA,而不需要检查由于脚本哈希不被识别而导致的错误。

SCRIPT命令

Redis提供了一个SCRIPT命令,可以用来控制脚本子系统。SCRIPT目前接受三个不同的命令:

  • 脚本刷新

此命令是强制Redis刷新脚本缓存的唯一方法。在云环境中,可以将相同的实例重新分配给不同的用户,这非常有用。测试客户端库的脚本功能实现也很有用。

SCRIPT EXISTS sha1 sha2 ... shaN
  • 给定一个SHA1摘要列表作为参数,这个命令返回一个1或0的数组,其中1表示特定的SHA1被识别为已存在于脚本缓存中的脚本,而0表示具有该SHA1的脚本从来没有见过或者至少从未见过最新的SCRIPT FLUSH命令)。
SCRIPT LOAD script
  • 该命令将指定的脚本注册到Redis脚本缓存中。这个命令在我们想要确保EVALSHA不会失败的所有上下文中是有用的 (例如在管道或MULTI / EXEC操作期间),而不需要真正执行脚本。
SCRIPT KILL
  • 此命令是中断长时间运行的脚本的唯一方法,该脚本达到配置的脚本最大执行时间。SCRIPT KILL命令只能用于在执行期间不修改数据集的脚本(因为停止只读脚本不会违反脚本引擎的保证原子性)。有关长时间运行的脚本的更多信息,请参阅下一节。

脚本作为纯粹的功能

脚本的一个非常重要的部分是编写纯函数的脚本。在默认情况下,在Redis实例中执行的脚本会通过发送脚本(而不是结果命令)在从属设备上复制到AOF文件中。

原因在于,将脚本发送到另一个Redis实例通常要比发送脚本生成的多个命令快得多,因此如果客户端将大量脚本发送给主设备,则将脚本转换为slave / AOF的单个命令会导致复制链接或“只附加文件”的带宽过多(而且由于调度通过网络接收到的命令,CPU数量也比分派由Lua脚本调用的命令要多得多)。

通常复制脚本而不是脚本的效果是有意义的,但不是在所有情况下。因此,从Redis 3.2开始,脚本引擎能够复制脚本执行产生的写入命令序列,而不是复制脚本本身。请参阅下一节获取更多信息。在本节中,我们将假设通过发送整个脚本来复制脚本。我们把这个复制模式称为整个脚本复制。

整个脚本复制方法的主要缺点是脚本需要具有以下属性:

  • 脚本必须始终使用给定相同输入数据集的相同参数来评估相同的Redis 写入命令。由脚本执行的操作不能依赖于任何隐藏的(非显式的)信息或状态,这些信息或状态可能会随着脚本执行的进行或脚本的不同执行而改变,也不能依赖于来自I / O设备的任何外部输入。
    比如使用系统时间,调用像RANDOMKEY这样的Redis随机命令 ,或者使用Lua随机数生成器,都可能导致脚本不总是以相同的方式进行评估。

为了在脚本中执行这个行为,Redis执行以下操作:

  • Lua不会导出命令来访问系统时间或其他外部状态。
  • 如果一个脚本调用了一个Redis命令,Redis命令可以在RANDOMKEY,SRANDMEMBER,TIME之类的Redis 随机命令 之后修改数据集, Redis将会阻塞该脚本。这意味着如果一个脚本是只读的,并且不修改数据集,则可以自由地调用这些命令。请注意,随机命令不一定意味着使用随机数的命令:任何非确定性命令都被视为随机命令(这方面的最佳示例是TIME命令)。
  • 可能以随机顺序返回元素的Redis命令 (如SMEMBERS(因为Redis集合是无序的))在从Lua调用时具有不同的行为,并在将数据返回到Lua脚本之前经历无声的词典排序过滤器。因此,redis.call("smembers",KEYS[1])将始终以相同的顺序返回Set元素,而从普通客户端调用的相同命令可能会返回不同的结果,即使该键包含完全相同的元素。
  • Lua伪随机数生成函数,math.random并且 math.randomseed被修改以便每次执行新的脚本时始终具有相同的种子。这意味着math.random如果math.randomseed不使用脚本,每次执行脚本时,调用将始终生成相同的数字序列。
    但是用户仍然可以使用以下简单的技巧编写随机行为的命令。想象一下,我想写一个Redis脚本,用N个随机整数填充一个列表。

我可以从这个小小的Ruby程序开始:

require 'rubygems'
require 'redis'
 
r = Redis.new
 
RandomPushScript = <<EOF
local i = tonumber(ARGV[1])
local res
while (i > 0) do
res = redis.call('lpush',KEYS[1],math.random())
i = i-1
end
return res
EOF
 
r.del(:mylist)
puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])

每次执行该脚本时,结果列表都将具有以下元素:

> lrange mylist 0 -1
1) "0.74509509873814"
2) "0.87390407681181"
3) "0.36876626981831"
4) "0.6921941534114"
5) "0.7857992587545"
6) "0.57730350670279"
7) "0.87046522734243"
8) "0.09637165539729"
9) "0.74990198051087"
10) "0.17082803611217"

为了使它成为一个纯函数,但仍然要确保每个脚本的调用都会导致不同的随机元素,我们可以简单地向脚本添加一个额外的参数,这个参数将用于播种Lua伪随机数发电机。新的脚本如下:

RandomPushScript = <<EOF
local i = tonumber(ARGV[1])
local res
math.randomseed(tonumber(ARGV[2]))
while (i > 0) do
res = redis.call('lpush',KEYS[1],math.random())
i = i-1
end
return res
EOF
 
r.del(:mylist)
puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))

我们在这里做的是发送PRNG的种子作为论据之一。这样,给定相同参数的脚本输出将是相同的,但我们正在改变每个调用中的一个参数,生成随机种子客户端。种子将作为复制链接和“附加文件”中的参数之一传播,以保证在重新加载AOF或从属进程处理脚本时将生成相同的更改。

注意:这种行为的一个重要部分是Redis实现的PRNG,math.random并math.randomseed保证具有相同的输出,而不管运行Redis的系统的体系结构如何。32位,64位,大端和小端系统都将产生相同的输出。

复制命令而不是脚本

从Redis 3.2开始,可以选择另一种复制方法。我们可以复制脚本生成的单个写入命令,而不是复制整个脚本。我们将这个脚本称为复制。

在这种复制模式下,当执行Lua脚本时,Redis收集由Lua脚本引擎执行的所有实际修改数据集的命令。当脚本执行结束时,脚本生成的命令序列被包装到一个MULTI / EXEC事务中,并被发送到从属和AOF。

根据用例,这在几个方面是有用的:

  • 当脚本计算速度慢时,但是这些效果可以通过一些写入命令来总结,重新计算从属脚本或重新加载AOF时,这是一个耻辱。在这种情况下只复制脚本的效果要好得多。
  • 当启用脚本效果复制时,关于非确定性功能的控件被禁用。例如,您可以在任何地方随意使用脚本中的TIME 或SRANDMEMBER命令。
  • 在这种模式下的Lua PRNG在每个呼叫中随机播种。
    为了启用脚本特效复制,您需要在脚本操作之前发出以下Lua命令:
redis.replicate_commands()

如果启用了脚本特效复制,则该函数返回true;否则,如果在脚本已经调用某个写入命令后调用该函数,则返回false,并使用正常的整个脚本复制。

命令的选择性复制

当选择脚本特效复制(参见上一节)时,可以更多地控制命令复制到从站和AOF的方式。这是一个非常先进的功能,因为滥用可以通过违反主控,从属和AOF都必须包含相同的逻辑内容的合同来造成破坏。

然而,这是一个有用的功能,因为有时我们只需要在主服务器上执行某些命令来创建中间值。

在我们执行两个集合之间的交集的Lua脚本中思考。选取五个随机元素,并用这五个随机元素创建一个新的集合。最后,我们删除表示两个原始集之间交集的临时密钥。我们要复制的只是五行元素的创造。还复制创建临时密钥的命令是没有用的。

因此,Redis 3.2引入了一个新的命令,该命令仅在启用脚本特效复制时才起作用,并且能够控制脚本复制引擎。redis.set_repl()如果禁用脚本特技复制,则调用该命令并在调用时失败。

该命令可以用四个不同的参数来调用:

redis.set_repl(redis.REPL_ALL) -- Replicate to AOF and slaves.
redis.set_repl(redis.REPL_AOF) -- Replicate only to AOF.
redis.set_repl(redis.REPL_SLAVE) -- Replicate only to slaves.
redis.set_repl(redis.REPL_NONE) -- Don't replicate at all.

默认情况下,脚本引擎始终设置为REPL_ALL。通过调用此函数,用户可以打开/关闭AOF和/或从属复制,并稍后按照自己的意愿将其复位。

一个简单的例子如下:

redis.replicate_commands() -- Enable effects replication.
redis.call('set','A','1')
redis.set_repl(redis.REPL_NONE)
redis.call('set','B','2')
redis.set_repl(redis.REPL_ALL)
redis.call('set','C','3')

在运行上面的脚本之后,结果是在从属和AOF上只创建了A和C键。

全局变量保护

Redis脚本不允许创建全局变量,以避免数据泄露到Lua状态。如果脚本需要在调用之间保持状态(非常罕见),应该使用Redis键。

当尝试全局变量访问时,脚本被终止,EVAL返回一个错误:

redis 127.0.0.1:6379> eval 'a=10' 0
(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

访问一个不存在的全局变量会产生类似的错误。

使用Lua调试功能或其他方法(例如修改用于实现全局保护的元表来避免全局保护)并不难。然而,意外地做到这一点很困难。如果用户使用Lua全局状态混淆,则AOF和复制的一致性不能保证:不要这样做。

注意Lua新手:为了避免在脚本中使用全局变量,只需使用local关键字声明要使用的每个变量。

在脚本中使用SELECT

可以像使用普通客户端一样在Lua脚本中调用SELECT。但是,Redis 2.8.11和Redis 2.8.12之间行为的一个细微方面发生了变化。在2.8.12发行版之前,由Lua脚本选择的数据库作为当前数据库被传输到调用脚本。从Redis 2.8.12开始,由Lua脚本选择的数据库只影响脚本本身的执行,但不会修改调用脚本的客户端选择的数据库。

修补程序级别版本之间的语义变化是必需的,因为旧的行为本身与Redis复制层不兼容,并且是错误的原因。

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

base 库。
table 库。
string 库。
math 库。
struct 库。
cjson 库。
cmsgpack 库。
bitop 库。
redis.sha1hex 功能。
redis.breakpoint and redis.debug函数在Redis Lua调试器的上下文中。

每个Redis实例都保证具有上述所有库,因此您可以确保Redis脚本的环境始终如一。

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

结构

struct是一个在Lua中打包/解包结构的库。

Valid formats:
> - big endian
< - little endian
![num] - alignment
x - pading
b/B - signed/unsigned byte
h/H - signed/unsigned short
l/L - signed/unsigned long
T - size_t
i/In - signed/unsigned integer with size `n' (default is size of int)
cn - sequence of `n' chars (from/to a string); when packing, n==0 means
the whole string; when unpacking, n==0 means use the previous
read number as the string length
s - zero-terminated string
f - float
d - double
' ' - ignored

例:

127.0.0.1:6379> eval 'return struct.pack("HH", 1, 2)' 0
"\x01\x00\x02\x00"
127.0.0.1:6379> eval 'return {struct.unpack("HH", ARGV[1])}' 0 "\x01\x00\x02\x00"
1) (integer) 1
2) (integer) 2
3) (integer) 5
127.0.0.1:6379> eval 'return struct.size("HH")' 0
(integer) 4

CJSON

CJSON库在Lua中提供了非常快速的JSON操作。

例:

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

cmsgpack

cmsgpack库在Lua中提供简单快速的MessagePack操作。

例:

127.0.0.1:6379> eval 'return cmsgpack.pack({"foo", "bar", "baz"})' 0
"\x93\xa3foo\xa3bar\xa3baz"
127.0.0.1:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz"
1) "foo"
2) "bar"
3) "baz"

bitop

Lua位操作模块在数字上添加按位操作。自2.8.18版以来,它可用于Redis中的脚本。

例:

127.0.0.1:6379> eval 'return bit.tobit(1)' 0
(integer) 1
127.0.0.1:6379> eval 'return bit.bor(1,2,4,8,16,32,64,128)' 0
(integer) 255
127.0.0.1:6379> eval 'return bit.tohex(422342)' 0
"000671c6"

它支持其他几个功能: bit.tobit,bit.tohex,bit.bnot,bit.band,bit.bor,bit.bxor, bit.lshift,bit.rshift,bit.arshift,bit.rol,bit.ror,bit.bswap。所有可用的功能都记录在Lua BitOp文档中

redis.sha1hex

执行输入字符串的SHA1。

例:

127.0.0.1:6379> eval 'return redis.sha1hex(ARGV[1])' 0 "foo"
"0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"

从脚本发出Redis日志

可以使用redis.log函数从Lua脚本写入Redis日志文件 。

redis.log(loglevel,message)

loglevel 是其中之一:

redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING

它们直接对应于正常的Redis日志级别。只有使用等于或大于当前配置的Redis实例日志级别的日志级别通过脚本发出的日志才会被发出。

该message参数是一个简单的字符串。例:

redis.log(redis.LOG_WARNING,"Something is wrong with this script.")

将生成以下内容:

[32343] 22 Mar 15:21:39 # Something is wrong with this script.

沙箱和最大的执行时间

脚本不应尝试访问外部系统,如文件系统或任何其他系统调用。脚本只能在Redis数据上运行并传递参数。

脚本也受到最大执行时间(默认为5秒)的限制。这个默认的超时时间很长,因为一个脚本通常应该运行在毫秒以下。限制主要是为了处理在开发过程中产生的意外的无限循环。

可以通过redis.conf或使用CONFIG GET / CONFIG SET命令修改脚本以毫秒级精度执行的最长时间。影响最大执行时间的配置参数被调用 lua-time-limit。

当脚本达到超时时,不会由Redis自动终止,因为这违反了Redis与脚本引擎之间的合约,以确保脚本是原子的。中断脚本意味着可能将数据集保留为半写入数据。由于这个原因,当脚本执行超过指定的时间时,会发生以下情况:

  • Redis记录脚本运行时间过长。
  • 它开始再次接受来自其他客户端的命令,但将向所有发送正常命令的客户端回复BUSY错误。唯一允许的命令是SCRIPT KILL和SHUTDOWN NOSAVE。
  • 可以使用SCRIPT KILL命令终止一个只执行只读命令的脚本。这并不违反脚本语义,因为没有数据被脚本写入数据集。
  • 如果脚本已经调用了写入命令,则唯一允许的命令变为 SHUTDOWN NOSAVE停止服务器而不保存磁盘上的当前数据集(基本上服务器被中止)。

EVALSHA在流水线的上下文中

在流水线请求的上下文中执行EVALSHA时要小心,因为即使在流水线中,也必须保证命令的执行顺序。如果EVALSHA将返回一个NOSCRIPT错误,则该命令不能在稍后重新发布,否则违反了执行顺序。

客户端库实现应采取以下方法之一:

  • 在管道环境中始终使用简单的EVAL。

  • 累积所有要发送到管道中的命令,然后检查EVAL 命令并使用SCRIPT EXISTS命令检查是否所有脚本都已经定义。如果没有,请根据需要在管道顶部添加SCRIPT LOAD命令,并对所有EVAL呼叫使用EVALSHA。

调试Lua脚本

从Redis 3.2开始,Redis支持原生Lua调试。Redis Lua调试器是一个远程调试器,由一个服务器(Redis本身)和一个默认的客户端组成redis-cli。

Lua调试器在Redis文档的Lua脚本调试部分进行了描述。

标签:脚本,事务,调用,Lua,redis,Redis,命令
From: https://www.cnblogs.com/fuqian/p/17336124.html

相关文章

  • 虹科干货 | 打破传统!金融界黑科技—虹科Redis企业版数据库
    金融行业数字化转型浪潮来袭,客户需求也正加速向在线金融服务转移。金融机构想要实现现代化改造技术堆栈,为客户提供实时交互、欺诈检测等一系列个性化创新服务,就必须重视遗留系统和传统数据库架构“老年病”问题!面对数字化颠覆带来的挑战和压力,接受变化是关键!一些极力打破传统束......
  • Day 25 25.2 Scrapy框架之分布式爬虫(scrapy_redis)
    分布式爬虫(scrapy_redis)分布式爬虫是指将一个大型的爬虫任务分解成多个子任务,由多个爬虫进程或者多台机器同时执行的一种爬虫方式。在分布式爬虫中,每个爬虫进程或者机器都具有独立的爬取能力,可以独立地爬取指定的网页或者网站,然后将爬取到的数据进行汇总和处理。分布式爬......
  • mysql,redis,mongodb常用命令
    MySQL常用命令:1.mysql-uusername-p:以指定用户身份登录MySQL数据库。2.showdatabases:列出所有数据库。3.usedatabase_name:选择指定的数据库。4.showtables:列出当前数据库中的所有表。5.describetable_name:显示指定表的结构。6.select*fromtable_name:查询指......
  • Redis为什么快?
    redis是一个开源的使用ANSIC语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。和Memcached类似。redis支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。它的速度快主要归功于以下几个方面:内存数......
  • go项目中数据库连接,以及redis连接
     文件:common/mysql.go数据库连接文件packagecommonimport("gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/schema""log")varDB*gorm.DB//全局定义DBvardbErrerror//定义数据库错误funcinit(){dsn:=......
  • 【Redis】面试题 GEO地理位置信息
    目录面试1http协议详情,http协议版本,http一些请求头2GET请求和POST请求的区别3如何实现服务器给客户端发送消息,websocket是什么?4悲观锁和乐观锁,如何实现今日内容1GEO地理位置信息面试1http协议详情,http协议版本,http一些请求头 -特点: 1基于请求响应--》服务端不能主......
  • redis----day03( )
    面试面试如何聊#第一面(笔试): -办公室做题:拍照,发群里---》自己课搜,同学帮着搜----》往上写-问不答了,可以直接面试吗?#第二面:正式面试(开启手机录音,放口袋中) -一个人,多个人坐你对面-最重要的:自我介绍(自己提前写出来,500字,尽可能展现自己的优势) -面试官您......
  • Spring05_Spring事务
    一、JdbcTemplate工具​ JdbcTemplate类是Spring框架提供一个用于操作数据库的模板类,JdbcTemplate类支持声明式事务管理。该类提供如下方法来执行数据库操作。​ 1、queryForObject查询单个对象​ queryForObject(Stringsql,RowMappermapper,Object[]args)​ 2、que......
  • centos9 redis安装报错(实在无解使用方法)
    报错如下[root@centosbin]#./redis-server./redis-server:errorwhileloadingsharedlibraries:libssl.so.1.1:cannotopensharedobjectfile:Nosuchfileordirectory使用的解决命令yummakecacheyum-yinstall*openssl*原过程root@centosbin]#lsredi......
  • MySQL数据库事务
    什么是数据库事务数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。数据库事务的四大特性数据库事务的四大特性和程序事务相同,......