导读:
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。
单进程好处:所有命令,包括或者事物这种大的打包命令,只要有一个执行其他阻塞
组合这四个命令实现事物:
MULTI才进入了事物状态,命令不会立即执行,会在队列中
EXEC执行事物
DISCARD 命令用于取消一个事务
WATCH 只能在客户端进入事务状态之前执行
为了防止其他客户端不用事物的方式进入事物状态,只是简单的执行单条命令改变事物命令中的值,此种情况回退
WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败
Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
Redis 也不会进行任何的重试或者回滚动作。
WATCH 的触发:
在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,诸如此类),
multi.c/touchWatchedKey 函数都会被调用 —— 它检查数据库的 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话,
程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开
只有事物状态exe执行前会检查REDIS_DIRTY_CAS,打开了就执行失败
由于单进程好处:所有命令,包括或者事物这种大的打包命令,只要有一个执行其他阻塞,所以只要执行前看有没有变化即可
原子性(Atomicity)(没有原子性---(已经执行的不会回滚)停电的状态)
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。
另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动作。
Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务功能, 本章首先讨论使用 MULTI 、 DISCARD 和 EXEC 三个命令实现的一般事务, 然后再来讨论带有 WATCH 的事务的实现。
因为事务的安全性也非常重要, 所以本章最后通过常见的 ACID 性质对 Redis 事务的安全性进行了说明。
事务
事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。
以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
下文将分别介绍事务的这三个阶段。
命令入队
当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行:
redis> SET msg "hello moto"
OK
redis> GET msg
"hello moto"
但是, 当客户端进入事务状态之后, 服务器在收到来自客户端的命令时, 不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED
, 表示命令已入队:
redis> MULTI
OK
redis> SET msg "hello moto"
QUEUED
redis> GET msg
QUEUED
以下流程图展示了这一行为:
事务队列是一个数组, 每个数组项是都包含三个属性:
- 要执行的命令(cmd)。
- 命令的参数(argv)。
- 参数的个数(argc)。
举个例子, 如果客户端执行以下命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那么程序将为客户端创建以下事务队列:
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
执行事务
前面说到, 当客户端进入事务状态之后, 客户端发送的命令就会被放进事务队列里。
但其实并不是所有的命令都会被放进事务队列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 这四个命令 —— 当这四个命令从客户端发送到服务器时, 它们会像客户端处于非事务状态一样, 直接被服务器执行:
如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令: 最先入队的命令最先执行, 而最后入队的命令最后执行。
比如说,对于以下事务队列:
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
程序会首先执行 SET 命令, 然后执行 GET 命令, 再然后执行 SADD 命令, 最后执行 SMEMBERS 命令。
执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。
比如说,对于上面给出的事务队列,程序将为队列中的命令创建如下回复队列:
0 |
status code reply | OK |
1 |
bulk reply | "Mastering C++ in 21 days" |
2 |
integer reply | 3 |
3 |
multi-bulk reply | ["Mastering Series", "C++", "Programming"] |
当事务队列里的所有命令被执行完之后, EXEC 命令会将回复队列作为自己的执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。
事务的整个执行过程可以用以下伪代码表示:
def execute_transaction():
# 创建空白的回复队列
reply_queue = []
# 取出事务队列里的所有命令、参数和参数数量
for cmd, argv, argc in client.transaction_queue:
# 执行命令,并取得命令的返回值
reply = execute_redis_command(cmd, argv, argc)
# 将返回值追加到回复队列末尾
reply_queue.append(reply)
# 清除客户端的事务状态
clear_transaction_state(client)
# 清空事务队列
clear_transaction_queue(client)
# 将事务的执行结果返回给客户端
send_reply_to_client(client, reply_queue)
在事务和非事务状态下执行命令
无论在事务状态下, 还是在非事务状态下, Redis 命令都由同一个函数执行, 所以它们共享很多服务器的一般设置, 比如 AOF 的配置、RDB 的配置,以及内存限制,等等。
不过事务中的命令和普通命令在执行上还是有一点区别的,其中最重要的两点是:
-
非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定是同一个;
而事务状态则是以一个事务为单位,执行事务队列中的所有命令:除非当前事务执行完毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。
-
在非事务状态下,执行命令所得的结果会立即被返回给客户端;
而事务则是将所有命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给客户端。
事务状态下的 DISCARD 、 MULTI 和 WATCH 命令
除了 EXEC 之外, 服务器在客户端处于事务状态时, 不加入到事务队列而直接执行的另外三个命令是 DISCARD 、 MULTI 和 WATCH 。
DISCARD 命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK
给客户端, 说明事务已被取消。
Redis 的事务是不可嵌套的, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 然后继续等待其他命令的入队。 MULTI 命令的发送不会造成整个事务失败, 也不会修改事务队列中已有的数据。
WATCH 只能在客户端进入事务状态之前执行, 在事务状态下发送 WATCH 命令会引发一个错误, 但它不会造成整个事务失败, 也不会修改事务队列中已有的数据(和前面处理 MULTI 的情况一样)。
带 WATCH 的事务
WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。
以下示例展示了一个执行失败的事务例子:
redis> WATCH name
OK
redis> MULTI
OK
redis> SET name peter
QUEUED
redis> EXEC
(nil)
以下执行序列展示了上面的例子是如何失败的:
T1 | WATCH name |
|
T2 | MULTI |
|
T3 | SET name peter |
|
T4 | SET name john |
|
T5 | EXEC |
在时间 T4 ,客户端 B 修改了 name
键的值, 当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name
这个被监视的键已经被修改, 因此客户端 A 的事务不会被执行,而是直接返回失败。
下文就来介绍 WATCH 的实现机制,并且看看事务系统是如何检查某个被监视的键是否被修改,从而保证事务的安全性的。
WATCH 命令的实现
在每个代表数据库的 redis.h/redisDb
结构类型中, 都保存了一个 watched_keys
字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。
比如说,以下字典就展示了一个 watched_keys
字典的例子:
其中, 键 key1
正在被 client2
、 client5
和 client1
三个客户端监视, 其他一些键也分别被其他别的客户端监视着。
WATCH 命令的作用, 就是将当前客户端和要监视的键在 watched_keys
中进行关联。
举个例子, 如果当前客户端为 client10086
, 那么当客户端执行 WATCH key1 key2
时, 前面展示的 watched_keys
将被修改成这个样子:
通过 watched_keys
字典, 如果程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 如果程序要获取监视某个键的所有客户端, 那么只要取出键的值(一个链表), 然后对链表进行遍历即可。
WATCH 的触发
在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,诸如此类), multi.c/touchWatchedKey
函数都会被调用 —— 它检查数据库的 watched_keys
字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS
选项打开:
当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
- 如果客户端的
REDIS_DIRTY_CAS
选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。 - 如果
REDIS_DIRTY_CAS
选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
可以用一段伪代码来表示这个检查:
def check_safety_before_execute_trasaction():
if client.state & REDIS_DIRTY_CAS:
# 安全性已破坏,清除事务状态
clear_transaction_state(client)
# 清空事务队列
clear_transaction_queue(client)
# 返回空回复给客户端
send_empty_reply(client)
else:
# 安全性完好,执行事务
execute_transaction()
举个例子,假设数据库的 watched_keys
字典如下图所示:
如果某个客户端对 key1
进行了修改(比如执行 DEL key1
), 那么所有监视 key1
的客户端, 包括 client2
、 client5
和 client1
的 REDIS_DIRTY_CAS
选项都会被打开, 当客户端 client2
、 client5
和 client1
执行 EXEC 的时候, 它们的事务都会以失败告终。
最后,当一个客户端结束它的事务时,无论事务是成功执行,还是失败, watched_keys
字典中和这个客户端相关的资料都会被清除。
事务的 ACID 性质
勘误:Redis 的事务是保证原子性的,本节的内容将原子性和回滚功能混淆了,等待修复中。 —— 2013.6.23
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。
Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
以下四小节是关于这四个性质的详细讨论。
原子性(Atomicity)
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。
另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动作。
一致性(Consistency)
Redis 的一致性问题可以分为三部分来讨论:入队错误、执行错误、Redis 进程被终结。
入队错误
在命令入队的过程中,如果客户端向服务器发送了错误的命令,比如命令的参数数量不对,等等, 那么服务器将向客户端返回一个出错信息, 并且将客户端的事务状态设为 REDIS_DIRTY_EXEC
。
当客户端执行 EXEC 命令时, Redis 会拒绝执行状态为 REDIS_DIRTY_EXEC
的事务, 并返回失败信息。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> set key
(error) ERR wrong number of arguments for 'set' command
redis 127.0.0.1:6379> EXISTS key
QUEUED
redis 127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
因此,带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。
执行错误
如果命令在事务执行的过程中发生错误,比如说,对一个不同类型的 key 执行了错误的操作, 那么 Redis 只会将错误包含在事务的结果中, 这不会引起事务中断或整个失败,不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令, 所以它对事务的一致性也没有影响。
Redis 进程被终结
如果 Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死,那么根据 Redis 所使用的持久化模式,可能有以下情况出现:
-
内存模式:如果 Redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所以数据总是一致的。
-
RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数据库快照(snapshot),所以它的数据可能不是最新的,但只要 RDB 文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的。
-
AOF 模式:因为保存 AOF 文件的工作在后台线程进行,所以即使是在事务执行的中途,保存 AOF 文件的工作也可以继续进行,因此,根据事务语句是否被写入并保存到 AOF 文件,有以下两种情况发生:
1)如果事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只要 AOF 文件本身没有因为其他问题而出错,那么还原后的数据库总是一致的,但其中的数据不一定是最新的。
2)如果事务的部分语句被写入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不完整,Redis 会退出,并报告错误。需要使用 redis-check-aof 工具将部分成功的事务命令移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直到事务执行之前为止)。
隔离性(Isolation)
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
持久性(Durability)
因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定:
-
在单纯的内存模式下,事务肯定是不持久的。
-
在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的。
-
在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用
fsync
或fdatasync
将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。
小结
- 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制。
- 事务在执行过程中不会被中断,所有事务命令执行完之后,事务才能结束。
- 多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。
- 带
WATCH
命令的事务会将客户端和被监视的键在数据库的watched_keys
字典中进行关联,当键被修改时,程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS
选项打开。 - 只有在客户端的
REDIS_DIRTY_CAS
选项未被打开时,才能执行事务,否则事务直接返回失败。 - Redis 的事务保证了 ACID 中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
相关推荐
Redis 事物(下).flv
该脚本文件将redis数据库处理数据的函数集中进行封装,方便使用.
Redis 事务是数据库系统中的一种特性,它允许用户在一个操作序列中执行多个命令,这些命令要么全部执行,要么全部不执行,确保了数据的一致性和完整性。在Redis中,事务的实现主要涉及到四个核心命令:MULTI、EXEC、...
### 一个简单的介绍并说明了一下resdis事物的不保证原子性 # redis集群 ### redis集群三种模式 ### Redis哈希槽 ### 什么情况下会导致整个集群不可用 ### 集群三种方案之间都有哪些区别,其优点和缺点是什么 ### ...
Redisson是基于Redis的Java客户端,它提供了丰富的数据结构和服务,包括分布式锁、信号量、队列、计数器等,极大地扩展了Redis在分布式系统中的应用能力。本篇文章将详细探讨如何使用Redisson实现Redis分布式事务锁...
同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)等 。 Redis具有丰富的客户端,支持现阶段流行的大多数编程语言。php常用的是phpredis, JAVA常用的是Jedis( ...
redis缓存数据库,引入属性文件,开启事务,可以通过transcational注解控制,
因为redis 本事的类库 没有函数说明,使用起来很是不变,在偶然的情况下看到一篇文章,特意分享给大家 事实上,IRedisClient里面的很多方法,其实就是Redis的命令名。只要对Redis的命令熟悉一点就能够非常...事物处理
回答好了加分,面试问到spring,你为什么用spring,spring是怎样提供事物管理的,然后spring ioc是怎么管理对象和对象之间的依赖关系的?-CSDN论坛 Spring及三大框架整合 - zgy_boke的博客 - CSDN博客 (3条消息)【二...
API 设计允许几乎任何事物的自定义实现。版本目前支持的基数有两个主要版本v3 是更成熟的版本,但缺少 v4 的完善 API。v3 目前仅接受错误修复。v4 具有与 v3 相同的功能,甚至更多!最大的卖点是更完善的 API。完全...
RedisTSCounter允许随时间跟踪事物的计数,在给定大小的桶中分割时间,并获得列表中前 N 个事物的计数排名。 聚合来自不同桶的数据时,默认行为是为所有桶分配相同的权重。 可以改用指数衰减加权,以便为最近的事件...
Redigo 是 Redis 数据库的 Go 客户端。特性一个类似 Print 的 API,支持所有 Redis 命令Pipelining,包含管道事物发布/订阅连接池脚本助手类型,优化 EVALSHA 使用Helper 函数 标签:Redigo redis
缓存框架 基于.NET Redis客户端库添加了一些... :计算独特的事物。 :默认情况下为压缩二进制序列化器,或提供您自己的序列化。 :列表,集合,排序集,哈希和位图支持为托管集合。 Redis键空间通知:订阅发布/订
node.js-清洁架构Node.js 中清洁架构的一个用例包括 Express.js、MongoDB 和 Redis 作为主要(但可替换)的基础设施。概述这个示例是一个简单的 RESTful API 应用程序,用户可以通过使用Clean Architecture创建/更新...
Redis目前对事物的支持相对简单。Redis只能保证一个client发起的事务中的命令可以连续的执行,而中间不会插入其他的client命令。当一个client在一个链接中发出multi命令时,这个链接会进入一个事务上下文,该连接...
在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。 2). 和关系型数据库中的事务相比,在Redis事务中如果有某一...
6. **事务**:虽然不支持传统数据库的ACID特性,但Redis提供了一种简单的事物模型,可以一次性执行多个命令。 7. **主从复制**:通过复制机制,可以创建多个从节点,提高读取性能和容错能力。 8. **Sentinel监控与...
1. **配置Redis连接**:首先需要在项目中集成Spring Data Redis,配置Redis连接信息,以便应用能够与Redis服务器通信。 2. **创建自定义切面**:创建一个名为`RedisLockAspect`的切面,该切面会在运行时检查带有`@...