Redis 事务
Redis 事务
Redis 仅支持“非严格”的事务。所谓“非严格”是指:Redis 事务保证“全部执行命令”;但是,Redis 事务“不支持回滚”。
关键词:
事务
、ACID
、MULTI
、EXEC
、DISCARD
、WATCH
Redis 事务简介
什么是 ACID
ACID 是数据库事务正确执行的四个基本要素。
- 原子性(Atomicity)
- 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。
- 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
- 一致性(Consistency)
- 数据库在事务执行前后都保持一致性状态。
- 在一致性状态下,所有事务对一个数据的读取结果都是相同的。
- 隔离性(Isolation)
- 一个事务所做的修改在最终提交以前,对其它事务是不可见的。
- 持久性(Durability)
- 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
- 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。
一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易。
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对系统崩溃的情况。
Redis 事务的特性
Redis 的事务总是支持 ACID 中的原子性、一致性和隔离性, 当服务器运行在 AOF 持久化模式下, 并且 appendfsync
选项的值为 always
时, 事务也具有持久性。
但需要注意的是:Redis 仅支持“非严格”的事务。这里的“非严格”,其实指的是 Redis 事务只能部分保证 ACID 中的原子性。
- Redis 事务保证全部执行命令 - Redis 事务中的多个命令会被打包到事务队列中,然后按先进先出(FIFO)的顺序执行。事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。
- Redis 事务不支持回滚 - 如果命令执行失败不会回滚,而是会继续执行下去。
Redis 官方的事务特性文档给出的不支持回滚的理由是:
- Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
Redis 事务应用
MULTI
、EXEC
、DISCARD
和 WATCH
是 Redis 事务相关的命令。
事务可以一次执行多个命令, 并且有以下两个重要的保证:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
MULTI
MULTI
命令用于开启一个事务,它总是返回 OK 。
MULTI
执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行。
以下是一个事务例子, 它原子地增加了 foo
和 bar
两个键的值:
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
EXEC
EXEC
命令负责触发并执行事务中的所有命令。
- 如果客户端在使用
MULTI
开启了一个事务之后,却因为断线而没有成功执行EXEC
,那么事务中的所有命令都不会被执行。 - 另一方面,如果客户端成功在开启事务之后执行
EXEC
,那么事务中的所有命令都会被执行。
MULTI
和 EXEC
中的操作将会一次性发送给服务器,而不是一条一条发送,这种方式称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
DISCARD
当执行 DISCARD
命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出。
示例:
> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"
WATCH
WATCH
命令可以为 Redis 事务提供 check-and-set (CAS)行为。
被 WATCH
的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC
执行之前被修改了, 那么整个事务都会被取消, EXEC
返回 nil-reply
来表示事务已经失败。
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
使用上面的代码, 如果在 WATCH
执行之后, EXEC
执行之前, 有其他客户端修改了 mykey
的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。
这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。
WATCH
使得 EXEC
命令需要有条件地执行:事务只能在所有被监视键都没有被修改的前提下执行,如果这个前提不能满足的话,事务就不会被执行。
WATCH
命令可以被调用多次。对键的监视从 WATCH
执行之后开始生效,直到调用 EXEC
为止。
用户还可以在单个 WATCH
命令中监视任意多个键,例如:
redis> WATCH key1 key2 key3
OK
取消 WATCH 的场景
当 EXEC
被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。
使用无参数的 UNWATCH
命令可以手动取消对所有键的监视。 对于一些需要改动多个键的事务, 有时候程序需要同时对多个键进行加锁, 然后检查这些键的当前值是否符合程序的要求。 当值达不到要求时, 就可以使用 UNWATCH
命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。
使用 WATCH 创建原子操作
WATCH
可以用于创建 Redis 没有内置的原子操作。
举个例子,以下代码实现了原创的 ZPOP
命令,它可以原子地弹出有序集合中分值(score
)最小的元素:
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC