《Redis设计与实现》读书笔记 第三/四部分

因为第四部分只挑了感兴趣的部分看,于是将第三第四部分合并起来。主要介绍的有:

  • redis是如何实现主从一致的
  • redissentinel是如何发现下线服务器并进行故障转移的
  • redis的集群是如何进行数据分片的,什么是Gossip协议
  • redis的发布与订阅功能是如何实现的
  • redis的事务

第十五章 复制

用户可以通过执行SLAVEOF命令让一个服务器(从服务器)去复制另一个服务器(主服务器)。复制功能包括同步(sync)和命令传播(command propagate):

  • 同步用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态;
  • 命令传播用户当主服务器被修改时,让主从服务器恢复到一致状态。

针对2.8版本以前采用的是旧版复制,具有较大的性能缺陷。

旧版复制

同步操作是通过由从服务器向主服务器发送SYNC命令完成的,主要包括两个部分:

  • 主服务器收到命令后,使用BGSAVE生成RDB,并且将RDB文件发送给从服务器,从服务器载入RDB文件;

  • 主服务器将执行BGSAVE命令(但没执行完)时保存在缓冲区里的写命令发送给从服务器。

同步操作完毕之后,需要根据命令传播保持主从数据库的一致。但是如果在命令传播的过程中,发生了主从服务器断线,从服务器仍然会向主服务器发送SYNC命令,即主数据库会重新执行BGSAVE生成RDB文件,其中包含了断线前已经同步的数据,是没有必要的。

SYNC命令十分耗费资源,因为在生成RDB的过程中,会耗费主服务器大量的CPU、内存和磁盘I/O资源;在发送RDB的过程中,会占用主从服务器大量的带宽;接收到RDB文件的从服务器需要载入,此时会发生阻塞。

新版复制

新版复制主要可以解决旧版复制功能处理断线重复复制的低效问题,它使用PSYNC替代SYNC,具有完整重同步和部分重同步两种模式:

  • 完整重同步用于初次复制情况,和SYNC处理方式一致

  • 部分重同步用于处理断线重复制情况,只需要将从服务器缺少的写命令发送给从服务器即可。

部分重同步的实现在于,需要记录三个部分:

  • 主从服务器的复制偏移量:主服务器通过比对与重服务器中数据的偏移量是否一致,判断主从服务器是否处于一致状态。

  • 主服务器的复制积压缓冲区:当主从数据库的复制偏移量不一致时,会判断复制积压缓冲区中是否存在偏移量之后的数据(复制积压区是一个固定长度的先进先出队列)。如果数据仍然存在,则进行部分重同步操作;否则执行完整重同步操作。由于复制积压区固定长度,所以需要针对网络断线情况以及主服务器接受的命令数来合理调整其大小。

  • 服务器的运行ID:从服务器中会保存主服务器的运行ID,当断线重连后,从服务器会重新发送该ID,如果与主服务器ID一致,主服务器可以继续执行部分重同步操作,否则执行完整重同步操作。

如果是初次复制操作,从服务器会发送PSYNC ? -1,否则发送PSYNC <runid> <offset>,主服务器会根据情况判断进行完整重同步还是部分重同步操作。

复制的实现

  • 客户端设置主服务器的地址和端口,保存到从服务器的redisServer中,只有当保存工作完成之后,复制工作才会真正开始;

  • 从服务器与主服务器之间建立套接字连接,从服务器会为它关联一个专门用于处理复制工作的文件事件处理器,负责接收RDB文件,接收传播来的写命令等;

  • 从服务器发送PING命令,用来检查套接字的读写状态是否正常以及主服务器是否能够正常处理命令请求(若没有出现PONG的回复,则表示异常,需要重新建立套接字);

  • 身份验证,如果主服务器设置了requirepass,服务器也需要设置masterauth,要么都不设置,否则都会使得从服务器目前的复制工作终端,并从创建套接字开始重新执行,直到身份验证通过或者从服务器主动放弃复制;

  • 从服务器发送REPLCONF listening-port <port-number>,主服务器会将该端口号记录在客户端状态中,主要用于INFO replication的打印中;

  • 从服务器发送PSYNC执行同步,执行以后主从服务器互为双方的客户端,因为主服务器需要将缓冲区/复制积压缓冲区的写命令发送给从服务器。

  • 完成同步之后,进入命令传播阶段。 主服务器只要一直讲命令发送给从服务器,从服务器只要一直接收主服务器的命令,就可以保证主从服务器一致了。

心跳检测

命令传播阶段,从服务器默认1s的频率向主服务器发送REPLCONF ACK <replication_offset>replication_offset是从服务器当前的复制偏移量,有三个用途:

  • 检测主从服务器的网络连接,如果主服务器超过1s没有收到从服务器的ACK命令(可以通过INFO replicationlag看到),说明主从服务器的连接出现了故障。

  • 辅助实现min-slaves配置,主服务器可以提供min-slaves-to-write(要求写入时的从服务器数量)和min-slaves-max-lag(要求写入时的从服务器延迟),防止在不安全的情况下执行写命令。

  • 检测命令丢失,如果在命令传播中发生了写命令丢失的情况,主服务器仍然可以根据复制偏移量将这些数据重新发送给从服务器(类似TCP中的ack)。

补发缺失数据操作在主从服务器没有断线的情况下执行,部分重同步操作在断线重连的情况下执行。

第十六章 Sentinel

Sentinel(哨兵)系统可以监视任意多个主服务器,以及主服务器下的所有从服务器,并且当主服务器下线时将从服务器升级为新的主服务器,选举局部Sentinel领头以及Sentinel领头。

本质

可以使用:

$redis-server /path/to/your/sentinel.conf --sentinel 

或者

$redis-sentinel /path/to/your/sentinel.conf 

启动一个sentinel服务器。

实际上,它是一个运行在特殊模式下的redis服务器。但是它不使用数据库,不会载入RDB或者AOF文件,端口使用REDIS_SENTINEL_PORT指定(普通redis服务器端口使用REDIS_SERVERPORT),它只支持PING/SENTINEL/INFO/SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBEPUNSUBSCRIBE七个命令。

基本数据结构:

struct sentinelState{ 
  // 当前纪元,同一纪元下sentinel领头不能发生变化 
  unint64_t current_epoch; 
  // 记录所有被监视的主服务器相关信息 
  dict * masters; 
  ... 
} 

masters保存了sentinelRedisInstance结构,每一个结构代表被sentinel监视的redis服务器实例(包括主服务器,从服务器,或者是另外的sentinel)。它结构体如下:

struct sentinelRedisInstance{ 
  // 实例名字/运行id 
  char * name; 
  char * runid; 
  unit64_t config_epoch; 
  // 实例响应超过down_after_period,会判断主观下线 
  mstime_t down_after_period; 
  // 判断实例客观下线需要的投票数量 
  int quorum; 
  dict * slaves; 
  // 所有监视该服务器的sentinel 
  dict * sentinels; 
  ... 
} 

当初始化完成之后,sentinel将成为主服务器的客户端。它会与主服务器创建2个异步网络连接:

  • 命令连接,用于发送命令与接收命令回复。

  • 订阅连接,用于订阅主服务器的__sentinel__:hello频道

获取主服务器信息

sentinel默认每10s一次向主服务器发送INFO,根据回复可以获得:

  • 主服务器本身的运行id,以及role域记录的服务器角色;

  • 主服务器下所有从服务器的信息,用来更新主服务器sentinelRedisInstance中的slaves字典

获取从服务器信息

由上面所说可以发现,sentinel可以自动发现从服务器。

当发现主服务器有新的从服务器出现时,sentinel不仅会为新的从服务器创建相应的实例结构,还会创建到从服务器的命令连接和订阅连接。也是默认每10s向从服务器发送INFO,获得从服务器的运行idip以及优先级、偏移量、连接状态等(为了方便以后将从服务器升级为主服务器)。

发现/更新sentinel信息

默认情况下,sentinel会每隔2s向被监视的主/从服务器的__sentinel__:hello频道发送消息。对于监视同一个服务器的多个sentinel来说,一个sentinel发送的信息会被其他sentinel接收,用以互相更新sentinel之间的认知:当一个sentinel接收到其他sentinel(源)发送而来的信息时,会在自己的masters字典中查找对应的主服务器实例结构,检查其sentinels中是否存在源sentinel并进行更新(不存在说明源sentinel刚开始监视该主服务器,此时需要创建并添加到sentinels中)。因此,监视同一个主服务器的多个sentinel可以自动发现对方。

监视同一主服务器的sentinel之间会创建命令连接,并不会创建订阅连接。订阅连接用来发现新的sentinel

检测主观/客观下线

默认情况,sentinel会以1s的频率向其他创建命令连接的实例(主/从服务器),其他sentinel发送PING命令判断实例是否在线。

主观下线

如果一个实例在down_after_milliseconds时间(这个时间也适用于主服务器下所有从服务器,以及监视主服务器的其他sentinel。多个sentinel设置的时间可以不同,即不同sentinel认为同一实例主观下线的标准并不一致)内连续向sentinel返回无效回复,sentinel对在创建的实例中的flags修改为SRI_S_DOWN,表示它认为此实例已经进入主观下线。

客观下线

sentinel判断主服务器已经主观下线之后,会向其他监视主服务器的sentinel询问是否他们也认为主服务器已经下线。当接收到足够数量(quorum)的下线判断时,sentinel会将从服务器判定为客观下线(不同sentinel认为同一实例客观下线的标准也并不一致)。此时flags标记为SRI_O_DOWN。客观下线条件只适用于主服务器。

按照我的理解,即使当主服务器被认为下线了,实际上仍然有sentinel并不觉得它已主观下线。只要半数以上的sentinel认为它已经主观下线,那么这个主服务器其实已经没有当主服务器的必要了,于是可以对它进行故障转移。

选举领头sentinel

只要一个Sentinel发现某个主服务器进入了客观下线状态,监视这个下线主服务器的各个sentinel会进行协商,选举领头sentinel,由其对失效的主服务器执行自动故障迁移操作。

每次领头sentinel选举,无论成功与否,配置纪元都会自增一次。局部领头(每个sentinel都会要求其他sentinel将自己设为局部领头,采取先到先得规则)一旦设置,配置纪元里不能再更改。只有获得半数以上的局部领头,才能真正成为真正的领头。

故障转移 (failover)

领头sentinel需要对已下线的主服务器执行故障转移:

  • 从从服务器中挑选一个转换为主服务器

  • 让已下线主服务器属下的从服务器改为复制新的主服务器

  • 将已下线主服务器设置为新主服务器的从服务器

在升级从服务器时,领头sentinel会以1s的频率发送INFO命令

当以前的主服务器上线之后,Leader会向其发送SLAVEOF命令,使其复制新master的数据。(因为需要更改实例中的masterport之类的参数)

Sentinel集群运行过程中故障转移完成,所有Sentinel又会恢复平等。Leader仅仅是故障转移操作出现的角色。

Leader并不会把自己成为Leader的消息发给其他Sentinel。其他Sentinel等待Leaderslave选出master后,检测到新的master正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。

第十七章 集群

redis集群是redis提供的分布式方案,通过分片进行数据共享,提供复制和故障转移等功能。

节点

一个节点是一个运行在集群模式下的服务器,通过cluser-enabled选项进行配置。它与普通服务器的区别包括:

  • serverCron函数会执行clusterCron,执行集群下常规操作
  • 集群中需要用到的数据,采用cluserNodeclusterLink以及clusterState来保存。

clusterNode

每个节点都有自己的clusterNode结构来记录自己的状态,包括节点的创建时间,名字,配置纪元IP地址和端口号等,还包含了一个clusterLink结构的指针,指向link

struct clusterNode{
  // 记录节点处理哪些槽
  unsigned char slots[16384/8];
  int numslots;
  
  clusterLink * link;
  // 当是从节点的情况下,指向主节点
  clusterNode * slaveof;
  // 是主节点
  int numslaves;
  clusterNode ** slaves;
  // 下线报告
  list * fail_reports;
  ...
}

clusterLink

这个结构保存了连接节点所需的比如套接字描述符,输入缓冲区和输出缓冲区以及与这个节点相关联的节点。

clusterLink中保存的输入缓冲区和输出缓冲区是用于连接节点的,不同于redisClient中的是用于连接客户端的。

clusterState

除了上面两个数据结构,每个节点还保存了clusterState,即在当前节点的视角下,集群目前所处的状态:

struct clusterState{
  // 指向自己
  clusterNode * myself;
  // 集群节点名单,键为节点名字,值为节点对应的clusterNode结构
  dict *nodes;
  // 记录所有槽的指派信息
  clusterNode * slots[16384];
  // 槽和键之间的关系
  zskiplist * slot_to_keys;
  // 当前节点正在从其他节点导入的槽
  clusterNode * importing_slots_from[16384];
  // 当前节点正在迁移至其他节点的槽
  clusterNode * migrating_slots_to[16384];
  ...
}

节点之间通过发送CLUSTER MEET命令,将另一个节点(B)加入至当前节点(A)所在的集群里。AB之间会互相为对方建立一个clusterNode结构,之后A通过Gossip协议将节点B的信息传播给其他节点,让其他节点也与B进行握手,最后被所有节点认识。

槽指派

集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这个槽中的一个,每个节点可以处理0~16384个槽。当数据库中有任何一个槽没有得到处理,那么集群处于fail(下线)状态

每个节点的slot属性是一个二进制位数组,总共2048个字节,可以根据每一个位上的二进制值来判断节点是否负责处理该槽(复杂度为o(1))。numslots负责记录节点处理的槽的数量,即值为1的二进制位数量。

一个节点除了会将自己负责的槽节点记录下来,还会通过消息发送给集群中的其他节点,以此来告知其他节点自己负责处理哪些槽。其他节点会在自己的nodes字典中查找该节点对应的clusterNode结构,对其中的slots进行保存更新。

由此可见,clusterNode中记录了当前节点处理的槽,而clusterState记录了所有槽分别被哪些节点处理,需要同时保存的原因在于:

  • 如果只有节点记录了自身处理的槽,当查询某个槽是否被指派或者指派给了所有节点,就需要遍历所有的clusterNodeslots数组
  • 如果程序需要将某个节点负责的槽信息发送给其他节点时(集群中的每个节点都会将自己的slots数组发送给集群中的其他节点),只需要发送对应节点的clusterNode.slots,否则就需要遍历clusterState.slots,再记录下该节点负责的槽

因此,当执行CLUSTER ADDSLOTS命令时,首先会将clusterState.slots中对应的槽指向当前节点的clusterNode结构(如果发现已经指派,则返回错误),然后将clusterNode.slots中对应的槽位置为1

处理命令

当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并且检查是否指派给了自己(clusterNode.slots[i]是否指向自己),如果属于当前节点,直接处理;否则节点会向客户端返回一个MOVED错误(不可见,单机会报错),指引客户端转向正确节点。

节点和单机服务器的区别在于,节点只能使用0号数据库。

节点还使用了跳跃表(slots_to_keys)保存槽与键之间的关系,每个节点的分值都是一个槽号,每个节点的成员都是数据库键(同一分值下,按成员字典序排序)。通过这个跳跃表,节点可以方便地对属于某个或者某些槽的所有数据库键进行批量操作。

重新分片

是指将已经指派给某个节点的槽改派给另一个节点,并且将所属的键值对从源节点移动至目标节点,可以在线进行。它由redis-trib负责,进行原子地迁移:

  • SETSLOT <i> IMPORTING <source_id>:向目标节点发送命令,将其clusterState.importing_slots_from[i]的值设置为source id所代表节点的clusterNode结构。
  • SETSLOG <i> MIGRATING <target_id>:向源节点发送命令,将其clusterState.migrating_slots_to[i]的值设置为target id所代表节点的clusterNode结构。
  • GETKEYSINSLOT
  • MIGRATE
  • SETSLOT NODE

需要注意的是,当在迁移的过程中,槽的一部分键值保存在源节点中,另一部分保存在目标节点中时,如果需要处理的数据库键下号属于正在被迁移的槽时:

  1. 首先在自己数据库里查找指定的键,如果找到了则进行处理
  2. 如果没有找到(查询migrating_slots_to[i]),有可能已经被迁移,源节点将返回一个ASK错误(不可见),指引客户端转向目标节点。之后客户端会先发送一个ASKING命令(打开自身的REDIS_ASKING标识,否则会被拒绝),再重新发送本来想要执行的命令

REDIS_ASKING是一个一次性标识。

复制与故障转移

集群中的主节点用于处理槽,从节点则用于复制某个主节点,在被复制的主节点下线时,代替主节点处理命令请求。

复制

从节点会在自己的clusterState.nodes字典中找到主节点对应的clusterNode结构,并且将自己的slaveof指向该结构,修改自身的flags。集群中的所有节点都会在自身存储的代表主节点的clusterNode结构的slave属性和numslaves属性中记录正在复制这个主节点的从节点名单。

故障转移

集群中的每个节点会定期向集群中的其他节点发送PING消息,以此检测对方是否在线。如果指定时间内,没有返回PONG,则源节点会将此节点标记为疑似下线。当主节点A得知主节点B认为主节点C疑似下线,A会在自身存储的CclusterNode结构中为其添加B对其的下线报告。

如果在一个集群中,半数以上的主节点都将某个主节点X标记为疑似下线,那么这个主节点X将会被标记为已下线,将它标记为下线的主节点会向集群广播主节点X已下线的消息,所有收到这条消息的节点都会将其标记为下线。

当从节点发现自己的主节点下线后,会选举一个新的主节点,将原先主节点负责处理的槽全部指派给自己,并且告知整个集群自己已经变成新的主节点。

选举的过程大概如下:在每一个配置纪元里,从节点会向所有负责处理槽的主节点发送投票请求,要求对方将票投给自己(仍然是采取先来先到的原则)。当一个从节点获得超过N/2+1张选票时(具有投票权的主节点个数为N),这个从节点就当选成为新的主节点。如果当前配置纪元没有选出节点,则会进入新的配置纪元直到选举完成。

消息

基本的消息结构为:

union clusterMsgData{
  // Gossip消息的正文
  struct {...} ping;
  struct {...} fail;
  struct {...} publish;
}

节点之间相互发送的消息有五种:

  • MEET:发送者邀请接收者加入发送者所在的集群
  • PING:每隔1s会从已知节点列表中随机选出5个节点,检测其中最长时间没有发送PING消息的节点是否在线(这样可以防止过久时间没有与该节点沟通)
  • PONG:作为对MEET或者PING的回复,也可以让整个集群中的其他节点刷新此节点的认识
  • FAIL:主节点A判断主节点B已经下线时,会向集群广播此节点的FAIL消息
  • PUBLISH:节点收到此命令会向集群广播,所有接收到的节点都会执行相同的命令

节点发送的所有消息都由消息头包裹,包含自身的一些信息,可以用以消息接收者对自己所记录的消息发送者的相关属性进行更新。

Gossip协议由前三种消息实现,它们使用同样的消息正文,接收节点根据type来判断具体是哪一种消息。每次发送时,发送者都从自己的已知节点中随机选出两个节点,将其消息包含在消息正文中的Gossip消息结构(clusterMsgDataGossip)中,接收者根据对这两个节点的认知程度选择进行握手或者更新。

显然,当集群节点数量比较大的情况下,单纯使用Gossip协议,传播信息会带来一定延迟。而FAIL消息需要尽快地告知所有节点,因此FAIL节点中只需要保存下线节点的名字,告知接收者该节点已经下线。

当某个节点收到客户端发来的PUBLISH <channel> <message>时,它不仅会向channel发送message,同时还会向集群广播PUBLISH消息,最终实现所有节点都向channel发送message

第十八章 发布与订阅

发布订阅功能包括PUBLISH/SUBSCRIBE/PSUBSCRIBE等组成。

订阅/退订频道

redis将频道订阅关系保存在pubsub_channels里,它是一个字典:

struct redisServer{
  dict * pubsub_channels;
}

其中,字典的键值代表某个订阅的频道,键的值是一个链表,代表订阅这个频道的所有客户端。

当用户执行SUBSCRIBE时,会有两种情况:

  • 频道已经有订阅者,那么客户端添加至链表的尾部
  • 如果没有订阅者,在字典中创建一个新的键,并且将客户端添加至链表中成为第一个元素

退订UNSUBSCRIBE与订阅正好相反,如果当删除客户端后链表变为空链表,那么也会在字典中删除这个频道。

订阅/退订模式

redis将模式订阅关系保存在pubsub_patterns里,它是一个链表:

struct redisServer{
  list * pubsub_patterns;
}

链表中的每一个节点都包含了一个pubsubPattern结构,记录了被订阅的模式,以及订阅模式的客户端。

当用户执行PSUBSCRIBE时,会有两个步骤:

  1. 新建一个pubsubPattern结构,记录被订阅的模式以及订阅客户端

  2. 将新建的结构添加至pubsub_patterns的表尾

也是就是,如果有订阅模式已经存在,也仍然会新建一个新的pubsubPattern节点。

当用户执行PUNSUBSCRIBE退订模式时,会在链表中寻找订阅模式与客户端都匹配的节点,将其删除。

PUBLISH

当用户执行发布PUBLISH <channel> <message>时,会分为两个步骤:

  1. pubsub_channels中寻找到channel,遍历其订阅者名单,然后将message发送给channel频道的订阅者

  2. 遍历整个pubsub_patterns链表,查找与channel频道相匹配的模式,并将消息发送给订阅了这些模式的客户端

于是PUBLISH可以表示为:

def publish(channel, message):
  channel_publish()
  pattern_publish()

查看订阅信息(PUBSUB)

PUBSUB CHANNELS [pattern]

  • 如果不给定pattern参数,返回服务器当前被订阅的所有频道
  • 如果给定pattern参数,返回服务器当前订阅频道中与其相匹配的频道

它是通过遍历pubsub_channels的所有键,并返回符合条件的键来实现的。

PUBSUB NUMSUB

接收任意多个频道作为输入参数,返回频道的订阅者数量。它是通过在pubsub_channels中找到对应频道的订阅者链表,并且返回链表长度来实现的。

PUBSUB NUMPAT

这个命令与订阅模式相关,返回当前被订阅模式的数量。是通过返回pubsub_patterns链表的长度来实现的。由于可能会有重复的模式订阅,因此这个命令返回参与了模式订阅的客户端之和,而不是总共有多少种模式订阅。

第十九章 事务

实际上redis事务提供的是一个将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,在事务执行期间不会中断事务去执行其他客户端的命令。

事务执行原理

包括三个步骤:

  1. 事务开始:使用MULTI标记,通过打开客户端的REDIS_MULTI标识来完成从非事务状态切换至事务状态
  2. 命令入队:当客户端处于非事务状态下,发送的命令会立即被执行;而当处于事务状态下,除了EXEC/DISCARD/WATCH/MULTI四个命令,服务器并不会立即执行,只是将其放入事务队列里,并返回QUEUED
  3. 事务执行:当客户端发送EXEC命令时,服务器会遍历客户端的事务队列,执行队列中保存的所有命令,移除REDIS_MULTI标志,清零入队命令计数器,释放事务队列,最后返回执行结果

事务队列:

每个客户端都有自己的事务队列,保存在mstate属性里:

struct redisClient{
  multiState mstate;
}
struct multiState{
  // 事务队列,FIFO
  multiCmd *commands;
  // 已入队命令计数
  int count;
}

每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针,命令的参数以及参数的数量。

WATCH

WATCH命令实际上一个乐观锁,可以在执行EXEC之前,监视任意数量的数据库键。它是通过在数据库中保存的watched_keys字典实现的:

struct redisDb{
  // key:value=监视的键:监视该键的客户端链表
  dict * watched_keys;
}

通过watched_keys,服务器可以知道哪些键被哪些客户端监视。每当执行对数据库进行修改的命令,例如SET/LPUSH等,执行之后都会调用touchWatchKeywatched_keys进行检查,查看是否有客户端正在监视刚被修改过的键:如果有,则打开客户端的REDIS_DIRTY_CAS标志,表示客户端的事务安全性已经被破坏。

服务器接收到客户端发来的EXEC后,根据客户端是否打开了REDIS_DIRTY_CAS标志来决定是否执行事务。当此标志打开时,服务器将拒绝客户端提交的事务。因此,即使事务中不涉及watchkey时,如果自身REDIS_DIRTY_CAS标志被打开(有其他客户端修改了key时),事务仍然不会提交。

事务的ACID

redis中,事务具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),当事务运行在某种持久化模式下,事务也具有持久性(Durability)。

原子性

redis的原子性表现为,事务队列中的命令要么全部执行,要么全部都不执行。事务执行有2种情况:

  • 语法错误:此时命令不会入队,整个事务都不会执行,保证了原子性
  • 执行错误:虽然出现了错误,但是也是在命令执行时出现的错误,事务中命令全部执行,也保证了原子性

也即是说,只要命令入队时没有发生错误而被服务器拒绝整个事务的执行,事务队列中的每条命令都会被执行

redis事务与关系型数据库事务的最大区别在于,不支持回滚。在事务队列某个命令执行出现错误时,并不影响前面命令执行的结果。

一致性

一致性是指,符合数据库本身定义和要求,不会包含非法的或者无效的错误数据

事务执行时如果出现:

  • 语法错误:在命令入队时发现错误,将会拒绝执行这个事务,因此redis不会被带有入队错误的事务影响
  • 执行错误:由于出错的命令会进行相应的错误处理,因此出错的命令也不会对数据库做任何修改,不会影响一致性
  • 服务器停机:如果运行在无持久化模式下,或者没有RDB/AOF文件的存在,数据库重启之后将是空白的。如果服务器存在AOF或者RDB的情况下,服务器仍然会将数据库还原到一致性的状态下。

隔离性

redi使用单线程的方式执行事务,并且服务器不会在执行事务期间对事务中断,因此事务总是以串行的方式运行,是具有隔离性的。

持久性

持久性是指,当事务执行完毕时,执行这个事务所得到的结果已经被保存在永久性存储介质里

redis并没有替事务提供任何额外的持久化功能,因此它的持久性由持久化模式决定:

  • 无持久化模式:所有数据将丢失,不具有持久性
  • RDB模式:异步的BGSAVE不能保证第一时间将数据存储在硬盘上,不具有持久性
  • AOF模式:只有在appendfsync的值为always时,总会在执行命令之后调用同步,才具有持久性;否则如果appendfsync值为everysec或者no时,都有可能造成数据丢失

不论redis在什么模式下运行,事务最后加上SAVE总可以保证持久性,但是效率太低。

posted @ 2020-10-25 15:37  yuyinzi  阅读(71)  评论(0)    收藏  举报