Redis教程 —— 事务详解

这一篇文章,没有写java代码示例,直接在redis客户端进行测试了。如果需要,可以自行测试。

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

关系型数据库有一个重要的特性就是事务,而事务有四个要素(简称 ACID):

  1. 原子性(atomicity)

  2. 一致性(consistency)

  3. 隔离性(isolation)

  4. 持久性(durability)。

我们可以这么认为:关系型数据库中的事务就是 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 执行之后,队列中的命令才会依次序执行,下面给出案例:

➜  ~ redis-cli --raw
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 程序喵
QUEUED
127.0.0.1:6379> set website http://www.ibloger.net
QUEUED
127.0.0.1:6379> set author 刘仁奎
QUEUED
127.0.0.1:6379> exec
OK
OK
OK
127.0.0.1:6379> get name
程序喵
127.0.0.1:6379> get website
http://www.ibloger.net
127.0.0.1:6379> get author
刘仁奎
127.0.0.1:6379>

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

2、DISCARD(丢弃)

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

➜  ~ redis-cli --raw
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set liu1 1
QUEUED
127.0.0.1:6379> set liu2 2
QUEUED
127.0.0.1:6379> set liu3 3
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
ERR EXEC without MULTI

127.0.0.1:6379>

3、WATCH 和 UNWATCH

Redis 事务的另一个重要成员 WATCH(类似于乐观锁),从 2.2.0 版本开始 redis 引入了 WATCH 和 UNWATCH 命令。

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

  • 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:6379> multi
OK
127.0.0.1:6379> watch name
ERR WATCH inside MULTI is not allowed

127.0.0.1:6379> set name 程序喵
QUEUED
127.0.0.1:6379> exec
OK
127.0.0.1:6379> get name
程序喵
127.0.0.1:6379>

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

127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name 程序喵
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 听风
QUEUED
127.0.0.1:6379> exec
// 没有命令被执行
127.0.0.1:6379> get name
程序喵
127.0.0.1:6379>

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

由于 WATCH 命令的作用只是当被监控的键被修改后取消之后的事务,并不能保证其他客户端不修改监控的值,所以当 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:

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).

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 之前的版本会忽略错误的命令,执行其他正确的命令

  • Redis 2.6.5 之后的版本会忽略这个事务中的所有命令,都不执行。

我用的 Redis 版本为 redis-4.0.8,举例如下:

➜  ~ redis-cli --raw
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 程序喵
QUEUED
127.0.0.1:6379> set website http://www.ibloger.net
QUEUED
127.0.0.1:6379> set aaa
ERR wrong number of arguments for 'set' command

127.0.0.1:6379> exec
EXECABORT Transaction discarded because of previous errors.

127.0.0.1:6379> get name

127.0.0.1:6379>

2、执行错误

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

举例如下:

➜  ~ redis-cli --raw
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> hset myhash name "tingfeng"
QUEUED
127.0.0.1:6379> hset myhash author "liurenkui"
QUEUED
127.0.0.1:6379> get myhash
QUEUED
127.0.0.1:6379> exec
1
1
WRONGTYPE Operation against a key holding the wrong kind of value

127.0.0.1:6379> hkyes myhash
ERR unknown command 'hkyes'

127.0.0.1:6379> hkeys myhash
name
author
127.0.0.1:6379> hget myhash name
tingfeng
127.0.0.1:6379>

3、参数错误

当参数个数错误时是入队错误还是执行错误呢?

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:6379> multi
OK
127.0.0.1:6379> set name tingfeng
QUEUED
127.0.0.1:6379> set website
ERR wrong number of arguments for 'set' command

127.0.0.1:6379> exec
EXECABORT Transaction discarded because of previous errors.

127.0.0.1:6379> get name

127.0.0.1:6379>

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

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

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name tingfeng
QUEUED
127.0.0.1:6379> set website a b c d e f g
QUEUED
127.0.0.1:6379> set age 25
QUEUED
127.0.0.1:6379> exec
OK
ERR syntax error

OK
127.0.0.1:6379> get age
25

不影响其他命令

四、Redis 为什么不支持回滚

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

官方是这样解释的:

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.

Redis is internally simplified and faster because it does not need the ability to roll back.

译文如下:

只有在使用错误的语法调用时,Redis 命令才会失败(在命令队列中,问题是无法检测到的),或者是针对持有错误数据类型的键:这意味着在实际操作中,失败的命令是编程错误的结果。

正是因为它不需要回滚的能力,所以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教程 —— 事务详解

点  赞 (1) 打  赏
分享到: