文章目录

  • 事务
  • 性质
  • 用法
  • 事务中的错误
  • 命令排队入队错误
  • 命令执行错误
  • 为什么Redis不支持回滚
  • DISCARD命令队列
  • WATCH实现乐观锁
  • Redis脚本和事务



相关命令

MULTI
EXEC
DISCARD
WATCH
UNWATCH

事务

事务是一组命令的集合。3

性质

  • 事务中的所有命令都被序列化并顺序执行。在Redis事务的执行过程中,永远不会发生另一个客户端发出的请求(命令不会加塞)。
  • 所有命令都将被执行,或者所有命令都不执行,因此Redis事务也是原子的(这句话有可能会引起争议)。

用法

使用MULTI命令输入Redis事务。该命令始终以答复OK。
此时,用户可以发出多个命令。Redis不会执行这些命令,而是将它们排队。
一旦调用EXEC,将执行所有命令。,并且EXEC返回了一个答复数组,其中每个元素都是事务中单个命令的答复,其发出顺序与命令相同。
如:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> GET k1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) "v1"
127.0.0.1:6379>

需要强调的是,MULTI之后的命令并不会执行,它们只是入队而已,知道EXEC命令发出,所有命令才会一起执行。

事务中的错误

命令排队入队错误

这种常见的错误有 ① 命令不存在 ② 命令参数的个数不对。
这种错误一旦发生,这个事务就会失败,也就是说事务中的命令全都不会执行
如:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> get b 1
(error) ERR wrong number of arguments for 'get' command
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 
127.0.0.1:6379> 
127.0.0.1:6379> get a
(nil)
127.0.0.1:6379> get c
(nil)

可见即使是正确的命令也没有执行成功。

从Redis 2.6.5开始,服务器会记住命令累积期间发生错误,并且将拒绝执行事务,并且在EXEC期间还会返回错误并自动丢弃该事务。

命令执行错误

常见的有,命令与键的类型冲突,给一个字符串类型的键自增但是键值不是数字,等等。

EXEC之后发生的错误不会以特殊方式处理:即使在事务期间某些命令失败,也会执行所有其他命令
也正是因为一点,让Redis事务的原子性受到质疑。

如:

127.0.0.1:6379> SET a hello
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR a
QUEUED
127.0.0.1:6379> SET b 2
QUEUED
127.0.0.1:6379> EXEC
1) (error) ERR value is not an integer or out of range
2) OK
127.0.0.1:6379> GET b
"2"
127.0.0.1:6379>

即使在INCR命令执行错误之后,可以看到之后的合法的命令仍然执行成功了。

为什么Redis不支持回滚

大致有三点原因:

  1. 程序员自身犯的错误是无法避免的。
  2. 不支持回滚可以使得Redis事务的实现更加高效。
  3. 发生错误一般发生在开发过程中,不太可能进入到生产环境中。

DISCARD命令队列

之前说,一旦客户端给Redis服务器发送一个MULTI此时,Redis受到的命令不会立即执行,而是存储在队列里。
也就是此时客户端和服务器的连接状态进入一种事务状态
DISCARD命令相反,它用来结束这种状态的,当然之前队列里的命令组也会被清空。

注意,此命令只在客户端和服务器之间有MULTI执行之后,才会返回OK,否则返回
(error) ERR DISCARD without MULTI

WATCH实现乐观锁

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

被WATCH的键,如果在事务执行(exec)前发生了变化,那么之后事务就不会执行,返回(null)。

下面的实现自增INCR命令组在多个客户端并发访问服务器的时候,就会产生竞争条件。
比如,一开始键的值的是10,向后两个客户端执行了自增命令,最终值应该是12。
但是,两个客户端可能先后拿到10,然后分别自增,最后分别赋值11。
就出现错误。

val = GET mykey
val = val + 1
SET mykey $val

可能初学者,有这样的想法,将这组命令当成一个事务执行,不就解决问题了吗。
不过,事务执行的结果是在exec之后才能拿到的,无法中途拿到(其次你也要知道的是 multi 之后也是不加任何锁的)

这个时候就需要WATCH命令了。
不妨换思路,

  1. 监控我们需要自增的键。
  2. 然后获取键值,并让值加1。
  3. 最后用set命令赋值。如果,在赋值的时候,发现值已经被另一个客户端修改了,这个时候如果还去set就会出现上面的情况,所以此次事务就放弃执行。

基于这个思路,我们采取先check后set的方式,执行事务,如果事务失败,再执行一次,并期望这次没有产生冲突,直到这个事务执行成功。
这种锁的形式成为乐观锁

最终借助WATCH和事务实现了自增命令如下:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

关于watch命令还有如下补充:

  • watch 可以在multi之前多次调用,并且可以一次watch多个键,只要有一个键被改变,事务就不会执行。
  • exec被调用时,所有键都会恢复 UNWATCHED 的状态,不管事务是否中止与否。
    同样,当客户端连接关闭时,所有键都会被UNWATCHED
  • 也可以使用unwatch命令(不带参数)来刷新所有监视的键。
  • 注意,很重要的一点是,键是否被监控都是相对于每个客户端而言的,也就是说它不是对于服务器的一个全局状态。以后会在Redis的实现里具体探讨。

一个例子:有序集合zset是没有弹出第一个元素的原子性命令的,我们可以使用watch和事务实现它。
使用WATCH实现ZPOP

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

如果EXEC失败(即返回Null答复),我们将重复该操作。

Redis脚本和事务

一个Redis的脚本是定义事务性的,所以一切都可以用Redis的事务做的,你也可以用一个脚本做的,平时脚本会更简单,更快速。
这种重复是由于以下事实:脚本是在Redis 2.6中引入的,而事务早已存在。但是,我们不太可能在短期内取消对事务的支持,因为在语义上似乎是适当的,即使不诉诸Redis脚本,仍然有可能避免竞争状况,尤其是因为Redis事务的实现复杂性极低。
但是,在不久的将来,我们会看到整个用户群只是在使用脚本,这并非不可能。如果发生这种情况,我们可能会弃用并最终删除事务。