文章目录
- 事务
- 性质
- 用法
- 事务中的错误
- 命令排队入队错误
- 命令执行错误
- 为什么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不支持回滚
大致有三点原因:
- 程序员自身犯的错误是无法避免的。
- 不支持回滚可以使得Redis事务的实现更加高效。
- 发生错误一般发生在开发过程中,不太可能进入到生产环境中。
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。
- 最后用
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事务的实现复杂性极低。
但是,在不久的将来,我们会看到整个用户群只是在使用脚本,这并非不可能。如果发生这种情况,我们可能会弃用并最终删除事务。