Redis事务操作(六)

1. Redis事务定义

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

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

2. Multi、Exec、discard

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

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

Redis事务分2个阶段:

  • 组队阶段:只是将所有的命令加入队列

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

2.1 常用命令

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

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

    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    127.0.0.1:6379> multi    #标记事务开始
    OK
    127.0.0.1:6379(TX)> incr userId # 多条命令按顺序入队,返回值为QUEUED,表示这个命令加入队列了,还没有被执行。
    QUEUED
    127.0.0.1:6379(TX)> incr userId
    QUEUED
    127.0.0.1:6379(TX)> incr userId
    QUEUED
    127.0.0.1:6379(TX)> ping
    QUEUED
    127.0.0.1:6379(TX)> exec #执行
    1) (integer) 1
    2) (integer) 2
    3) (integer) 3
    4) PONG #如果ping通了,返回pong
  2. exec:执行所有事务块内的命令

    执行事务块内的所有命令

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

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

    示例1:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    127.0.0.1:6379> watch lock lock_times     #watch监听多个key 
    OK
    127.0.0.1:6379> multi #开启事务
    OK
    127.0.0.1:6379(TX)> set lock "hangz" #设置值
    QUEUED
    127.0.0.1:6379(TX)> incr lock_times
    QUEUED
    127.0.0.1:6379(TX)> exec #执行
    1) OK
    2) (integer) 1
    示例2:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> watch k1 k2
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> set k1 v1 #另外一个客户端执行了set k1 aaa,修改了k1的值
    QUEUED
    127.0.0.1:6379(TX)> incr k2
    QUEUED
    127.0.0.1:6379(TX)> exec # 因为k1被修改,事务执行失败
    (nil)
  3. discard:取消事务

    取消事务,放弃执行事务块内的所有命令。 总是返回 OK

    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> ping
    QUEUED
    127.0.0.1:6379(TX)> set name xiaoyuge
    QUEUED
    127.0.0.1:6379(TX)> discard
    OK

3. 事务的错误处理

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

    示例代码:事务中执行了3个set命令,而第3个set命令有问题,加入队列失败,最后在执行exec时,所有命令被取消执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> multi                     #开启一个事务块
    OK
    127.0.0.1:6379(TX)> set name xiaoyuge
    QUEUED
    127.0.0.1:6379(TX)> set age 18
    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.
  2. 情况二:组队没有问题,执行中部分成功部分失败

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> set 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
    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"

4. 事务冲突的问题

例子:

1
2
3
4
你的账户中只有10000,有多个人使用你的账户,同时去参加双十一抢购
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000

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

4.1 悲观锁

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

4.2 乐观锁

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

4.3 watch监视

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set balance 1000
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 xiaoyuge #设置name的值为xiaoyuge
QUEUED
127.0.0.1:6379(TX)> incrby balance 10 #将balance的值+10, 然后新开一个窗口执行窗口2的命令
QUEUED
127.0.0.1:6379(TX)> exec #当窗口2的命令执行完了之后,在执行窗口1的事务
(nil)
127.0.0.1:6379> get balance #窗口1的事务执行失败,因为窗口2修改了值,结果为1050
"1050"
127.0.0.1:6379> get name #事务执行失败,name为空
(nil)

窗口2示例:

1
2
3
4
127.0.0.1:6379> incrby balance 50       #balance原子+50
(integer) 1050
127.0.0.1:6379> get balance
"1050"

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

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

4.4 unwatch:取消监视

取消watch命令对所有的key的监视,如果在执行watch命令之后,exec命令或discard命令先被执行的话,那么就不需要再执行unwatch了。
因为EXEC命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

1
2
3
4
127.0.0.1:6379> watch k1 k2
OK
127.0.0.1:6379> unwatch
OK

5. Redis事务的3大特性

  • 单独的隔离操作

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

  • 没有隔离级别的概念

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

  • 不能保证原子性

    事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。 如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败就这条失败,其他的命令则正常执行,不保证都成功或都失败