1. 事务概念
Redis
中的事务 (transaction
)是一组命令的集合。事务同命令一样是 Redis
的最小执行单位,一个事务中的命令要么都执行,要么都不执行。事务的原理是先将属于一个事务的命令发送给 Redis
,然后再让 Redis
依次执行这些命令。
2. 事务命令
命令 | 说明 |
---|---|
watch key[key…] | 锁定key,直到执行了multi/exec命令 |
multi | 标记一个事务块开始 |
exec | 执行所有multi之后发的命令 |
discard | 丢弃所有multi之后发的命令 |
3. 事务使用
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 2
QUEUED
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
127.0.0.1:6379>
multi
标记一个事务的开始,表示之后发送的命令都属于同一个事务,而 exec
命令则告诉 Redis
将等待执行的事务队列中的所有命令(即刚才返回 QUEUED
的命令)按照发送顺序依次执行。exec
命令的返回值是事务命令的返回值组成的列表,返回值顺序和命令的顺序相同。
Redis
会保证一个事务中的命令要么都执行,要么都不执行。 如果在执行 exec
命令之前客户端断线了那么 Redis
会自动清空事务队列,事务中的所有命令都不会执行;而如果客户端执行了 exec
命令后断线也没有关系,Redis
已经记录了所有要执行的命令。
所有的指令在 exec
之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec
指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis
的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。
Redis
为事务提供了一个 discard
指令,用于丢弃事务缓存队列中的所有指令,在 exec
执行之前。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set x 1
QUEUED
127.0.0.1:6379(TX)> set y 2
QUEUED
127.0.0.1:6379(TX)> set z 3
QUEUED
127.0.0.1:6379(TX)> discard # 丢弃 multi 之后发送的命令
OK
127.0.0.1:6379> get x
(nil)
127.0.0.1:6379> mget x y z
1) (nil)
2) (nil)
3) (nil)
127.0.0.1:6379>
4. 错误处理
如果 Redis
事务中一个命令发生错误,那么其它的命令还会执行吗?我们主要看两种错误:
-
语法错误
只要事务中有一个命令的语法发生错误,那么整个事务中的命令都不会执行。
-
运行错误
运行错误是指在执行命令时出现的错误,这种错误在实际执行之前是无法发现的,当事务中出现这种运行错误时,其它命令仍然会继续执行的。
> multi
OK
> set books python
QUEUED
> incr books
QUEUED
> set phone huawei
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
> get books
"python"
> get phone
"huawei"
上面的例子是事务执行到中间遇到失败了,因为我们不能对一个字符串进行数学运算,事务在遇到指令执行失败后,后面的指令还继续执行,所以 phone 的值能继续得到设置。
到这里,你应该明白 Redis
的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。
Redis
事务不支持关系型数据库事务提供的回滚功能。
5. watch 命令
watch
命令可以监控一个或者多个键,一旦其中有一个键被修改或删除,之后的事务就不会执行。监控一直持续到 exec
命令 (事务中的命令是在 exec
命令之后执行的,所以在 multi
命令之后可以修改 watch
监控的键值)。
watch
会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到了 exec
指令要顺序执行缓存的事务队列时,Redis
会检查关键变量自 watch
之后,是否被修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec
指令就会返回 null
回复告知客户端事务执行失败,这个时候客户端一般会选择重试。
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> watch a # 监控 a 是否变化
OK
127.0.0.1:6379> set a 2 # 键被修改,则后面的 multi 不会被执行
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 3
QUEUED
127.0.0.1:6379> exec # 变量被修改,服务器回复 nil
(nil)
127.0.0.1:6379> get a # 被修改之后的值,multi 命令之后的值没有被执行
"2"
127.0.0.1:6379>
也可以用 unwatch
命令来取消监控。
Redis
事务在执行时是单线程运行的。但是在执行前有可能别的客户端已经修改了事务里执行的 key
。所以在 multi
事务开始之前用 watch
检测这个 key
避免被其他客户端改变的。如果这个 key
被改变 了 exec
的时候就会报错不执行这个事务。
Redis
禁止在 multi
和 exec
之间执行 watch
指令,而必须在 multi
之前做好盯住关键变量,否则会出错。
6. 事务结合管道使用
Redis
事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO
时间也会线性增长。所以通常 Redis
的客户端在执行事务时都会结合 pipeline
一起使用,这样可以将多次 IO
操作压缩为单次 IO
操作。
比如我们在使用 Python
的 Redis
客户端时执行事务时是要强制使用 pipeline
的。
pipe = redis.pipeline(transaction=true)
pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute()