点击蓝色“架构文摘”关注我哟
加个“星标”,每天上午 09:25,干货推送!
来源:https://juejin.cn/post/6932447503816245262
对于熟练使用关系型数据库各位来说,"事务" 这个名词大家已经不再陌生了 虽说不再陌生
但按照惯例还是会简单进行说明, 所谓事务简单理解就是 "将一组业务当作一个业务来处理"
这样做能够保证数据库中数据的 一致性正确性,事务一般都遵守 ACID原则;即 原子性、一致性、隔离性、持久性
而 Redis中的事务是不包括 原子性的,Redis中的事务更像是一组命令的集合;
开启事务后我们可以同时执行一组命令,这些命令在事务执行前会进行一个入队的操作(书写的命令不会被立即执行
这些入队的命令会根据顺序在事务执行后,一个一个执行 但有几点需要注意
如果事务执行时 命令集合中存在命令的书写错误,那么整个集合的命令都不会被执行
不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录
并在客户端调用 EXEC 命令时(事务执行命令),拒绝执行并自动放弃这个事务
如参数数量错误、参数名错误等等,或者其他更严重的错误,比如内存不足
对于这种情况,Redis早些时候会在事务执行前检查命令入队所得的返回值:如果命令入队时返回 `QUEUED` ,那么入队成功;否则,就是入队失败
如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务
如果命令编译通过,异常为运行时异常 那么其他的命令仍会照常执行,存在运行时异常的命令会执行失败
比如使用`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
命令,该命令可以让我们取消对所有数据的监控
end
关注公众号《Java派》
学Java不迷路
往期推荐
如有收获,点个在看,诚挚感谢