任何数据库中的事务都让人怯步。它不仅要求理解存储的内容,还需要理解存储的时间。与无数抽象层能够保护用户免于复杂性的快乐世界不同,事务要求深入了解。Redis 在这方面并不罕见。事实上,它对事务思考方式的不同导致很多人都说它根本没有事务。Redis 拥有事务,只是它的方法与你可能从小就习惯的回滚方式完全不同。
若要大致了解事务,你需要了解 Redis 的一些事项。其一是单线程(好吧,但随着逐渐增多的例外情况而存在 exceptions 列表)。这意味着如果它正在做某事,那就是它唯一在做的事。当然,对于 Redis 来说,“做某事”的最佳衡量单位是毫秒或纳秒。其次,请记住,Redis 具有可调节的耐用性,有些选项可提供很好的耐用性,而有些选项则完全是短暂的。这显然会对事务产生影响。第三,它不具备回滚功能,但如果某个键在事务开始前更改,它可能会使事务失败。这种控制事务的逆向方式使你可以将数据拉回到客户端,并对数据进行逻辑评估,以确保在事务开始前数据没有更改。
对于大多数人来说,最大的绊脚石是:事务中的各个命令可能会出错。这可能导致每个命令都已执行,但一个或多个命令执行都出现错误的情况。了解这一点是理解和控制这些情况的关键。
首先,让我们看看 Redis 中的语法错误和语义错误之间的区别。语法错误就是语法本身就存在错误,并且这种错误无需访问数据即可得知。例如,发送不存在的命令或违反参数键/值序列。语法错误会导致事务无法启动。
看这个示例
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> STE foo bar
(错误) ERR 命令“STE”未知,参数从以下内容开始:`foo`, `bar`,
127.0.0.1:6379> EXEC
(错误) EXECABORT 由于之前错误,事务已丢弃。
Redis 知道 STE 不是一条命令,因此它可以将其全部抛出,甚至不必评估底层数据,并将拒绝整个 MULTI/EXEC 块。Redis 立即捕获的其他语法错误包括参数数量和(部分)参数模式问题。这些事务错误非常安全 - 使用 MULTI,所有后续命令都会排队,等待 EXEC 调用,因此会触发 EXECABORT 的任何操作都不会执行。
下一个,更宽泛的类别是语义事务错误,其行为与语法错误不同。在 Redis 无法在静态环境中捕获错误时出现这种情况,通常需要 Redis 对底层数据进行求值。此行为的一个经典示例如下
127.0.0.1:6379> SET foo "hello world"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR foo
QUEUED
127.0.0.1:6379> SET bar baz
QUEUED
127.0.0.1:6379> EXEC
1) (error) ERR value is not an integer or out of range
2) OK
显然,一个有实际能力的开发者绝不会有意地去递增字符串“hello world”。但当您考虑到接受用户输入却将其期望为一个数字但未被验证为数字的端到端应用程序时,这个示例就会变得更贴合实际。此处另一个隐秘的问题是,第二个 SET 命令被执行了,而 INCR 却没有。这并不是大多数开发者想要或希望从事务中获得的结果。好消息是,使用 WATCH 命令可以控制这类错误。借助 WATCH,您可以观察变更的键,并且如果出现了变更,它将立即向客户端发送错误。这非常有效,可以让您将数据显示给客户端并对其进行求值。在此情况下,您可以求值 foo 是否可递增(又称为整数)。
看看以下伪代码
1 > SET foo 1234
2 > WATCH foo
3 如果 watchError 返回 2,否则继续
4 > GET foo
5 如果第 4 行的结果不是一个数字,则抛出一个错误,否则继续
6 > MULTI
7 > INCR foo
8 > SET bar baz
9 > EXEC
现在,如果任何已连接客户端在第 3 行和第 9 行之间更改了 foo(但不包括在第 9 行,因为这允许事务本身更改所监控的数据),该进程会跳回到第 2 行。在第 4 行后,它将在应用程序中查看 foo 的内容并确定是否有可能增加。因为它会在出现变更时立即重试,所以这样可以确保不会因为 foo 的内容错误而出现任何错误。
这涉及到 Redis 处理整数与浮点数的工作方式。我之前就此做过一点讨论,但基本上,如果数据是浮点数,那么你必须使用 INCRBYFLOAT 而非 INCR 或 INCRBY。然而,如果该值恰好等于一个整数,那么你可以再次使用 INCR 或 INCRBY。需要指出的是,你可以使用 INCRBYFLOAT 使非浮点数增加非浮点数,这不会影响任何内容。从本质上说,INCR 系列命令绝不会创建“1.0”。在 MULTI / EXEC 情况下,使用 INCRBYFLOAT 比使用 INCR 或 INCRBY 更安全,因为它不会创建错误。在该情况下使用 INCR 或 INCRBY 的唯一原因是,如果你有浮点数,则确保会引发错误 – 在该情况下,无论如何你都必须使用 WATCH。
关于以这种方式评估数据的一点说明:这并不是免费的。如果你将数字提取到客户端,则评估很小。但假设你有一个未知键,你评估它以确定是否合适,但数字占用 5-7 个字节,而你却有 500 MB 的二进制 Blob。在网络上传输并评估这需要一段时间。这是一个极端情况,但需要牢记这一点。
相同的模式(WATCH / MULTI / EXEC)可用于防范在你的事务结果中出现的命令/类型不匹配(又称 WRONGTYPE)。对于 INCR 问题,你必须通过某些客户端数据评估来追踪一个字符串包含可供 INCR 或 INCRBYFLOAT 使用的数据。或者,你可以更加直接,仅使用 TYPE 命令来评估该数据类型而不是数据本身(巧妙地避免了意外的 500 MB 评估)。
让我们看看这是如何工作的
1 > HSET foo bar 1234
2 > WATCH foo
3 如果 watchError 返回 2,否则继续
4 > TYPE foo
5 如果第 4 行的结果不是“哈希”,则引发错误,否则继续
6 > MULTI
7 > HSET foo baz helloworld
8 > HLEN foo
9 > EXEC
在这种情况下,你知道你的 HSET 和 HLEN 命令将会起作用,因为你在运行 EXEC 时验证了类型且类型没有发生变更。当然,还有 HINCRBY/HINCRBYFLOAT 命令,你将需要结合使用前一个技术,使用 HGET 而非 GET 来针对 INCR 能力评估字段。
有趣的是,BITFIELD具有处理越界值的控件。您可以根据命令本身中声明的类型进行换行或饱和,或者可以使用 FAIL 选项。奇怪的是,这不会产生错误,而是忽略 INCR,保持之前的数值。但是,BITFIELD 确实还有另一点需要注意。该命令相当复杂,而 Redis 只执行一些基本的语法检查,因此,如果您传入错误的语法,它不会在将命令添加到事务时评估,而是在事务内执行命令时评估。这会导致语法错误,而不会取消事务,并且会在值中返回该错误。防止此类错误的唯一方法是在将语法抛入事务之前,确保语法在客户端级别正确无误。
总体而言,Redis 对数据不需要初始化步骤。对某些人来说,这很令人担忧,但对大多数人来说,他们明白。如果一个 key 为空,并且您向其中添加数据,则新创建的数据结构将由用于添加数据的命令定义。随着模块的出现,这种情况已开始慢慢发生改变,而模块使此惯例不那么明确。例如,RedisGraph 要求您添加一些节点和关系,然后才能查询图。
看这个示例
> MULTI
OK
> GRAPH.QUERY mygraph "MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r,t"
QUEUED
> LPUSH teamqueries Yamaha
QUEUED
> EXEC
1) (error) key doesn't contains a graph object.
2) (integer) 1
实际上,您也可以在 Redis Streams 中看到这种行为。例如
> MULTI
OK
> XGROUP CREATE my-stream my-consumer-group $
QUEUED
> LPUSH my-stream-groups my-consumer-group
QUEUED
> EXEC
1) (error) ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.
2) (integer) 1
值得庆幸的是,通过使用上述 WATCH、TYPE、MULTI/EXEC 模式,可以非常简单地解决这些问题。您只需检查类型匹配(使用这些示例),然后匹配 graphdata 或 stream。
使用 MULTI 和 EXEC 的 Redis 事务实际上并不复杂。但是,您确实需要注意一些注意事项,以确保您的事务按照预期行为。如果您从本文中学到任何东西,请记住您可以假设引用 key 的类型、内容或存在性没有任何内容,从而进行防弹 Redis 事务。