Ⅰ redis单实例数据迁移到cluster(redis5.0.5)
1、检查redis集群状态
/app/redis_6379/bin/redis-cli -p 6379 -a 123456 --cluster check 10.1.1.90:6379
2、将所有master上的slots重新分配到一个master上
/app/redis_6379/bin/redis-cli -p 6379 -a 123456 --cluster reshard --cluster-from --cluster-to --cluster-slots 5462 --cluster-yes 10.1.1.90:6379
/app/redis_6379/bin/redis-cli -p 6379 -a 123456 --cluster reshard --cluster-from --cluster-to --cluster-slots 5462 --cluster-yes 10.1.1.90:6379
3、停止除唯一持有slots的master的其他节点
4、停止唯一持洞和有slots的master节点
5、将单实例的持久化文件rdb/aof文件拷贝到唯一持有slots的master节点的数据目录下
6、启动唯一持有slots的master节点
7、启缓大动除唯一持有slots的master的其他节点
8、在集群masters间重新分配slots
/app/redis_6379/bin/redis-cli -p 6379 -a 123456 --cluster reshard --cluster-from --cluster-to --cluster-slots 5462 --cluster-yes 10.1.1.90:6379
/app/redis_6379/bin/redis-cli -p 6379 -a 123456 --cluster reshard --cluster-from 纳哪盯--cluster-to --cluster-slots 5462 --cluster-yes 10.1.1.90:6379
9、检查redis集群状态
/app/redis_6379/bin/redis-cli -p 6379 -a 123456 --cluster check 10.1.1.90:6379
Ⅱ Redis cluster 原理
Redis cluster 实现渗裂了所有的single key 操作,对于multi key操作的话,这些key必须在一个节点上面,redis cluster 通过 hash tags决定key存贮在哪个slot上面。
节点首要功能是存贮数据,集群状态,映射key到相应的节点。自动发现其他节点,发现失败节点,让从变为主。
为了完成以上功能,cluster使用tcp和二进制协议(Redis Cluster Bus),节点间互联.node 同时使用gossip协议传播信息,包括节点的发现,发送ping包,Pub/Sub信息。
因为节点并不代理请求转发,会返回MOVED和ASk错误,clients就可以直连到其他节点。client理论上面可以给任意节点发送请求,如果需要就重定向。但实际应用中client存贮一个从key到node的map来提高性能。
Redis cluster 使用异步复制的模式,故障转移的时候,被选为主的节点,会用自己的数据去覆盖其他副本节点的数据。所以总有一个时间口会丢失数据。
下面一个例子会丢失数据:
master partition 变得不可用
它的一个从变为主
一定时间之后,这个主又可用了
客户端这时候还使用旧的的路由,在这个主变为从之前,写请求到达这个主。
3、可用性
假设n个主节点,每个主下面挂载一个从,挂掉一个,集群仍然可用。挂点两个,可用性是1 -(1/(n 2 -1))(第一个节点挂掉后,还剩下n 2-1个节点),只有一个节点的主挂掉的可能性是 1/n*2 -1)
replicas migration 使可用性更高
4、性能
reids cluster 不代理请求到正确的节点,而是告诉客户端正确的亮槐节点
client 会保存一份最新的key与node映射,一般情况,会直接访问到正确的节点。
异步写副本
一般的操作和单台redis有相同的性能,一个有n个主节点的集群性能接近n*单个redis
综上 高性能 线性扩展 合理的写安全 高可用 是rediscluser 的主要目标
因为首先redis 存贮的数据量会特别大,如果合并需要更大的空间
key空间分布被划分为16384个slot,所以一个集群,主节点的个数最大敬喊友为16384(一般建议master最大节点数为1000)
HASH_SLOT = CRC16(key) mod 16384
hash tag 是为了保证不同的key,可以分布到同一个slot上面,来执行multi-key的操作
hash tag的规则是以第一个{开始,到第一个}结尾,中间的内容,来做hash。
例子
{user1000}.following 与 {user1000}.followers user1000作为key
foo{}{bar} 整个key
{{bar}} {bar 为key
{bar}{zap} bar 为key
Ruby Example
从左到右依次为:node id, address:port, flags, last ping sent, last pong received, configuration epoch, link state, slots
其中node id是第一次启动获得的一个160字节的随机字符串,并把id保存在配置文件中,一直不会再变
每个节点有一个额外的TCP端口,这个端口用来和其他节点交换信息。这个端口一般是在与客户端链接端口上面加10000,比如客户端端口为6379,那么cluster bus的端口为16379.
node-to-node 交流是通过cluster bus与 cluster bus protocol进行。其中cluster bus protocol 是一个二进制协议,因为官方不建议其他应用与redis 节点进行通信,所以没有公开的文档,要查看的话只能去看源码。
Redis cluster 是一个网状的,每一个节点通过tcp与其他每个节点连接。假如n个节点的集群,每个节点有n-1个出的链接,n-1个进的链接。这些链接会一直存活。假如一个节点发送了一个ping,很就没收到pong,但还没到时间把这个节点设为 unreachable,就会通过重连刷新链接。
node 会在cluster bus端口一直接受连接,回复ping,即使这个ping 的node是不可信的。但是其他的包会被丢掉,如果发送者不是cluster 一员。
一个node有两种方式接受其他其他node作为集群一员
这样只要我们把节点加入到一个节点,就会自动被其他节点自动发现。
客户端可以自由的连接任何一个node,如果这个node 不能处理会返回一个MOVED的错误,类似下面这样
描述了key 的hash slot,属于哪个node
client 会维护一个hash slots到IP:port的映射
当收到moved错误的时候,可以通过CLUSTER NODES或者CLUSTER SLOTS去刷新一遍整个client
cluster 支持运行状态下添加和删除节点。添加删除节点抽象:把一部分hash slot从一个节点移动到另一个节点。
所以,动态扩容的核心就是在节点之间移动hash slot,hash slot 又是key的集合。所以reshare 就是把key从一个节点移动到其他节点。
redis 提供如下命令:
前两个指令:ADDSLOTS和DELSLOTS,用于向当前node分配或者移除slots,指令可以接受多个slot值。分配slots的意思是告知指定的master(即此指令需要在某个master节点执行)此后由它接管相应slots的服务;slots分配后,这些信息将会通过gossip发给集群的其他nodes。
ADDSLOTS指令通常在创建一个新的Cluster时使用,一个新的Cluster有多个空的Masters构成,此后管理员需要手动为每个master分配slots,并将16384个slots分配完毕,集群才能正常服务。简而言之,ADDSLOTS只能操作那些尚未分配的(即不被任何nodes持有)slots,我们通常在创建新的集群或者修复一个broken的集群(集群中某些slots因为nodes的永久失效而丢失)时使用。为了避免出错,Redis Cluster提供了一个redis-trib辅助工具,方便我们做这些事情。
DELSLOTS就是将指定的slots删除,前提是这些slots必须在当前node上,被删除的slots处于“未分配”状态(当然其对应的keys数据也被clear),即尚未被任何nodes覆盖,这种情况可能导致集群处于不可用状态,此指令通常用于debug,在实际环境中很少使用。那些被删除的slots,可以通过ADDSLOTS重新分配。
SETSLOT是个很重要的指令,对集群slots进行reshard的最重要手段;它用来将单个slot在两个nodes间迁移。根据slot的操作方式,它有两种状态“MIGRATING”、“IMPORTING”
1)MIGRATING:将slot的状态设置为“MIGRATING”,并迁移到destination-node上,需要注意当前node必须是slot的持有者。在迁移期间,Client的查询操作仍在当前node上执行,如果key不存在,则会向Client反馈“-ASK”重定向信息,此后Client将会把请求重新提交给迁移的目标node。
2)IMPORTING:将slot的状态设置为“IMPORTING”,并将其从source-node迁移到当前node上,前提是source-node必须是slot的持有者。Client交互机制同上。
假如我们有两个节点A、B,其中slot 8在A上,我们希望将8从A迁移到B,可以使用如下方式:
1)在B上:CLUSTER SETSLOT 8 IMPORTING A
2)在A上:CLUSTER SETSLOT 8 MIGRATING B
在迁移期间,集群中其他的nodes的集群信息不会改变,即slot 8仍对应A,即此期间,Client查询仍在A上:
1)如果key在A上存在,则有A执行。
2)否则,将向客户端返回ASK,客户端将请求重定向到B。
这种方式下,新key的创建就不会在A上执行,而是在B上执行,这也就是ASK重定向的原因(迁移之前的keys在A,迁移期间created的keys在B上);当上述SET SLOT执行完毕后,slot的状态也会被自动清除,同时将slot迁移信息传播给其他nodes,至此集群中slot的映射关系将会变更,此后slot 8的数据请求将会直接提交到B上。
动态分片的步骤:
在上文中,我们已经介绍了MOVED重定向,ASK与其非常相似。在resharding期间,为什么不能用MOVED?MOVED意思为hash slots已经永久被另一个node接管、接下来的相应的查询应该与它交互,ASK的意思是当前query暂时与指定的node交互;在迁移期间,slot 8的keys有可能仍在A上,所以Client的请求仍然需要首先经由A,对于A上不存在的,我们才需要到B上进行尝试。迁移期间,Redis Cluster并没有粗暴的将slot 8的请求全部阻塞、直到迁移结束,这种方式尽管不再需要ASK,但是会影响集群的可用性。
1)当Client接收到ASK重定向,它仅仅将当前query重定向到指定的node;此后的请求仍然交付给旧的节点。
2)客户端并不会更新本地的slots映射,仍然保持slot 8与A的映射;直到集群迁移完毕,且遇到MOVED重定向。
一旦slot 8迁移完毕之后(集群的映射信息也已更新),如果Client再次在A上访问slot 8时,将会得到MOVED重定向信息,此后客户端也更新本地的集群映射信息。
可能有些Cluster客户端的实现,不会在内存中保存slots映射关系(即nodes与slots的关系),每次请求都从声明的、已知的nodes中,随机访问一个node,并根据重定向(MOVED)信息来寻找合适的node,这种访问模式,通常是非常低效的。
当然,Client应该尽可能的将slots配置信息缓存在本地,不过配置信息也不需要绝对的实时更新,因为在请求时偶尔出现“重定向”,Client也能兼容此次请求的正确转发,此时再更新slots配置。(所以Client通常不需要间歇性的检测Cluster中配置信息是否已经更新)客户端通常是全量更新slots配置:
遇到MOVED时,客户端仅仅更新特定的slot是不够的,因为集群中的reshard通常会影响到多个slots。客户端通过向任意一个nodes发送“CLUSTER NODES”或者“CLUSTER SLOTS”指令均可以获得当前集群最新的slots映射信息;“CLUSTER SLOTS”指令返回的信息更易于Client解析。
通常情况下,read、write请求都将有持有slots的master节点处理;因为redis的slaves可以支持read操作(前提是application能够容忍stale数据),所以客户端可以使用“READONLY”指令来扩展read请求。
“READONLY”表明其可以访问集群的slaves节点,能够容忍stale数据,而且此次链接不会执行writes操作。当链接设定为readonly模式后,Cluster只有当keys不被slave的master节点持有时才会发送重定向消息(即Client的read请求总是发给slave,只有当此slave的master不持有slots时才会重定向,很好理解):
1)此slave的master节点不持有相应的slots
2)集群重新配置,比如reshard或者slave迁移到了其他master上,此slave本身也不再支持此slot。
集群中的nodes持续的交换ping、pong数据,这两种数据包的结构一样,同样都携带集群的配置信息,唯一不同的就是message中的type字段。
通常,一个node发送ping消息,那么接收者将会反馈pong消息;不过有时候并非如此,比如当集群中添加新的node时,接收者会将pong信息发给其他的nodes,而不是直接反馈给发送者。这样的好处是会将配置尽快的在cluster传播。
通常一个node每秒都会随机向几个nodes发送ping,所以无论集群规模多大,每个nodes发送的ping数据包的总量是恒定的。每个node都确保尽可能半个NODE_TIMEOUT时间内,向那些尚未发送过ping或者未接收到它们的pong消息的nodes发送ping。在NODE_TIMEOUT逾期之前,nodes也会尝试与那些通讯异常的nodes重新建立TCP链接,确保不能仅仅因为当前链接异常而认为它们就是不可达的。
当NODE_TIMEOUT值较小、集群中nodes规模较大时,那么全局交换的信息量也会非常庞大,因为每个node都尽力在半个NODE_TIMEOUT时间内,向其他nodes发送ping。比如有100个nodes,NODE_TIMEOUT为60秒,那么每个node在30秒内向其他99各nodes发送ping,平均每秒3.3个消息,那么整个集群全局就是每秒330个消息。这些消息量,并不会对集群的带宽带来不良问题。
心跳数据包的内容
ping和pong数据包中也包含gossip部分,这部分信息告诉接受者,当前节点持有其他节点的状态,不过它只包含sender已知的随机几个nodes,nodes的数量根据集群规模的大小按比例计算。
gossip部分包含了
集群失效检测就是,当某个master或者slave不能被大多数nodes可达时,用于故障迁移并将合适的slave提升为master。当slave提升未能有效实施时,集群将处于error状态且停止接收Client端查询。
每个node持有其已知nodes的列表包括flags,有2个flag状态:PFAIL和FAIL;PFAIL表示“可能失效”,是一种尚未完全确认的失效状态(即某个节点或者少数masters认为其不可达)。FAIL表示此node已经被集群大多数masters判定为失效(大多数master已认定为不可达,且不可达时间已达到设定值,需要failover)。
nodes的ID、ip+port、flags,那么接收者将根据sender的视图,来判定节点的状态,这对故障检测、节点自动发现非常有用。
当node不可达的时间超过NODE_TIMEOUT,这个节点就被标记为PFAIL(Possible failure),master和slave都可以标记其他节点为PFAIL。所谓不可达,就是当“active ping”(发送ping且能受到pong)尚未成功的时间超过NODE_TIMEOUT,因此我们设定的NODE_TIMEOUT的值应该比网络交互往返的时间延迟要大一些(通常要大的多,以至于交互往返时间可以忽略)。为了避免误判,当一个node在半个NODE_TIMEOUT时间内仍未能pong,那么当前node将会尽力尝试重新建立连接进行重试,以排除pong未能接收
Ⅲ 如何实现高可用的 redis 集群
Redis 因具有丰富的数据结构和超高的性能以及简单的协议,使其能够很好的作为数据库的上游缓存层。但在大规模的 Redis 使用过程中,会受限于多个方面:单机内存有限、带宽压力、单点问题、不能动态扩容等。
基于以上, Redis 集群方案显得尤为重要。通常有 3 个途径:官方 Redis Cluster ;通过 Proxy 分片;客户端分片 (Smart Client) 。以上三种方案各有利弊。
Redis Cluster( 官方 ) :虽然正式版发布已经有一年多的时间,但还缺乏最佳实践;对协议进行了较大修改,导致主流客户端也并非都已支持,部分支持的客户端也没有经过大规模生产环境的验证;无中心化设计使整个系统高度耦合,导致很难对业务进行无痛的升级。
Proxy :现在很多主流的 Redis 集群都会使用 Proxy 方式,例如早已开源的 Codis 。这种方案有很多优点,因为支持原声 redis 协议,所以客户端不需要升级,对业务比较友好。并且升级相对平滑,可以起多个 Proxy 后,逐个进行升级。但是缺点是,因为会多一次跳转,平均会有 30% 左右的性能开销。而且因为原生客户端是无法一次绑定多个 Proxy ,连接的 Proxy 如果挂了还是需要人工参与。除非类似 Smart Client 一样封装原有客户端,支持重连到其他 Proxy ,但这也就带来了客户端分片方式的一些缺点。并且虽然 Proxy 可以使用多个,并且可以动态增加 proxy 增加性能,但是所有客户端都是共用所有 proxy ,那么一些异常的服务有可能影响到其他服务。为每个服务独立搭建 proxy ,也会给部署带来额外的工作。
而我们选择了第三种方案,客户端分片 (Smart Client) 。客户端分片相比 Proxy 拥有更好的性能,及更低的延迟。当然也有缺点,就是升级需要重启客户端,而且我们需要维护多个语言的版本,但我们更爱高性能。
下面我们来介绍一下我们的Redis集群:
概貌:
如图0所示,
我们的 Redis 集群一共由四个角色组成:
Zookeeper :保存所有 redis 集群的实例地址, redis 实例按照约定在特定路径写入自身地址,客户端根据这个约定查找 redis 实例地址,进行读写。
Redis 实例:我们修改了 redis 源码,当 redis 启动或主从切换时,按照约定自动把地址写到 zookeeper 特定路径上。
Sentinel : redis 自带的主从切换工具,我们通过 sentinel 实现集群高可用。
客户端( Smart Client ):客户端通帆拆过约定查找 redis 实例在 ZooKeeper 中拦轿森写入的地址。并且根据集群的 group 数,进行一致性哈希计算,确定 key 唯一落入的 group ,随后对这个 group 的主库进行操作。客户端会在Z ooKeeper 设置监视,当某个 group 的主库发生变化时,Z ooKeeper 会主动通知客户端,客户端会更新对应 group 的最新主库。
我们的Redis 集群是以业务为单位进行划分的,不同业务使用不同集群(即业务和集群是一对一关系)。一个 Redis 集群会由多个 group 组成 ( 一个 group 由一个主从对 redis 实例组成 ) 。即 group 越多,可以部署在更多的机器上,可利用的内存、带宽也会更多。在图0中,这个业务使用的 redis 集群由 2 个 group 组成,每个 group 由一对主从实例组成。
Failover
如图1所示,
当 redis 启动时,会 把自己的 IP:Port 写入到 ZooKeeper 中。其中的 主实例模式启动时会在 /redis/ 业务名 / 组名 永久节点写入简亩自己的 IP:Port (如果节点不存在则创建)。由 主模式 变成 从模式 时,会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节 点,并写入自己的 IP:Port (如果相同节点已经存在,则先删除,再创建)。而从实例 模式 启动时会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并写入自己的 ip:port (如果相同节点已经存在,则先删除,再创建)。由 从模式 变成 主模式 时,先删除 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port 。
ZooKeeper 会一直保存当前有效的 主从实例 IP:Port 信息。至于主从自动切换过程,使用 redis 自带的 sentinel 实现,现设置为超过 30s 主 server 无响应,则由 sentinel 进行主从实例的切换,切换后就会触发以主、从实例通过以上提到的一系列动作,从而完成最终的切换。
而客户端侧通过给定业务名下的所有 groupName 进行一致性哈希计算,确定 key 落入哪个组。 客户端启动时,会从 ZooKeeper 获取指定业务名下所有 group 的 主从 IP:Port ,并在 ZooKeeper 中设置监视(监视的作用是当 ZooKeeper 的节点发生变化时,会主动通知客户端)。若客户端从 Zookeeper 收到节点变化通知,会重新获取最新的 主从 I:Port ,并重新设置监视( ZooKeeper 监视是一次性的)。通过此方法,客户端可以实时获知当前可访问最新的 主从 IP:Port 信息。
因为我们的所有 redis 实例信息都按照约定保存在 ZooKeeper 上,所以不需要针对每个实例部署监控,我们编写了一个可以自动通过 ZooKeeper 获取所有 redis 实例信息,并且监控 cpu 、 qps 、内存、主从延迟、主从切换、连接数等的工具。
发展:
现在 redis 集群在某些业务内存需求超过预期很多后,无法通过动态扩容进行扩展。所以我们正在做动态扩容的支持。原先的客户端我们是通过一致性哈希进行 key 的
路由策略,但这种方式在动态扩容时会略显复杂,所以我们决定采用实现起来相对简单的预分片方式。一致性哈希的好处是可以无限扩容,而预分片则不是。预分片
时我们会在初始化阶段指定一个集群的所有分片数量,这个数量一旦指定就不能再做改变,这个预分片数量就是后续可以扩容到最大的 redis 实例数。假设预分片 128 个 slot ,每个实例 10G 也可以达到 TB 级别的集群,对于未来数据增长很大的集群我们可以预分片 1024 ,基本可以满足所有大容量内存需求了。
原先我们的 redis 集群有四种角色, Smart Client, redis , sentinel , ZooKeeper 。为了支持动态扩容,我们增加了一个角色, redis_cluster_manager (以下简称 manager ),用于管理 redis 集群。主要工作是初始化集群(即预分片),增加实例后负责修改Z ooKeeper 状态,待客户端做好准备后迁移数据到新增实例上。为了尽量减少数据迁移期间对现性能带来的影响,我们每次只会迁移一个分片的数据,待迁移完成,再进行下一个分片的迁移。
如图2所示
相比原先的方案,多了 slots 、M anager Lock 、 clients 、M igrating Clients 节点。
Slots: 所有分片会把自身信息写入到 slots 节点下面。 Manager 在初始化集群时,根据设置的分片数,以及集群下的 group 数,进行预分片操作,把所有分片均匀分配给已有 group 。分片的信息由一个 json 串组成,记录有分片的状态 (stats) ,当前拥有此分片的 group(src) ,需要迁移到的 group(dst) 。分片的状态一共有三种: online 、 pre_migrate 、 migrating 。
Online 指这个分片处于正常状态,这时 dst 是空值,客户端根据 src 的 group 进行读写。
Pre_migrate 是指这个分片被 manager 标记为需要迁移,此时 dst 仍然为空, manager 在等所有 client 都已经准备就绪,因为 ZooKeeper 回掉所有客户端有时间差,所以如果某些 client 没有准备就绪的时候 manager 进行了数据迁移,那么就会有数据丢失。
Migrating 是 manager 确认了所有客户端都已经做好迁移准备后,在 dst 写入此分片需要迁移的目标 group 。待迁移完成,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。
Manager Lock: 因为我们是每次只允许迁移一个 slot ,所以不允许超过一个 manager 操作一个集群。所以 manager 在操作集群前,会在M anager Lock 下注册临时节点,代表这个集群已经有 manager 在操作了,这样其他 manager 想要操作这个集群时就会自动退出。
Clients 和M igrating Clients 是为了让 manager 知道客户端是否已经准备就绪的节点。客户端通过 uid 代表自己,格式是 客户端语言 _ 主机名 _pid 。当集群没有进行迁移,即所有分片都是 online 的时候,客户端会在 clients 下创建 uid 的临时节点。
当某个 slot 从 online 变成 pre_migrate 后,客户端会删除 clients 下的 uid 临时节点,然后在M igrating Clients 创建 uid 临时节点。注意,因为需要保证数据不丢失,从 pre_migrate 到 migrating 期间,这个 slot 是被锁定的,即所有对这个 slot 的读写都会被阻塞。所以 mananger 会最多等待 10s ,确认所有客户端都已经切换到准备就绪状态,如果发现某个客户端一直未准备就绪,那么 mananger 会放弃此次迁移,把 slot 状态由 pre_migrate 改为 online 。如果客户端发现 slot 状态由 pre_migrate 变成 online 了,那么会删除 migrating_clients 下的 uid 节点,在 clients 下重新创建 uid 节点。还需要注意的一点是,有可能一个客户刚启动,并且正在往 clients 下创建 uid 节点,但是因为网络延迟还没创建完成,导致 manager 未确认到这个 client 是否准备就绪,所以 mananger 把 slot 改为 pre_migrate 后会等待 1s 再确认所有客户端是否准备就绪。
如果 Manager 看到 clients 下已经没有客户端的话(都已经准备就绪),会把 slot 状态改为 migrating 。 Slot 变成 migrating 后,锁定也随之解除, manager 会遍历 src group 的数据,把对应 slot 的数据迁移到 dst group 里。客户端在 migrating 期间如果有读写 migrating slot 的 key ,那么客户端会先把这个 key 从 src group 迁移到 dst group ,然后再做读写操作。即这期间客户端性能会有所下降。这也是为什么每次只迁移一个 slot 的原因。这样即使只有 128 个分片的集群,在迁移期间受到性能影响的 key 也只有 1/128 ,是可以接受的。
Manager 发现已经把 slot 已经迁移完毕了,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。客户端也删除 migrating_clients 下的 uid ,在 clients 下创建 uid 节点。
Ⅳ ant-design-vue之form源码解读
form组件理庆唯解关键问题
1.form的form属性怎么和form-item的v-decorator对应起来?
遍历form-item的棚纳slots的子元素,利用vnode的属性vnode.data.directives来获取v-decorator绑定的对象
2.v-decorator 中的属性改变时,怎么联动form表单的数据进行修改?
在渲染(render)form-item的时候,先判断父组件是否存在链差没要绑定的form属性,如果存在,给所有v-decorator的元素绑定默认的change事件(或者其他在rules设置的触发的事件),根据数据变化触发收集数据的函数
3.getFieldDecorator 和 v-decorator 有什么区别?
getFieldDecorator是一个返回vnode的函数,适用于jsx
v-decorator适用于模版
Ⅳ 急急急急!!!求qtopia/qpeapplication.h源代码
/**********************************************************************
** Copyright (C) 2000-2005 Trolltech AS. All rights reserved.
**
** This file is part of the Qtopia Environment.
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 2 of the License, or (at your
** option) any later version.
**
** A of the GNU GPL license version 2 is included in this package as
** LICENSE.GPL.
**
** This program is distributed in the hope that it will be useful, but
** WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU General Public License for more details.
**
** In addition, as a special exception Trolltech gives permission to link
** the code of this program with Qtopia applications righted, developed
** and distributed by Trolltech under the terms of the Qtopia Personal Use
** License Agreement. You must comply with the GNU General Public License
** in all respects for all of the code used other than the applications
** licensed under the Qtopia Personal Use License Agreement. If you modify
** this file, you may extend this exception to your version of the file,
** but you are not obligated to do so. If you do not wish to do so, delete
** this exception statement from your version.
**
** See http://www.trolltech.com/gpl/ for GPL licensing information.
**
** Contact [email protected] if any conditions of this licensing are
** not clear to you.
**
**********************************************************************/
#ifndef __QPE_APPLICATION_H__
#define __QPE_APPLICATION_H__
#include <qtopia/qpeglobal.h>
#include <qapplication.h>
#include <qdialog.h>
#include <qtopia/timestring.h>
#ifdef QTOPIA_TEST
# include "qpetestslave.h"
#endif
class QCopChannel;
class QPEApplicationData;
struct QWSEvent;
struct QWSKeyEvent;
class PluginLibraryManager;
class QTOPIA_EXPORT QPEApplication : public QApplication
{
Q_OBJECT
public:
QPEApplication( int& argc, char **argv, Type=GuiClient );
~QPEApplication();
static QString qpeDir();
static QString documentDir();
void applyStyle();
static int defaultRotation();
static void setDefaultRotation(int r);
static void grabKeyboard();
static void ungrabKeyboard();
enum StylusMode {
LeftOnly,
RightOnHold
// RightOnHoldLeftDelayed, etc.
};
static void setStylusOperation( QWidget*, StylusMode );
static StylusMode stylusOperation( QWidget* );
enum InputMethodHint {
Normal,
AlwaysOff,
AlwaysOn
,
Number,
PhoneNumber,
Words,
Text,
Named,
};
enum screenSaverHint {
Disable = 0,
DisableLightOff = 1,
DisableSuspend = 2,
Enable = 100
};
static void setInputMethodHint( QWidget *, InputMethodHint, const QString& param=QString::null );
static void setInputMethodHint( QWidget *, const QString& named );
static InputMethodHint inputMethodHint( QWidget * );
static QString inputMethodHintParam( QWidget * );
void showMainWidget( QWidget*, bool nomax=FALSE );
void showMainDocumentWidget( QWidget*, bool nomax=FALSE );
static void showDialog( QDialog*, bool nomax=FALSE ); // libqtopia
static int execDialog( QDialog*, bool nomax=FALSE ); // libqtopia
static void setMenuLike( QDialog *, bool ); // libqtopia2
static bool isMenuLike( const QDialog* ); // libqtopia2
static void setTempScreenSaverMode(screenSaverHint); // libqtopia
static void setKeepRunning();
static void setHideWindow(); // internal
bool keepRunning() const;
bool keyboardGrabbed() const;
int exec();
#ifdef QTOPIA_INTERNAL_LOADTRANSLATIONS
static void loadTranslations(const QStringList&);
#endif
#ifdef QTOPIA_INTERNAL_INITAPP
void initApp( int argc, char **argv );
#endif
signals:
void clientMoused();
void timeChanged();
void categoriesChanged();
void clockChanged( bool pm );
void volumeChanged( bool muted );
void appMessage( const QCString& msg, const QByteArray& data);
void weekChanged( bool startOnMonday );
void dateFormatChanged( DateFormat );
void flush();
void reload();
void linkChanged( const QString &linkFile );
private slots:
void systemMessage( const QCString &msg, const QByteArray &data );
void pidMessage( const QCString &msg, const QByteArray &data );
void removeSenderFromStylusDict();
void removeSenderFromIMDict();
void hideOrQuit();
void pluginLibraryManager(PluginLibraryManager**);
void lineEditTextChange(const QString &);
void multiLineEditTextChange();
void removeFromWidgetFlags();
protected:
#if defined(QTOPIA_PHONE) || defined(QTOPIA_TEST) // since not binary compatible
bool notify(QObject*,QEvent*);
#endif
bool qwsEventFilter( QWSEvent * );
void internalSetStyle( const QString &style );
void prepareForTermination(bool willrestart);
virtual void restart();
virtual void shutdown();
bool eventFilter( QObject *, QEvent * );
void timerEvent( QTimerEvent * );
bool raiseAppropriateWindow();
virtual void tryQuit();
private:
void mapToDefaultAction( QWSKeyEvent *ke, int defKey );
void processQCopFile();
#if defined(QTOPIA_INTERNAL_SENDINPUTHINT)
static void sendInputHintFor(QWidget*,QEvent::Type);
#endif
#if defined(Q_WS_QWS) && !defined(QT_NO_COP)
QCopChannel *sysChannel;
QCopChannel *pidChannel;
#endif
QPEApplicationData *d;
bool reserved_sh;
#if defined QTOPIA_TEST
public:
QString appName();
void stopCycleCount() { hasPerfMonitor = FALSE; };
void startCycleCount() { hasPerfMonitor = TRUE; };
QPETestSlave app_slave;
private:
bool hasPerfMonitor;
#endif
};
#ifdef Q_OS_WIN32
#include <stdlib.h>
QTOPIA_EXPORT int setenv(const char* name, const char* value, int overwrite);
QTOPIA_EXPORT void unsetenv(const char *name);
#endif
#endif
// Application main/plugin macro magic
#include <qmap.h>
#include <qtopia/applicationinterface.h>
#include <qmetaobject.h>
typedef QWidget* (*qpeAppCreateFunc)(QWidget*,const char *,Qt::WFlags);
typedef QMap<QString,qpeAppCreateFunc> QPEAppMap;
#define QTOPIA_ADD_APPLICATION(NAME,IMPLEMENTATION) \
static QWidget *create_ ## IMPLEMENTATION( QWidget *p, const char *n, Qt::WFlags f ) { \
return new IMPLEMENTATION(p,n,f); } \
QPEAppMap *qpeAppMap(); \
static QPEAppMap::Iterator mmy_ ## IMPLEMENTATION = qpeAppMap()->insert(NAME,create_ ## IMPLEMENTATION);
#ifdef QTOPIA_NO_MAIN
#define QTOPIA_MAIN
#else
#ifdef QTOPIA_APP_INTERFACE
# define QTOPIA_MAIN \
struct ApplicationImpl : public ApplicationInterface { \
ApplicationImpl() : ref(0) {} \
QRESULT queryInterface( const QUuid &uuid, QUnknownInterface **iface ) { \
*iface = 0; \
if ( uuid == IID_QUnknown ) *iface = this; \
else if ( uuid == IID_QtopiaApplication ) *iface = this; \
else return QS_FALSE; \
(*iface)->addRef(); \
return QS_OK; \
} \
virtual QWidget *createMainWindow( const QString &appName, QWidget *parent, const char *name, Qt::WFlags f ) { \
if ( qpeAppMap()->contains(appName) ) \
return (*qpeAppMap())[appName](parent, name, f); \
return 0; \
} \
virtual QStringList applications() const { \
QStringList list; \
for ( QPEAppMap::Iterator it=qpeAppMap()->begin(); it!=qpeAppMap()->end(); ++it ) \
list += it.key(); \
return list; \
} \
Q_REFCOUNT \
private: \
ulong ref; \
}; \
QPEAppMap *qpeAppMap() { \
static QPEAppMap *am = 0; \
if ( !am ) am = new QPEAppMap(); \
return am; \
} \
Q_EXPORT_INTERFACE() { Q_CREATE_INSTANCE( ApplicationImpl ) }
#else
# define QTOPIA_MAIN \
QPEAppMap *qpeAppMap(); \
int main( int argc, char ** argv ) { \
QPEApplication a( argc, argv ); \
QWidget *mw = 0; \
\
QString executableName(argv[0]); \
executableName = executableName.right(executableName.length() \
- executableName.findRev('/') - 1); \
\
if ( qpeAppMap()->contains(executableName) ) \
mw = (*qpeAppMap())[executableName](0,0,0); \
else if ( qpeAppMap()->count() ) \
mw = qpeAppMap()->begin().data()(0,0,0); \
if ( mw ) { \
if ( mw->metaObject()->slotNames(true).contains("setDocument(const QString&)") ) \
a.showMainDocumentWidget( mw ); \
else \
a.showMainWidget( mw ); \
int rv = a.exec(); \
delete mw; \
return rv; \
} else { \
return -1; \
} \
} \
QPEAppMap *qpeAppMap() { \
static QPEAppMap *am = 0; \
if ( !am ) am = new QPEAppMap(); \
return am; \
} \
#endif
#endif
Ⅵ 玩转Redis的高可用(主从、哨兵、集群)
所谓的高可用,也叫 HA(High Availability),是分布式系统架构设计中必须考虑的因素之一,它是保证系统SLA的重要指标。Redis 高可用的主要有三种模式: 主从模式 , 哨兵模式和集群模式 。
Redis 提供了 Redis 提供了复制(replication)功能,当一台 redis 数据库中的数据发生了变化,这个变化会被自动地同步到其答键念他的 redis 机器上去。
Redis 多机器部署时,这些机器节点会被分成两类,一类是主节点(master 节点),一类是从节点(slave 节点)。一般 主节点可以进行读、写操作 ,而 从节点只能进行读操作 。一个主节点可以有多个从节点,但是一个从节点只会有一个主节点,也就是所谓的 一主多从结构 。
· 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
· Master 是以非阻塞的方式为主 Slaves 提供服务。所以在 Master-Slave 同步期间,客户端仍然可以提交查询或修改请求;
· Slave 同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis 则返回同步之前的数据。
· Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP 才能恢复;
· 主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后面还会引入数据不一致的问题,降低了系统的可用性;
· Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;
· Redis 的主节点和亮尘从节点中的数据是一样的,降低的内存的可用性
实际生产中,我们优先考虑哨兵模式。这种模式下,master 宕机,哨兵会自动选举 master 并将其他的 slave 指向新的 master。
在主从模式下,redis 同时提供了哨兵命令 redis-sentinel ,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵进程向所有的 redis 机器人发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。一般为了便于决策选举,使用 奇数个哨兵 。多个哨兵构成一个哨兵集群,哨兵直接也会相互通信,检查哨兵是否正常运行,同时发现 master 战机哨兵之间会进行决策选举新的 master
哨兵模式的作用:
· 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器;
· 然而一个哨兵进程对 Redis 服务器进行监控,也可能会清困出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多种哨兵模式。
哨兵很像 kafka 集群中的 zookeeper 的功能。
· 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
· 主从可以自动切换,系统更健壮,可用性更高。
· 具有主从模式的缺点,每台机器上的数据是一样的,内存的可用性较低。
· Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
Redis 集群模式本身没有使用一致性 hash 算法,而是使用 slots 插槽 。
Redis 哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容;每个节点都会通过集群总线(cluster bus),与其他的节点进行通信。 通讯时使用特殊的端口号,即对外服务端口号加 10000。例如如果某个 node 的端口号是 6379,那么它与其它 nodes 通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。
对客户端来说,整个 cluster 被看做是一个整体,客户端可以连接任意一个 node 进行操作,就像操作单一 Redis 实例一样, 当客户端操作的时候 key 没有分配到该 node 上时,Redis 会返回转向指令,指向正确的 node,这有点儿像浏览器页面的 302 redirect 跳转。
根据官方推荐,集群部署至少要 3 台以上的 master 节点,最好使用 3 主 3 从六个节点的模式。
在 Redis 的每一个节点上,都有这么两个东西, 一个是插槽(slot),它的的取值范围是:0-16383, 可以从上面 redis-trib.rb 执行的结果看到这 16383 个 slot 在三个 master 上的分布。还有一个就是 cluster,可以理解为是一个集群管理的插件,类似的哨兵。
当我们的存取的 Key 到达的时候,Redis 会根据 crc16 的算法对计算后得出一个结果,然后把结果和 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
为了保证高可用, redis-cluster 集群引入了主从模式 ,一个主节点对应一个或者多个从节点。当其它主节点 ping 主节点 master 1 时,如果半数以上的主节点与 master 1 通信超时,那么认为 master 1 宕机了,就会启用 master 1 的从节点 slave 1,将 slave 1 变成主节点继续提供服务。
如果 master 1 和它的从节点 slave 1 都宕机了,整个集群就会进入 fail 状态,因为集群的 slot 映射不完整。 如果集群超过半数以上的 master 挂掉,无论是否有 slave,集群都会进入 fail 状态。
redis-cluster 采用去中心化的思想 ,没有中心节点的说法,客户端与 Redis 节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
对 redis 集群的扩容就是向集群中添加机器,缩容就是从集群中删除机器,并重新将 16383 个 slots 分配到集群中的节点上(数据迁移)。
扩缩容也是使用集群管理工具 redis-tri.rb。
扩容时,先使用 redis-tri.rb add-node 将新的机器加到集群中,这是新机器虽然已经在集群中了,但是没有分配 slots,依然是不起做用的。在使用 redis-tri.rb reshard 进行分片重哈希(数据迁移),将旧节点上的 slots 分配到新节点上后,新节点才能起作用。
缩容时,先要使用 redis-tri.rb reshard 移除的机器上的 slots,然后使用 redis-tri.rb add-del 移除机器。
采用去中心化思想,数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布;
可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除;
高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升;
降低运维成本,提高系统的扩展性和可用性。
1.Redis Cluster 是无中心节点的集群架构,依靠 Goss 协议(谣言传播)协同自动化修复集群的状态。但 GosSIp 有消息延时和消息冗余的问题,在集群节点数量过多的时候,节点之间需要不断进行 PING/PANG 通讯,不必须要的流量占用了大量的网络资源。虽然 Reds4.0 对此进行了优化,但这个问题仍然存在。
2.数据迁移问题
Redis Cluster 可以进行节点的动态扩容缩容,这一过程,在目前实现中,还处于半自动状态,需要人工介入。在扩缩容的时候,需要进行数据迁移。
而 Redis 为了保证迁移的一致性,迁移所有操作都是同步操作 ,执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会接触发集群内的故障转移,造成不必要的切换。
主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。
哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。
集群模式:数据量比较大,QPS 要求较高的时候使用。 Redis Cluster 是 Redis 3.0 以后才正式推出,时间较晚,目前能证明在大规模生产环境下成功的案例还不是很多,需要时间检验。
Ⅶ Mono源码阅读-GC造成内存泄露问题
本文主要记录Mono源码中会因为GC的问题,造成Unity游戏不可避免的都会存在一定得内存泄露问题的底层原因,涉及到Mono源码中GC机制的逻辑。
要出现这种内存泄露,必须先准备一块任意的内存块:(无任何外部引用,理论上应该会在用完后被GC,但在该BUG下会错误的泄露,不被GC掉)
NOTE:大小任意,越大越容易被泄露。
一块struct结构的数组:(struct内必须有一个类似指针的值类型, 如int,和另一个引用类型,如string)
NOTE:大数陪小任意,数组内的元素越多越容易触发泄露。例如 HashSet<String> 内部使用了该数据格式。
通过在GC中打点,和使用GDB调用GC过程,以便观察所有对象的分配和GC的过程发现:buffer对象错误的被slots对象引用,导致buffer对象无法被正常GC,造成内存泄露。
首先对于mono/il2cpp的Boehm GC库而言, mono/il2cpp的对象在分配内存的时候,会有几种类型:
而在本例中:slots的分配是用NORMAL类型,buffer对象的分配是用PTRFREE类型。
因此在做GC的禅配时候,对于slots对象,GC会扫描该对象的内存区间,查找其内部的指针地址,即从0xde45f000到0xde468c50地址按照指针对齐的方式查找指针地址:
例贺毕指如:0xde45f000 0xde464d44 0xde464f40 0xde46513c ....
其中出现了 0xde464f40 这个地址的值刚好为:0xbe82f000(即Slot结构体内hashCode的值),而GC会错误的将该int型数值当做指针,而该指针刚好又指向了一块GC托管的内存块,即buffer对象,因此GC认为该buffer对象被slots对象内部引用了,buffer对象也被GC标记,不会被释放。
该问题的关键在于,GC将slot结构体内的hashcode这个int值错误的当做的指针,而该int值刚好又指向了另一个托管的对象,因此GC错认为了两个对象存在引用关系,而造成内存泄露。
最小化Demo:
将struct Slot修改为class Slot,则可修复内存泄露问题,因为class对象的内存分配时TYPED类型。
因为Mono的GC的设计问题,Unity游戏中几乎不可避免的都会随着时间出现内存泄露问题,因为例如HashSet这种数据结构内部都会出现该问题。但我们可以做的事情,依然是内存使用的两大真理(特别是虚拟机类型的语言):
这样做,不能完全避免Mono的底层GC问题,但是它可以让这种内存泄露的变得更加平缓。
Ⅷ 【Redis】Redis Cluster-集群数据迁移
Redis通过对KEY计算hash,将KEY映射到slot,集群中每个节点负责一部分slot的方式管理数据,slot最大个数为16384。
在集群节点对应的结构体变量clusterNode中可以看到slots数组,数组的大小为CLUSTER_SLOTS除以8,CLUSTER_SLOTS的值是16384:
clusterState
clusterNode里面保存了节点相关的信息,集群数据迁移信息并未保存在clusterNode中,而是使用了clusterState结构体来保存:
clusterState与clusterNode的关系
在手动进行数据迁移时,需要执行以下步骤:
在进行数据迁移之前,首先在需要迁入的目标节点使用 SETSLOT 命令标记要将SLOT从哪个节点迁入到当前节点:
然后在源节点也就是slot所在节点使用 MIGRATING 命令标记将数据迁出到哪个节点:
比如slot1当前在node1中,需要将slot1迁出到node2,那么首先在nodd2上执行 IMPORTING 命令,标记slot准备从node1迁到当前节点node2中:
然后在node1中执行 MIGRATING 命令标记slot1需要迁移到node2:
clusterCommand
SETSLOT 命令的处理在clusterCommand函数(cluster.c文件中)中:
在标记完迁入、迁出节点后,就可以使用 CLUSTER GETKEYSINSLOT 命令获取待迁出的KEY:
<slot>:哈希槽的值
<count>:迁出KEY的数量
getkeysinslot 命令的处理也在clusterCommand函数中,处理逻辑如下:
完成上两步之后,接下来需要在源节点中执行 MIGRATE 命令进行数据迁移, MIGRATE 既支持单个KEY的迁移,也支持多个KEY的迁移,语法如下:
migrateCommand
MIGRATE 命令对应的处理函数在migrateCommand中(cluster.c文件中),处理逻辑如下:
createDumpPayload
createDumpPayload函数在cluster.c文件中:
restoreCommand
目标节点收到迁移的数据的处理逻辑在restoreCommand中(cluster.c文件中):
数据迁移的最后一步, 需要使用 CLUSTER SETSLOT 命令,在源节点和目标节点执行以下命令,标记slot最终所属的节点,并清除第一步中标记的迁移信息 :
<slot>:哈希槽
<node>:哈希槽最终所在节点id
clusterCommand
CLUSTER SETSLOT <slot> NODE <node> 命令的处理依旧在 clusterCommand 函数中,处理逻辑如下:
总结
参考
极客时间 - Redis源码剖析与实战(蒋德钧)
Redis版本:redis-6.2.5
Ⅸ redis架构模式(4) Codis
codis是在redis官方集群方案redis-cluster发布之前便已被业界广泛使用的redis集群解决方案。
codis server :这是进行了二次开发的 Redis 实例,其中增加了额外的数据结构,支持
数据迁移操作,主要负责处理具体的数据读写请求。
codis proxy :接收客户端请求,并把请求转发给 codis server。
Zookeeper 集群 :保存集群元数据,例如数据位置信息和 codis proxy 信息。
codis dashboard 和 codis fe :共同组成了集群管理工具。其中,codis dashboard 负
责执行集群管理工作,包括增删 codis server、codis proxy 和进行数据迁移。
而 codis fe 负责提供dashboard的Web 操作界面,便于我们直接在 Web 界面上进行集群管理
codis proxy本身支持 Redis 的 RESP 交互协议,所以只需和proxy建立连接,就相当于和
整个codis集群建立连接,codis proxy 接收到请求,就会查询请求数据和 codis server 的
映射关系,并把请求转发给相应的 codis server 进行处理。当 codis server 处理完请求后,
会把结果返回给codis proxy,proxy 再把数据返回给客户端。
codis集群喊模兆共有1024个slot,编号从0-1023,可以自定义或者自动均匀分配到各个server上,
使用 CRC32 算法计算数据 key 的哈希值然后对1024取模,得到slot编号,然后通过slot
和server的映射关系请求到具体实例。
Slot 和 codis server 的映射关系称为数据路由表,我们在 codis dashboard 上分配好路由表后,
dashboard 会把路由表发送给 codis proxy,同时,dashboard 也会把路由表保存在
Zookeeper 中。codis-proxy 会把路由表缓存在本地,当它接收到客户端请求后,直接查询
本地的路由表进而完成请求的转发。
跟redis cluster的区别是,codis的把映射关系存在zookeeper和proxy中,redis cluster
是通过各个实例相互通信进而存在各个实例本地,当实例较多时,可能会受到网络状况
影响而且整个集群的网络资源消耗相较之下也比较大。
codis按照slot粒度进行数据迁移,比如从serverA迁移至serverB,会从A要迁移的slot中
随机选择一个数据,发送给B,A得到确认消息后,删除本地数据。重复这个过程直到
迁移完毕。
迁移方式有两种,一种是同步迁移,一种是异步迁移。
同郑租步迁移时,源server是阻塞的,由于迁移过程中需要码型数据的序列化、网络传输、删除
等等操作,如果是bigkey则会阻塞较长时间。
所以通常采用异步迁移。异步迁移模式下,源server发送完迁移数据之后,就可以继续接受
处理请求,等到目标server返回ack之后,再执行删除操作,这个过程中,为了防止数据
不一致的情况,被迁移的数据是只读模式。
对于bigkey的数据,异步迁移采用拆分指令的方式,对bigkey中的每个元素用一条指令
进行迁移,而不是等待整个数据序列化完毕再迁移,进而避免了需要序列化大量数据而
阻塞的风险,因为整个bigkey的数据是被拆分的,如果迁移期间server宕机,便会破坏
迁移的原子性,所以codis对bigkey的数据设置了一个过期时间,如果原子性遭到破坏,
目标server的数据就会过期后删除以保证迁移的原子性。可以通过异步迁移命令
SLOTSMGRTTAGSLOT-ASYNC 的参数numkeys 设置每次迁移的 key 数量,以提升
bigkey异步迁移的速度。
最后分享一下我的一些架构设计心得,通过redis cluster和codis的slot设计,把原本
数百上千万的key和实例的映射抽象成slot和实例的映射,大大减少了映射量,进而把
slot和key的映射分担到各个实例中,这种粒度的抽象,值得借鉴,另外codis与redis cluser
不同的是,codis通过zk作为第三方储存系统,保存了集群实例、状态和slot分配等信息,
这样避免了实例间的大量心跳来同步集群状态进而减少实例间的网络通信量,对于实现
大规模的集群是有益的设计,网络带宽可以集中在客户端请求上面,需要注意的是需要保证
zk的通信带宽,毕竟集群的信息同步都交给了zk