Redis专题之7、第7篇:Redis事务操作

7.1、redis事务定义

redis事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

redis事务的主要作用就是串联多个命令防止 别的命令插队。

7.2、Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令依次执行。

组队的过程中可以通过discard来放弃组队。

redis事务分2个阶段:组队阶段、执行阶段

  • 组队阶段:只是将所有命令加入命令队列
  • 执行阶段:依次执行队列中的命令,在执行这些命令的过程中,不会被其他客户端发送的请求命令插队或者打断。

7.2.1、相关的几个命令

multi:标记一个事务块的开始

标记一个事务块的开始。

事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 exec 命令原子性(atomic)地执行。

示例

redis> MULTI            # 标记事务开始
OK
redis> INCR user_id     # 多条命令按顺序入队,返回值为QUEUED,表示这个命令加入队列了,还没有被执行。
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC             # 执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG

exec:执行所有事务块内的命令

执行所有事务块内的命令。

假如某个(或某些) key 正处于watch命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么exec命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。

返回值:

事务块内所有命令的返回值,按命令执行的先后顺序排列。

当操作被打断时,返回空值 nil

3个示例

redis> MULTI
OK
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG

redis> WATCH lock lock_times
OK
redis> MULTI
OK
redis> SET lock "huangz"
QUEUED
redis> INCR lock_times
QUEUED
redis> EXEC
1) OK
2) (integer) 1

redis> WATCH lock lock_times
OK
redis> MULTI
OK
redis> SET lock "joe"        # 就在这时,另一个客户端修改了 lock_times 的值
QUEUED
redis> INCR lock_times
QUEUED
redis> EXEC                  # 因为 lock_times 被修改, joe 的事务执行失败
(nil)

discard:取消事务

取消事务,放弃执行事务块内的所有命令。

返回值:

总是返回 OK

示例

redis> MULTI
OK
redis> PING
QUEUED
redis> SET greeting "hello"
QUEUED
redis> DISCARD
OK

7.3、事务的错误处理

7.3.1、情况1:组队中命令有误,导致所有命令取消执行

组队中某个命令出现了错误报告,执行时整个队列中所有的命令都会被取消。

示例代码如下,事务中执行了3个set命令,而第3个命令set address命令本身有问题,加入队列失败,最后执行exec的时候,所有的命令都被取消执行。

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> multi #开启一个事务块
OK
127.0.0.1:6379(TX)> set name ready
QUEUED
127.0.0.1:6379(TX)> set age 30
QUEUED
127.0.0.1:6379(TX)> set address #命令有问题,导致加入队列失败
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec #执行exec的时候,事务中所有命令都被取消
(error) EXECABORT Transaction discarded because of previous errors.

7.3.2、情况2:组队中没有问题,执行中部分成功部分失败

命令组队的过程中没有问题,执行中出现了错误会导致部分成功部分失败。

示例代码如下,事务中有3个命令,3个命令都入队列成功了,执行exec命令的时候,1和3命令成功了,第2个失败了

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1 #命令1:设置k1的值为v1
QUEUED
127.0.0.1:6379(TX)> incr k1 #命令2:k1的值递增1,由于k1的值不是数字,执行的时候会失败的
QUEUED
127.0.0.1:6379(TX)> set k2 v2 #命令3:设置k2的值为v2
QUEUED
127.0.0.1:6379(TX)> exec #执行命令,1和3命令成功,第2个失败了
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> mget k1 k2 #查看k1和k2的值
1) "v1"
2) "v2"

7.4、事务冲突的问题

7.4.1、例子

想象一个场景:

你的账户中只有10000,有多个人使用你的账户,同时去参加双十一抢购

一个请求想给金额减8000

一个请求想给金额减5000

一个请求想给金额减1000

3个请求同时来带①,看到的余额都是10000,大于操作金额,都去执行修改余额的操作,最后导致金额变成了-4000,这显然是有问题的。

7.4.2、悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人拿到这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。

7.4.3、乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去那数据的时候都认为别人不会修改,所以不会上锁,但是在修改的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是使用这种check-and-set机制实现事务的。

7.4.4、watch key [key …]

在执行multi之前,先执行watch key1 [key2 …],可以监视一个或者多个key,若在事务的exec命令之前这些key对应的值被其他命令所改动了,那么事务中所有命令都将被打断,即事务所有操作将被取消执行。

示例

开启2个窗口,按照下表的时间点在不同的窗口执行对应的命令,注意看结果。

时刻窗口1窗口2
T1 flushdb  
T2 set balance 100  
T3 watch balance  
T4 multi  
T5 set name ready incrby balance 50
T6 incrby balance 10 get balance
T7 exec  
T8 get balance  
T9 get name

窗口1中,对balance进行了监视,也就是说在执行watch balance命令之后,在exec命令之前,如果有其他请求对balance进行了修改,那么窗口1事务中所有的命令都会将会被取消执行。

窗口1watch balance后,由于T5时刻窗口2对balance进行了修改,导致窗口1中事务所有命令被取消执行。

窗口1执行结果如下

127.0.0.1:6379> flushdb #清空db,方便测试
OK
127.0.0.1:6379> set balance 100 #设置balance的值为100
OK
127.0.0.1:6379> watch balance #监视balance,若balance在事务阶段被其他命令修改,事务执行将被取消
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set name ready #设置name的值为ready
QUEUED
127.0.0.1:6379(TX)> incrby balance 10 #将balance的值+10
QUEUED
127.0.0.1:6379(TX)> exec #执行事务,由于balance被窗口2修改了,所以本事务执行失败,返回nil
(nil)
127.0.0.1:6379> get balance #获取balance,原始值为100,被窗口2加了50,结果为150
"150"
127.0.0.1:6379> get name #获取name的值,事务中set name 未成功,所以name没有
(nil)

窗口2执行结果

127.0.0.1:6379> incrby balance 50 #balance原子+50
(integer) 150
127.0.0.1:6379> get balance #获取balance的值,为150
"150"

7.4.5、unwatch:取消监视

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

redis> WATCH lock lock_times
OK

redis> UNWATCH
OK

7.5、redis事务三特性

(1)单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请求所打断。

(2)没有隔离级别的概念

队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

(3)不能保证原子性

事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。

如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败就这条失败,其他的命令则正常执行,不保证都成功或都失败。

来源:http://www.itsoku.com/course/15/254
posted @ 2022-04-27 13:24  程序员小明1024  阅读(67)  评论(0编辑  收藏  举报