Redis事务详解

前言:这一次博主决定,一步一步慢慢讲,各位骚年不要急。

一.关系型数据库中的事务

关系型数据库有一个重要的特性就是事务,而事务有四个要素:1.原子性(atomicity)2.一致性(consistency)3.隔离性(isolation)4.持久性(durability)。简称ACID。我们可以这么认为:关系型数据库中的事务就是ACID,只有当一个完整的操作同时满足原子性,一致性,隔离性和持久性时,那么就可以说这个操作是在一个事务(事务的书面化的解释是访问并可能更新数据库中各种数据项的一个程序执行单元)。现在我们已经知道了什么是关系型数据库中的事务,那么事务的这个4要素具体又代表什么意思呢?我下面给出一个简要精炼的解释

要素 定义
atomicity 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
consistency 事务必须是使数据库从一个一致性状态变到另一个一致性状态
isolation 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
durability 指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响

原子性:同时执行4个操作,一个修改a的值,一个修改b的值,一个修改c的值,一个修改d的值。修改a,c,d的值都成功了,但是修改b的值的时候出错了,那么这4个操作都不会执行,同时会被回滚回来。

一致性:a有500块钱,b有500块钱,现在a转给b200块钱需要两个动作,一个是从自己的账号上扣200,另一个是在b的账号上增两百。不能a扣了200而b没有增200或者不能a的200没扣而b的200却增了,这样会导致总额从原来的1000变为800或者1200,这就是不具备一致性。而只有当一开始一共是1000,操作完之后总和还是1000时才具有一致性。

隔离性:有一个苹果,我吃了一口放在了桌子上然后出去了,回来发现苹果被吃了两口,说明我走的这一段时间有其他人也来吃了一口。现在,我又吃了一口放在桌子上喊了一句:这个苹果已经被隔离了大家都不能动了!然后又出去了。期间我弟过来看到了这个苹果准备吃一口,但准备吃的时候发现这个苹果已经被我隔离了,只有等我解除隔离才能吃到,所以只能在哪等我回来,等我回来之后,我又啃了两口后已经吃腻了,于是喊了一句:解除隔离!于是在旁边的弟弟便可以得到这个已经被解除隔离的苹果,继续吃了起来!这个就是隔离性。(好SB的栗子)

持久性:就是当事务成功提交后将操作的数据保存到数据库中,这么理解就行了,没那么重要。

二.Redis中的事务

Redis中的事务(transaction)是一组命令的集合。包括MULTI, EXEC, DISCARD ,WATCH ,UNWATCH.当然,这些指令的作用就是实现一个类似于关系型数据库的事务。

1.MULTI和EXEC

从1.2.0版本开始,redis引入了MULTI和EXEC指令,MULTI标志着一个事务的开始,后面的命令暂时不会执行,而是会存到队列中,等到 EXEC 执行之后,队列中的命令才会依次序执行,下面给出案例:

[root@localhost redis-4.0.1]# ./src/redis-cli -p 10000 -a 123456
127.0.0.1:10000> multi
OK
127.0.0.1:10000> set liu1 1
QUEUED
127.0.0.1:10000> set liu2 2
QUEUED
127.0.0.1:10000> set liu3 3
QUEUED
127.0.0.1:10000> exec
1) OK
2) OK
3) OK
127.0.0.1:10000> get liu1
"1"
127.0.0.1:10000> get liu2
"2"
127.0.0.1:10000> get liu3
"3"

键入MULTI总是会返回OK,标示一个事务开始了,后面进来的指令并不会马上执行,而是返回”QUEUED”,这表示命令已经被服务器接受并且暂时保存起来,最后输入EXEC命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了三个OK,这里返回的结果与发送的命令是按顺序一一对应的,这说明这次事务中的命令全都执行成功了。

2.DISCARD

从2.0.0版本开始redis引入了DISCARD命令,其作用是刷新事务中先前排队的所有命令,并将连接状态恢复正常。就是清除之前存在队列中的所有指令,然后直接结束该事务。下面给出案例:

[root@localhost redis-4.0.1]# ./src/redis-cli -p 10000 -a 123456
127.0.0.1:10000> multi
OK
127.0.0.1:10000> set liu1 1
QUEUED
127.0.0.1:10000> set liu2 2
QUEUED
127.0.0.1:10000> discard
OK
127.0.0.1:10000> exec
(error) ERR EXEC without MULTI

可以看出当我们执行EXEC指令时报错了:在没有MULTI的前提下执行了EXEC命令。代表当前环境中已经没有了事务,而这正是DISCARD做到的。

3.WATCH和UNWATCH

下面让我看一下Redis事务的另一个重要成员WATCH(类似于乐观锁)

Redis官方:WATCH is used to provide a check-and-set (CAS) behavior to Redis transactions.

从2.2.0版本开始redis引入了WATCH和UNWATCH命令。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,EXEC命令执行完之后被监控的键会自动被UNWATCH)。UNWATCH的作用是取消WATCH命令对多有key的监控,所有监控锁将会被取消。
下面举出网上对乐观锁总结的比较好的解释:

乐观锁:就像他的名字,不会认为数据不会出错,他不会为数据上锁,但是为了保证数据的一致性,他会在每条记录的后面添加一个标记(类似于版本号),假设A 获取K1这条标记,得到了k1的版本号是1,并对其进行修改,这个时候B也获取了k1这个数据,当然,B获取的版本号也是1,同样也对k1进行修改,这个时候,如果B先提交了,那么k1的版本号将会改变成2,这个时候,如果A提交数据,他会发现自己的版本号与最新的版本号不一致,这个时候A的提交将不会成功,A的做法是重新获取最新的k1的数据,重复修改数据、提交数据。
悲观锁:这个模式将认定数据一定会出错,所以她的做法是将整张表锁起来,这样会有很强的一致性,但是同时会有极低的并发性(常用语数据库备份工作,类似于表锁)。

解释永远不足以说明,上详细的案例:

案例一: 在事务开始后使用WATCH

127.0.0.1:10000> multi
OK
127.0.0.1:10000> watch a
(error) ERR WATCH inside MULTI is not allowed
//(错误) 不允许在事务内部使用WATCH
127.0.0.1:10000> set a a
QUEUED
127.0.0.1:10000> exec
1) OK

从上例可以看到Redis不允许在事务内部使用WATCH命令,会报错,但是即使使用了WATCH也不会因为这个错误导致事务中止,事务照常执行。

案例二: 同一客户端下,在WATCH之后MULTI之前改变被监视的key

127.0.0.1:10000> watch a
OK
127.0.0.1:10000> set a a
OK
127.0.0.1:10000> multi
OK
127.0.0.1:10000> set a b
QUEUED
127.0.0.1:10000> exec
(nil)
// 没有命令被执行
127.0.0.1:10000> get a
"a"

可以看出在a被监视后,执行事务中set a b的命令之前a的值已经被改变(set a a),所以事务中的所有命令得不到执行(nil)。

案例三: 不同客户端下,一个客户端先监控一个键进入事务,然后另一个客户端改变这个别监控键的值
(此处用图片展示,骚年们不能复制粘贴了)执行顺序是图一,图二,图三

图一:左边的客户端先监控一个键进入事务,右边的客户端不做操作
图一:一个客户端先监控一个键进入事务,另一个客户端不做操作

图二:右边的客户端改变这个别监控键的值
图二:另一个客户端改变这个别监控键的值

图三:左边的客户端执行事务exec
图三:第一个客户端执行事务exec
由于WATCH命令的作用只是当被监控的键被修改后取消之后的事务,并不能保证其他客户端不修改监控的值,所以当EXEC命令执行失败之后需要手动重新执行整个事务

案例四: 用WATCH实现Redis的命令INCR(这里用伪代码)

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

案例五: 用WATCH实现Redis的命令ZPOP(这里用伪代码)

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

那么怎样取消对键的监控呢?

1. WATCH 对 key 的监视从调用 WATCH 开始生效,直到调用 EXEC 为止。EXEC 被调用的时候不管事务是否执行,都会取消对 key 的监视。
2.另外当客户端断开连接后也会取消监视。
3.使用无参数的 UNWATCH 可以取消对所有 key 的监视。

三.Redis事务错误时的处理

Redis官网是是这样说的:

During a transaction it is possible to encounter two kind of command errors:
1. A command may fail to be queued, so there may be an error before EXEC is called. For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, …), or there may be some critical condition like an out of memory condition (if the server is configured to have a memory limit using the maxmemory directive).
2. A command may fail after EXEC is called, for instance since we performed an operation against a key with the wrong value (like calling a list operation against a string value).

译文如下:

在事务中,可能会遇到两种类型的命令错误
1. 命令可能无法排队,因此在调用EXEC之前可能会出现错误。 例如,命令可能在语法上是错误的(错误的参数数量,错误的命令名称,…),或者可能存在一些关键条件,如内存不足条件(如果服务器配置为使用maxmemory指令具有内存限制)
2. 调用EXEC后,命令可能会失败,例如因为我们对一个具有错误值的键进行了一个操作(比如针对一个字符串调用一个列表操作)

博主对其进行一个划分:

1. 入队错误:命令不存在,命令格式不正确(例如参数错误)等。这种情况需要区分Redis的版本,Redis 2.6.5之前的版本会忽略错误的命令,执行其他正确的命令,2.6.5之后的版本会忽略这个事务中的所有命令,都不执行。博主用的Redis版本为redis-4.0.1,举例如下:

127.0.0.1:10000> multi
OK
127.0.0.1:10000> get a
QUEUED
127.0.0.1:10000> get b
QUEUED
127.0.0.1:10000> get a a
(error) ERR wrong number of arguments for 'get' command
127.0.0.1:10000> exec
(error) EXECABORT Transaction discarded because of previous errors.
//(错误) 执行中止,由于之前的错误导致事务被放弃

2. 执行错误:运行错误表示命令在执行过程中出现错误,比如用GET命令获取一个散列表类型的键值。这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令会被Redis接受并执行。如果食物里有一条命令执行错误,其他命令依旧会执行(包括出错之后的命令)。值得注意的是执行错误的逻辑是不分版本的,举例如下:

127.0.0.1:10000> multi
OK
127.0.0.1:10000> hset a a a
QUEUED
127.0.0.1:10000> get a
QUEUED
127.0.0.1:10000> exec
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) "a"
//(错误) 对持有错误类型的键的操作

注意: 当参数个数错误时是入队错误还是执行错误呢?redis官方是这么说的:A command may fail to be queued, so there may be an error before EXEC is called. For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, …),他们把错误的参数个数归类为入队错误。那么下面我们做个测试:

测试一:让命令少一个参数

127.0.0.1:10000> multi
OK
127.0.0.1:10000> set a
(error) ERR wrong number of arguments for 'set' command     
127.0.0.1:10000> exec
(error) EXECABORT Transaction discarded because of previous errors.
//(错误) 执行中止,由于之前的错误导致事务被放弃

可以看出入队时就报错了,那么这是一个入队错误。

测试二:让命令多几个参数

127.0.0.1:10000> multi
OK
127.0.0.1:10000> set a a a a a a
QUEUED
127.0.0.1:10000> exec
1) (error) ERR syntax error
//(错误) 语法错误

返回QUEUED,入队成功,执行错误。

那么,参数个数的错误究竟是属于哪一种错误呢?是官方文档错了吗?有独到见解的骚年欢迎与博主私信交流:QQ354311909

四.Redis为什么不支持回滚

Redis命令在事务中可能会失败,但是Redis将执行事务中其余的命令,而不是回滚。如果你有关系型数据库的背景你肯定会对此感到奇怪!是的,redis不支持回滚!换一句话说,redis的事务在遇到博主上述所说的执行错误时是不具有原子性的(atomicity )。那么redis为什么不支持回滚呢?

官方是这样解释的:

1. Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
2. Redis is internally simplified and faster because it does not need the ability to roll back.

译文如下:

1. 只有在使用错误的语法调用时,Redis命令才会失败(在命令队列中,问题是无法检测到的),或者是针对持有错误数据类型的键:这意味着在实际操作中,失败的命令是编程错误的结果。
2. 正是因为它不需要回滚的能力,所以Redis内部才如此简化和更快。

简单来说就是:执行时发生错误其实都是我们程序员的代码编程出现了BUG,跟他们Redis没有半毛钱关系,只要语法对了那么就不会出现异常。同时没有rollback也使得redis更加优秀。那么出现执行错误怎么办呢?捕获异常处理呗。所以,我是同意redis的这种做法的。

五.总结

我们最开始跟各位骚年们提到了ACID,那么Redis的事务具有ACID吗?

1.原子性:当出现队列错误时,2.6.5之后的版本会忽略这个事务中的所有命令,都不执行,所以此时redis是符合原子性的,但是当出现执行错误时由于redis没有rollback机制,所以不具有一致性,需要我们自己动手处理异常,保证事务的原子性。
2.一致性:Redis 通过谨慎的错误检测和简单的设计保证事务的一致性。
3.隔离性:Redis的WATCH命令保证了事务的隔离性。
4.持久性:(用列表展示)

类型 是否具有持久性
无持久化机制 事务不具有持久性,服务器停机重启后数据丢失,故而不具有持久性
RDB 机制 在特定条件下才会保存数据集快照,不能保证数据在第一时间被保存在硬盘中,故而不具有持久性
AOF机制appendfsync = always 时 程序总会在执行命令之后调用同步函数,将命令数据存在硬盘中,这种情况下的事务具有持久性
AOF 机制appendfsync = everysec 程序会每秒同步一次数据到硬盘。因为停机可能就发生在命令执行完毕但是尚未同步的那一秒钟内,这样会造成事务数据丢失,故而不具有持久性
AOF 机制appendfsync = no 由操作系统决定何时将数据同步到硬盘。因为事务数据可能在等待同步的过程中丢失,这样会造成事务数据丢失,故而不具有持久性

总结得出:Redis的事务原生具有一致性,需要我们通过程序保证原子性,通过WATCH指令保证隔离性,通过设置持久话机制为AOF机制appendfsync = always 保证持久性。所以,Redis最终可以做到ACID!

话外音: Redis官方表示事务迟早会被Redis脚本替代,让我们拭目以待

Redis scripting and transactions
A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.
This duplication is due to the fact that scripting was introduced in Redis 2.6 while transactions already existed long before. However we are unlikely to remove the support for transactions in the short time because it seems semantically opportune that even without resorting to Redis scripting it is still possible to avoid race conditions, especially since the implementation complexity of Redis transactions is minimal.
However it is not impossible that in a non immediate future we’ll see that the whole user base is just using scripts. If this happens we may deprecate and finally remove transactions.
译文:
Redis脚本和事务
Redis脚本通过定义进行事务处理,所以您可以使用Redis事务处理所有事情,您还可以使用脚本,通常脚本会更简单,更快。这种重复是由于在Redis 2.6中引入了脚本,而事务早已存在。 然而,我们不太可能在短时间内消除对事务的支持,因为它似乎在语义上是适时的,即使不使用Redis脚本,仍然有可能避免竞争条件,特别是因为Redis事务的实现复杂度很小。然而,在不久的将来,我们将看到整个用户群只是使用脚本,这并不是不可能的。 如果发生这种情况,我们可能会弃用并最终删除事务。

刚刚我们提到了Redis的持久化机制,那么下一章博主就和大家说一说Redis的持久化机制!博主将不定时频繁更新博客,欢迎互相交流!

上一篇:RedisTemplate访问Redis数据结构(五)——ZSet
下一篇:敬请期待,博主不定时更新

猜你喜欢

转载自blog.csdn.net/weixin_37490221/article/details/78197613