Redis原理 — 事务

Redis原理 — 事务

Redis 是支持事务的,与之相关的指令有 multi、exec、discard,分别代表事务的开始、提交、丢弃。

以下案例中,使用 multi 开启一个事务,使用 exec 结束事务。在 exec 执行之前,所有指令都不会被执行,而是缓存到了服务端的事务队列里,服务器一旦收到 exec 指令,才执行整个事务队列,一次性返回所有响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 演示:开启事务-提交事务 */
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set number 1
QUEUED # QUEUED 代表指令已经被缓存到了事务队列中
127.0.0.1:6379> incr number
QUEUED
127.0.0.1:6379> get number
QUEUED
127.0.0.1:6379> set string a
QUEUED
127.0.0.1:6379> get string
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 2
3) "2"
4) OK
5) "a"

/* 演示:开启事务-丢弃事务 */
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key value
QUEUED
127.0.0.1:6379> get key
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get key
(nil)

由于 Redis 单线程的特性,保证了事务中的指令执行过程中不会执行其他指令。但是注意:Redis 只能保证执行过程的「原子性」,而不能保证执行结果的「原子性」

执行过程的「原子性」:事务中的指令执行时不会被其他指令插队
执行结果的「原子性」:事务中的某个指令执行失败,不会导致整个事务回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set str1 HelloWorld
QUEUED
127.0.0.1:6379> incrby str1 100 # 对一个 string 类型执行自增操作会报错,但是没有回滚!
QUEUED
127.0.0.1:6379> get str1
QUEUED
127.0.0.1:6379> set str2 YoYoYo
QUEUED
127.0.0.1:6379> get str2
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) "HelloWorld"
4) OK
5) "YoYoYo"
127.0.0.1:6379> get str1 # 事务中某条执行的失败不会导致事务中其他指令自动 discard
"HelloWorld"
127.0.0.1:6379> get str2 # 事务中某条执行的失败不会导致事务中其他指令自动 discard
"YoYoYo"

使用管道提高事务执行效率

事务发送每条执行都要经历一次网络读写,当事务中的指令过多时会导致网络IO操作时间变长,所以一般会结合管道和事务一起使用,将事务中的多次网络IO操作压缩成一次。

使用 watch 实现乐观锁

在关系型数据库中,保证并发修改数据时只有一个线程能操作成功(操作原子性),可以使用 where version 实现乐观锁或者使用分布式锁。

在 Redis 中,如何保证并发修改数据的操作原子性呢?可以使用分布式锁,但是它是悲观锁,那么有没有类似于关系型数据库的乐观锁实现呢?

Watch 就是这样的一种乐观锁机制,在事务提交之前,使用 watch 指令监视一个或多个变量,服务器收到 exec 指令要执行事务队列中缓存的所有指令时,会去检查变量自 watch 之后是否被修改,如果被修改了,服务端会返回一个 NULL 告知客户端事务执行失败,客户端收到之后决定是放弃操作还是重试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 事务执行失败 */
127.0.0.1:6379> watch amount # 监视一个变量
OK
127.0.0.1:6379> set amount 0 # 模拟事务操作时其他事务改变了变量的值
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> get amount
QUEUED
127.0.0.1:6379> incrby amount 100 # 事务操作
QUEUED
127.0.0.1:6379> get amount
QUEUED
127.0.0.1:6379> exec # 提交事务
(nil) # 返回 null 告知客户端执行失败
127.0.0.1:6379> get amount # 事务的操作没有生效
"0"

/* 事务执行成功 */
# -------------------------------------------------------------
127.0.0.1:6379> set amount 0
OK
127.0.0.1:6379> watch amount
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get amount
QUEUED
127.0.0.1:6379> incrby amount 100
QUEUED
127.0.0.1:6379> get amount
QUEUED
127.0.0.1:6379> exec
1) "0"
2) (integer) 100
3) "100"

注意:Redis 禁止 watch 指令在 multi 和 exec 中间执行,必须在 multi 之前监视关键变量。

为什么 Redis 事务不支持回滚?

在关系型数据库中,出现异常时事务回滚,这种异常可以是代码抛出来的业务异常,也可以是 SQL 执行异常。

在 Redis 中,怎么鉴别一个错误会导致需要 “回滚” 呢,只有两种情况:1.指令写错了,2.指令用在了错误的数据类型上面。这些情况是开发人员的语法错误,不应该出现在生产环境中,没有事务的必要。所以事务执行过程中会忽略错误的指令语法执行后续的指令。

不需要对事务回滚进行支持,Redis 内部可以保持简单和快速。

Comments