【Redis】事务

599 阅读8分钟

Redis简单事务

对于熟练使用关系型数据库各位来说,"事务" 这个名词大家已经不再陌生了 虽说不再陌生

但按照惯例还是会简单进行说明, 所谓事务简单理解就是 "将一组业务当作一个业务来处理"

这样做能够保证数据库中数据的 一致性正确性,事务一般都遵守 ACID原则;即 原子性、一致性、隔离性、持久性


而 Redis中的事务是不包括 原子性的,Redis中的事务更像是一组命令的集合;

开启事务后我们可以同时执行一组命令,这些命令在事务执行前会进行一个入队的操作(书写的命令不会被立即执行

这些入队的命令会根据顺序在事务执行后,一个一个执行 但有几点需要注意

  • 如果事务执行时 命令集合中存在命令的书写错误,那么整个集合的命令都不会被执行

    • 如参数数量错误、参数名错误等等,或者其他更严重的错误,比如内存不足
    • 对于这种情况,Redis早些时候会在事务执行前检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败
    • 如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务
      • 不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录
      • 并在客户端调用 EXEC 命令时(事务执行命令),拒绝执行并自动放弃这个事务
  • 如果命令编译通过,异常为运行时异常 那么其他的命令仍会照常执行,存在运行时异常的命令会执行失败

    • 比如使用 incy命令对一个非整形数据进行原子 +1操作
  • 事务是可以取消的,如果事务取消 那么这些入队的命令也不会被执行

    • 可以手动使用命令取消,也可以直接 ctrl + F4强制取消(在事务未被执行前打断施法即可

这几种情况在下面都会进行演示,而从第二种情况就能明白 Redis的事务是不存在原子性的

事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

除了不存在原子性之外,Redis中的事务也没有隔离级别的概念

既然如此,就不可能会存在 脏读、幻读、不可重复读等一系列问题了


演示

Redis事务的执行有三个过程,即 开启事务、命令入队、执行事务,如果上面这么多字都有认真看的话应该很容易理解

开启事务使用 multi命令,该命令执行后会返回提示 "OK"

127.0.0.1:6379> multi
OK

之后就是命令入队的操作了,我们输入的一系列命令都会被序列化

如果命令的书写没有问题会返回提示 "QUEUED",而有问题则会返回提示 "对应的 error"

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
QUEUED
127.0.0.1:6379> set ktest test
QUEUED
127.0.0.1:6379> get k3
QUEUED

确认入队的命令没有遗漏就可以使用 EXEC命令来执行事务,执行事务后 入队的命令会按照顺序一个一个的执行

注意不要手快按错了,如果按错了 Redis可不会给第二次机会让你执行;该事务会被直接丢弃

127.0.0.1:6379> exce # 手快按错后
(error) ERR unknown command `exce`, with args beginning with: 

127.0.0.1:6379> exec # 按错后因为存在命令的书写错误,提示该事务已经被丢弃了.....
(error) EXECABORT Transaction discarded because of previous errors.

以下为正常事务执行后的返回提示

127.0.0.1:6379> exec
1) OK
2) OK
3) "v3"

可以看到是根据顺序来的,命令没有被打乱 以上就是一次完整的事务执行

这次事务中,我们对第一种异常情况已经有所了解了(如果存在命令的书写错误,那么事务会被丢弃无法执行)

碰到这种情况只能再一次开启事务,避免命令的书写错误

而第二种我们则需要单独的进行测试


命令运行时异常

在如下演示中,我们将 k1的 value设置为 字符串 "v1",也就是非整形数据

在执行 incr命令时,如果进行原子 +1的数据是 非整形数据那么应该会抛出 error,但命令书写是没有问题的 在编译时能够通过

也就是该命令在入队时会返回提示 "QUEUED"

而事务执行后能够发现第三条命令也就是 incr命令抛出了 error,但事务是执行成功的 往下的 get命令没有执行失败

127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) "v1"
3) (error) ERR value is not an integer or out of range
4) "v1"

这也就是前面说到的,如果是运行时异常 事务会忽略该命令,继续向下执行其他命令


事务的取消

偶尔我们会碰到在命令入队时想取消事务的情况,而取消事务的主动做法有三种

  • 第一种为最规范的做法,即使用discard命令
  • 第二种就是随便书写一条不会通过编译的命令(写个 a然后回车)再使用 exec命令执行事务
  • 最后一种就是物理取消了,关机或者关闭客户端都可以实现

我们仅演示第一种,其他的可以自行测试

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> discard # 手动取消事务,返回 "OK"字样表示取消成功
OK # 如果报 error表示你手快了,执行 EXEC即可取消事务(该命令一般也不会有返回 "QUEUED"的情况吧

为什么 Redis不支持回滚

如果你有使用关系式数据库的经验, 那么 "Redis 在事务失败时不进行回滚,而是继续执行余下的命令" 这种做法可能会让你觉得有点奇怪。

以下是这种做法的优点:

  • Redis命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis的内部可以保持简单且快速。

有种观点认为 Redis处理事务的做法会产生 bug, 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。

举个例子, 如果你本来想通过 INCR命令将键的值加上 1, 却不小心加上了 2, 又或者对错误类型的键执行了INCR, 回滚是没有办法处理这些情况的。

Redis实现乐观锁

虽说 Redis不支持直接回滚,但我们可以通过 Redis提供的一个命令来实现回滚

这个命令就是 watch,该命令可以为 Redis事务提供 check-and-set (CAS)行为。

我们可以使用 watch命令来监视一个 或多个key,如果被监视的 key在事务执行前被修改过那么本次事务将会被取消,也就是所谓的回滚

只有确保被监视的 key,在事务开始前到执行 这段时间内未被修改过 事务才会执行成功(类似乐观锁)


如果一次事务中存在被监视的 key,无论此次事务执行成功与否,该 key的监视都将会在执行后失效 也就是说监视是一次性的。

另外, 当客户端断开连接时, 该客户端对键的监视也会被取消

以下为示例:

# 在客户端 1中执行如下操作
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
-------------------------------------------------
# 在开启事务后打开客户端 2
# 在客户端 2中对数据 "money"进行修改
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> get newName
"1000"
--------------------------------------------------

# 修改完毕后切换至客户端 1
# 此时我们进行命令的入队
127.0.0.1:6379> set money 20
QUEUED
127.0.0.1:6379> exec # 执行事务
(nil) # 很明显事务执行失败了,失败的原因是 该 key在开启事务后被 客户端 2进行了修改
# 前面也说到被监视的 key如果在事务执行前被修改,那么本次事务就会被取消

为防止各位对上述 代码块有所疑惑,我以 gif的形式复现一次该操作

在上述操作中我是对数据的 value进行修改,但监视并不只是监视数据的 value;对 key进行修改本次事务一样不会执行成功

Redis还提供了一个 unwatch命令,该命令可以让我们取消对所有数据的监控


放松一下眼睛

原图P站地址

画师主页