dot Redis 8 已发布——且它是开源的

了解更多

关于类型和事务

任何数据库中的事务都令人望而生畏。它不仅需要理解存储了什么,还需要理解何时存储。与无数抽象层可以将您从复杂性中屏蔽开的快乐世界不同,事务要求您深入了解。在这方面,Redis 并不特殊。事实上,它完全不同的事务思考方式导致许多人说它根本没有事务。Redis 确实有事务,只是它的方法与您可能熟悉的那些带回滚的方法完全不同。

要从一万英尺高空看事务,您需要了解关于 Redis 的几件事。首先,它是单线程的(当然有一些例外,而且这个列表可能还在增长)。这意味着如果它正在做某事,那就只是在做这件事。当然,在 Redis 中,“做某事”最好以毫秒或纳秒为单位衡量。其次,请记住 Redis 具有可调的持久性,有些选项提供非常好的持久性,有些则完全是瞬时的。这显然会影响事务。第三,它缺乏回滚,但如果在事务开始前键发生变化,它会导致事务失败。这种颠倒的事务控制方式让您可以将数据拉回到客户端,并通过逻辑评估来确保数据在事务开始前没有发生变化。

对大多数人来说,最大的绊脚石是事务中的单个命令可能存在错误。这可能导致每个命令都执行了,但一个或多个命令执行时出现了错误。了解这一点是理解和控制这些情况的关键。

首先,我们来看看 Redis 中语法错误和语义错误之间的区别。语法错误就是这样——语法本身是错误的,无需访问数据即可知道。例如,发送一个不存在的命令或违反参数键/值顺序。语法错误会导致事务根本无法开始。

看这个例子

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> STE foo bar
(错误) ERR unknown command `STE`, with args beginning with: `foo`, `bar`,
127.0.0.1:6379> EXEC
(错误) EXECABORT Transaction discarded because of previous errors.

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) (错误) 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 行之间(但不包括第 9 行期间,因为事务本身可以改变被 WATCH 的数据)有任何连接的客户端改变了 foo,进程将跳回第 2 行。在第 4 行之后,它将在应用程序中查看 foo 的内容,并确定是否可以递增。由于一旦发生变化它会立即重试,这确保您不会因 foo 的内容错误而收到任何错误。

这与 Redis 处理整数/整型数和浮点数的方式有关。我之前已经稍微提过一点,但基本上,如果您的数据是浮点数,那么您必须使用 INCRBYFLOAT 而不是 INCR 或 INCRBY。然而,如果该值恰好等于一个整数,那么您可以再次使用 INCR 或 INCRBY。需要注意的是,您可以使用 INCRBYFLOAT 对非浮点数进行非浮点数的递增,这不会影响任何东西。本质上,INCR 系列命令绝不会创建“1.0”。在 MULTI / EXEC 场景下,使用 INCRBYFLOAT 比 INCR 或 INCRBY 更安全,因为它不会产生错误。在这种情况下,使用 INCR 或 INCRBY 的唯一原因是确保如果遇到浮点数时会抛出错误——在这种情况下,您无论如何都必须使用 WATCH。

关于这种数据评估方式的一点注意:它不是免费的。如果您将数字拉到客户端进行评估,开销很小。但假设您有一个未知的键,您对其进行适用性评估,结果不是组成数字的 5-7 字节,而是 500MB 的二进制大对象。这将需要一些时间通过网络传输并进行评估。这虽然是一个边缘情况,但值得记住。

同样的模式 (WATCH / MULTI / EXEC) 也可用于防范命令/类型不匹配(即 WRONGTYPE),这些不匹配可能会出现在您的事务结果中。对于 INCR 问题,您必须通过某种客户端数据评估来跟踪字符串是否包含可以被 INCR 或 INCRBYFLOAT 的数据。或者,您可以更直接地使用 TYPE 命令来评估数据的类型而不是数据本身(巧妙地避免了意外的 500MB 评估)。

我们来看看这可能如何运作

1 > HSET foo bar 1234
2 > WATCH foo
3 如果 watchError 则跳转到 2 否则继续
4 > TYPE foo
5 如果第 4 行的结果不是“hash”,则抛出错误,否则继续
6 > MULTI
7 > HSET foo baz helloworld
8 > HLEN foo
9 > EXEC

在这种情况下,您知道您的 HSET 和 HLEN 命令会起作用,因为您已经验证了类型,并且在您运行 EXEC 时类型没有改变。当然,还有 HINCRBY/HINCRBYFLOAT 命令的问题,您需要结合先前的技术,使用 HGET 而不是 GET 来评估字段是否可以递增。

有趣的是,BITFIELD 提供了处理越界值的控制。您可以根据命令本身声明的类型进行环绕或饱和处理,或者您可以使用 FAIL 选项。奇怪的是,这不会产生错误,而是忽略了 INCR 操作,保持值不变。然而,BITFIELD 确实还有另一个陷阱。该命令相当复杂,并且 Redis 只进行一些基本的语法检查,因此如果您传递了错误的语法,这不会在添加到事务时进行评估,而是在事务内部执行该命令时进行评估。这会导致一个语法错误,该错误不会取消事务,而是作为返回值的一部分返回。防止这类错误的唯一方法是在客户端级别确保您的语法正确,然后再将其放入事务中。

总的来说,Redis 不需要对数据进行初始化步骤。对一些人来说,这令人担忧,但对大多数人来说,他们理解。如果一个键是空的,您向其中添加数据,新创建的数据结构由用于添加数据的命令定义。随着模块的出现,这种情况开始慢慢改变,这使得这个约定变得不那么确定。例如,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) (错误) 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) (错误) 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 匹配。

Redis 使用 MULTI 和 EXEC 的事务实际上并不那么复杂。然而,您确实需要注意一些陷阱,以确保您的事务行为符合预期。如果您从本文中记住任何东西,请记住,通过不对引用键的类型、内容或存在性做出任何假设,您可以构建防弹级的 Redis 事务。