天天看点

Redis事务:用法,常见错误和API

MULTI

EXEC DISCARD

WATCH

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

  • 事务中的所有命令都被序列化并按顺序执行。在执行Redis事务的过程中,不会发生由另一个客户端发出的请求。这保证了命令作为一个单独的操作被执行。
  • 要么所有的命令都没有被处理、要么没有命令被执行,所以Redis事务也是原子的。 命令,触发事务中的所有命令的执行,因此,如果客户端在执行 命令前,在事务的上下文中丢失与服务器的连接,没有命令会被执行,然而如果 命令被调用时,所有的操作都被执行。使用 append-only file  ,Redis确保使用单个写操作,系统调用将事务写入磁盘。但是,如果Redis服务器崩溃或被系统管理员以某种方式杀死,则可能只有部分命令被执行。Redis会在重新启动时检测到这种情况,会退出并显示错误信息。使用该

    redis-check-aof

    工具,可以修复将删除部分事务的append only file,以便服务器可以重新启动。

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

Redis事务--用法

使用

命令开启Redis事务。该命令总是回复

OK

。此时用户可以发出多个命令。Redis不会执行这些命令,而是将它们排队。一旦

被调用,所有的命令被执行。

调用

刷新事务队列,并且退出事务。

以下示例递增键

foo

bar

原子。

  1. > MULTI

  2. OK

  3. > INCR foo

  4. QUEUED

  5. > INCR bar

  6. QUEUED

  7. > EXEC

  8. 1) (integer) 1

  9. 2) (integer) 1

从上面的会话中可以看出,

EXEC

返回一个响应数组,其中每个元素都是事务中单个命令的回复,这与命令发出的顺序相同。

当Redis连接处于

MULTI

请求的上下文中时,所有命令都将回复该字符串

QUEUED

(从Redis协议的角度来看,这是作为状态回复的发送)。当

EXEC

被调用时,queued命令被有计划地执行。

事务中的错误

在事务过程中,可能遇到两种命令错误:

  • 命令可能无法进入队列,所以在调用

    EXEC

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

    maxmemory

    指令,具有内存限制)。

  • 在 调用

    EXEC 

    命令之后,命令可能会失败,例如,我们对具有错误值的键执行操作(例如,针对string值调用list 操作)。

EXEC

调用之前,客户端通过检查排队命令的返回值,来感知第一类错误:如果命令使用QUEUED进行响应,则排队正确,否则Redis返回错误。如果排队命令时发生错误,大多数客户端将中止放弃该事务。

但是从Redis 2.6.5开始,服务器会记住在命令不断累加执行过程中出现的错误,将

EXEC

命令期间会拒绝事务,并返回错误,还会自动丢弃事务。

在Redis 2.6.5之前,这种行为只是在成功排队的命令子集内执行事务,以防客户端调用

EXEC

而不管以前的错误。新的行为使得将transactions与pipelining,混合在一起变得更加简单,因此整个事务可以一次发送,一次读取所有的回复。

在 

EXEC 

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

这在协议层面更加清晰。在以下示例中,即使语法正确,一个命令在执行时也会失败:

  1. Trying 127.0.0.1...

  2. Connected to localhost.

  3. Escape character is '^]'.

  4. MULTI

  5. +OK

  6. SET a 3

  7. abc

  8. +QUEUED

  9. LPOP a

  10. +QUEUED

  11. EXEC

  12. *2

  13. +OK

  14. -ERR Operation against a key holding the wrong kind of value

EXEC

返回了两个元素的

Bulk string reply

,其中一个是

OK

代码,另一个是

-ERR

答复。这是由客户端库找到一个明智的方式,来提供错误给用户。

需要注意的是,即使命令失败,队列中的所有其他命令也会被处理 --Redis 不会停止命令的处理。

另一个例子,再次用telnet使用Wire协议,显示了如何报告语法错误:

  1. MULTI

  2. +OK

  3. INCR a b c

  4. -ERR wrong number of arguments for 'incr' command

  • 这次由于语法错误,错误的

    INCR

    命令根本没有排队。

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

如果您有关系数据库背景,那么Redis命令在事务处理期间可能会失败,但Redis仍然会执行事务的其余部分而不是回滚事务,这可能对您来说看起来很奇怪。

但是对于这种行为有很好的观点:

  • 如果使用错误的语法调用Redis命令(并且在命令排队期间无法检测到问题),或者针对保存错误数据类型的键,则Redis命令可能会失败:这意味着,实际上,失败的命令是编程错误的结果,以及在开发过程中很可能被检测到的一种错误,而不是在生产中。

  • Redis的内部简化和更快,因为它不需要回滚的能力。

反对Redis观点的一个观点是错误发生了,但是应该注意的是一般情况下回滚并不能避免编程错误。例如,如果一个查询增加了一个键而不是1,或者增加了错误的键,那么回滚机制就没有办法提供帮助。鉴于没有人能够挽救程序员的错误,并且Redis命令失败所需的错误类型不太可能进入生产环境,所以我们选择了不支持错误回滚的更简单快捷的方法。

DISCARD

命令

https://redis.io/commands/discard

可以使用

DISCARD

来中止事务。在这种情况下,不执行任何命令,并且连接状态恢复正常。

  1. > SET foo 1

  2. OK

  3. > MULTI

  4. OK

  5. > INCR foo

  6. QUEUED

  7. > DISCARD

  8. OK

  9. > GET foo

  10. "1"

乐观锁定使用check-and-set

WATCH

用于为Redis事务提供检查和设置(check-and-set)(CAS)行为。

被WATCH

监控的键,可以检测他们的变化。如果在

EXEC

命令之前,至少修改了一个被监视的键( watched key ),则整个事务将中止,

EXEC

返回一个

Null reply

以通知事务失败。

例如,假设我们需要将键的值自动递增1(让我们假设Redis没有

INCR

)。

第一次尝试可能如下:

  1. val = GET mykey

  2. val = val + 1

  3. SET mykey $val

只有当我们有一个客户端在特定的时间执行操作时,这才能可靠地工作。如果多个客户端尝试在大约同一时间递增key,则会出现竞争状况。例如,客户端A和B将读取旧值,例如10,两个客户端的值将递增为11,最后将

SET设置

为键的值。所以最终的价值将是11而不是12。

感谢

WATCH,

我们能够很好地模拟这个问题:

  1. WATCH mykey

  2. val = GET mykey

  3. val = val + 1

  4. MULTI

  5. SET mykey $val

  6. EXEC

使用上面的代码,如果有竞争条件,另一个客户端在我们调用

WATCH

和调用

EXEC

之间,修改val的值,事务将失败。

我们不得不重复这个操作,希望这次我们不会再有新的竞争。这种形式的锁定称为乐观锁定,是一种非常强大的锁定形式。在许多用例中,多个客户端将访问不同的key,因此碰撞是不太可能的 - 通常不需要重复操作。

  1. 悲观锁(Pessimistic Lock), 

  2. 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,

    这样别人想拿这个数据就会block直到它拿到锁。

  3. 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

  4. 乐观锁(Optimistic Lock), 

  5. 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,

    可以使用版本号等机制。

  6. 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

  7. 两种锁各有优缺点,

  8. 不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

  9. 但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

WATCH

解释

  • 那么

    WATCH

    真的是什么?

  • 这是一个使

    EXEC

    有条件的命令:只有在任何被

    WATCH的

    键没有被修改的情况下,我们才会要求Redis执行事务。(但是,他们可能会被事务内相同的客户端没有放弃它而改变。)否则事务不会进入的。(请注意:如果您

    WATCH

    一个存在有效期的键<volatile key>,键过期后,

    EXEC

    仍然可以工作。)

  • WATCH

    可以被多次调用。简单地说,所有的

    WATCH

    调用,都将具有监视从调用开始,直到

    EXEC

    被调用期间变化的效果。您也可以将任意数量的key发送给一个

    WATCH

    调用。

  • EXEC

    被调用时,不管事务是否中止,所有的key都被UNWATCH。另外,当一个客户端连接关闭,所有的key也都被

    UNWATCH

  • 也可以使用

    UNWATCH

    命令(不带参数)来刷新所有watched keys。有时候,我们乐观地锁定了几个键,这是非常有用的,因为可能我们需要执行一个事务来改变这些键,但是在读完这些键的当前内容之后,我们不想继续。发生这种情况时,我们只需调用

    UNWATCH,

    以便连接可以自由地用于新的事务。

使用

WATCH

来实现ZPOP

一个很好的例子来说明

WATCH

如何被用来创建新的原子操作,否则Redis是不支持实现ZPOP,这是一个以原子的方式从一个有序集合中以较低分数弹出元素的命令。这是最简单的实现:

  1. WATCH zset

  2. element = ZRANGE zset 0 0

  3. MULTI

  4. ZREM zset element

  5. EXEC

  • 如果

    EXEC

    失败(即返回一个

    空的答复

    ),我们只是重复这个操作。

Redis脚本和事务

  • 一个

    Redis的脚本

    是有事务性的,所以一切都可以用Redis的事务做的,你也可以做一个脚本,通常脚本会更简单,更快速。

  • 这是由于在Redis 2.6中引入了脚本,事务早已存在。然而,我们不可能在短时间内取消对事务的支持,因为即使不采用Redis脚本,在语义上似乎也是合适的,但仍然有可能避免竞争状况,特别是因为Redis事务的实施复杂性是最小的。

  • 然而,在不远的将来,我们将看到整个用户群只是使用脚本。如果发生这种情况,我们可能会弃用并最终删除事务。

  • 更多脚本内容:

    http://blog.csdn.net/fly910905/article/details/78955343

Redis事务命令

  1. MULTI:类似于mysql中的BEGIN; 标记一个事务块的开始。

  2. EXEC:类似于COMMIT; 执行所有事务块内的命令。

  3. DISCARD:类似于ROLLBACK;取消事务,放弃执行事务块内的所有命令。

  4. WATCH key [key ...] : 则是用于来实现mysql中类似锁的功能。 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

  5. UNWATCH: 

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

来源: 

https://redis.io/topics/transactions