【Redis】事务和锁

纸上得来终觉浅,绝知此事要躬行。

什么是事务

事务可以一次执行多个命令,本质是一组命令的集合, 并且带有以下两个重要的保证:

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

为什么要事务

首先我们先来看一个案例:

session-1session-2
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> set money 1
OK
127.0.0.1:6379> get money
"1"
127.0.0.1:6379> get money
"1"

上面相当于两个客户端同时进行,session-1为客户端1,session-2为客户端2。客户端1先set money 1000,紧接着客户端2执行set money 1,当客户端1通过get money查看的时候最终的值变成了1,也就是说客户端1在执行的过程中被客户端2插队了,有没有办法保证一个客户端在执行过程中整个操作是一个整体,有,就是下面需要了解的事务以及事务相关的操作命令。

事务的操作

MULTI

  • 用于开启一个事务,它总是返回 OK 。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中。 所有传入的命令都会返回一个内容为 QUEUED 的状态回复(status reply), 这些被入队的命令将在 EXEC 命令被调用时执行。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> INCR age
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379>

EXEC

  • 命令被调用时, 所有队列中的命令才会被执行。回复是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。 其中, 回复元素的先后顺序和命令发送的先后顺序一致。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> INCR age
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 23
3) "23"
127.0.0.1:6379>

DISCARD

  • 命令被调用时,客户端可以清空事务队列, 并放弃执行事务。之后不能在提交。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> INCR age
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI
127.0.0.1:6379>

事务工作流程

【Redis】事务和锁

事务中的错误

使用事务时可能会遇上以下两种错误:

  • 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误。
127.0.0.1:6379> set name ydongy
QUEUED
127.0.0.1:6379> aaa name ydongy2
(error) ERR unknown command `aaa`, with args beginning with: `name`, `ydongy2`, 
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>

我们发现,当事务中出现语法错误,最后exec提示事务不存在,也就是说在事务中执行语法错误,整体事务中所有命令均不会执行。包括那些语法正确的命令

  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set name ydongy
QUEUED
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> lpush name 1 2 3
QUEUED
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "ydongy"
3) OK
4) "22"
5) (error) WRONGTYPE Operation against a key holding the wrong kind of value
6) "ydongy"
127.0.0.1:6379>

上面的案例有一步错误操作,就是把name当成列表进行追加数据,语法本身没有错,但是在最后执行的时候那一条指令并没有执行,但是整个事务中正确的指令都执行了。需要注意已经执行完毕的命令对应的数据不会自动回滚,即 Redis 不支持回滚,需要程序员自己在代码中实现回滚。

业务场景:

【Redis】事务和锁

为了解决这种并发操作带来的问题,Redis 的 WATCH 命令可以为事务提供 check-and-set (CAS)行为,即:乐观锁。被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消。

session-1session-2
127.0.0.1:6379> set age 22
OK
127.0.0.1:6379> get age
"22"
127.0.0.1:6379> WATCH age
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR age
QUEUED
127.0.0.1:6379> DECR age
(integer) 21
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get age
"21"
127.0.0.1:6379> get age
"21"

通过EXEC 返回nil来表示事务已经失败。当 EXEC 被调用时,不管事务是否成功执行,对所有键的监视都会被取消。另外,当客户端断开连接时,该客户端对键的监视也会被取消。如果需要WATCH 对多个键进行监视,可以使用无参数的 UNWATCH 命令手动取消对所有键的监视。