1、几种基础数据类型
String | 计数器,用户信息(id)映射,唯一性(例如用户资格判断),bitmap |
Hash | 常见场景:存储对象的属性信息(用户资料) |
List | 常见场景:评论存储,消息队列 |
Set | 常见场景:资格判断(例如用户奖励领取判断),数据去重等 |
ZSet(Sorted Set) | 常见场景:排行榜,延时队列 |
Redis快的原因:https://zhuanlan.zhihu.com/p/57089960
还有IO多路复用:https://zhuanlan.zhihu.com/p/83398714
Redis分布式锁:redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作
分类 | 方案 | 实现原理 | 优点 | 缺点 |
基于数据库 | 基于mysql 表唯一索引 | 1.表增加唯一索引 2.加锁:执行insert语句,若报错,则表明加锁失败 3.解锁:执行delete语句 | 完全利用DB现有能力,实现简单 | 1.锁无超时自动失效机制,有死锁风险 2.不支持锁重入,不支持阻塞等待 3.操作数据库开销大,性能不高 |
基于MongoDB findAndModify原子操作 | 1.加锁:执行findAndModify原子命令查找document,若不存在则新增 2.解锁:删除document | 实现也很容易,较基于MySQL唯一索引的方案,性能要好很多 | 1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员 2.锁无超时自动失效机制 | |
基于分布式协调系统 | 基于ZooKeeper | 1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点 2.解锁:删除节点 | 1.由zk保障系统高可用 2.Curator框架已原生支持系列分布式锁命令,使用简单 | 需单独维护一套zk集群,维保成本高 |
基于缓存 | 基于redis命令 | 1. 加锁:执行setnx,若成功再执行expire添加过期时间 2. 解锁:执行delete命令 | 实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好 | 1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入 |
基于redis Lua脚本能力 | 1. 加锁:执行SET lock_name random_value EX seconds NX 命令 2. 解锁:执行Lua脚本,释放锁时验证random_value -- ARGV[1]为random_value, KEYS[1]为lock_name if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end | 同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。 | 不支持锁重入,不支持阻塞等待 |
Redis 集群方案:
1、http://blog.ipalfish.com/?p=576
2、动态迁移过程 https://blog.csdn.net/productshop/article/details/50387075 https://zhuanlan.zhihu.com/p/53044266 https://github.com/tiancaiamao/go.blog/blob/master/content/codis%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB%E6%9C%9F%E9%97%B4%E7%9A%84%E4%B8%80%E8%87%B4%E6%80%A7.md (准确的)
a. Dashboard向所有codis-proxy发出 pre_migrate slot_1 to group 2,将slot_1状态标记为”pre_migrate”。
b. 处在pre_migrate的slot_1是只读的。
c. 如果收到了所有proxy回复收到迁移指令,那么将slot_1的状态修改为”migrating”,并将slot_1的server group改为group2。
d. (谁?)不断发送SLOTSMGRT命令给group1的redis ,直到slot_1所有的key迁移完成。
e. 如果迁移期间,有请求 slot_1 的 key 数据。先在group1上强行执行一次 MIGRATE key,将数据迁移过去,然后再将请求转发到group2上。
a.刚开始,Codis Dashboard 会先统计哪些 slot 是需要迁移的。然后开始针对每个 slot 进行迁移,从代码上来看同时迁移的 slot 数量是可以配置的,一般应该不会同时迁移过多 slot。
b.迁移 slot 数据时,会先将 slot 的最新信息通知给各个 Codis Proxy,信息包括 slot 的 id、原 Redis Group ID(我们叫它 From),新 Redis Group ID(我们叫它 Target),通过这些信息告知 Proxy 这个 slot 正在迁移数据。
c.当且只有当所有的 Proxy 都成功更新了 Slot 信息之后,Codis Dashboard 才会真正开始迁移数据。为了迁移数据 Codis Dashboard 会发送 SLOTSMGRTTAGSLOT 命令,让 Target 节点从 From 节点迁移 slot 的 kv 数据
d.Redis Group 完成数据迁移之后,Codis Dashboard 会将新的 slot 信息发送给各个 Proxy,告知 Proxy 迁移工作已经完成,可以按照普通的请求代理逻辑来处理这个 slot 的请求了。
Redis
Redis不是强一致性性的
- 1 Redis持久化方式
- RDB持久化方式会在一个特定的时间点保存那个时间点的一个数据快照(全量的);
- AOF(AOF Append Only File,仅追加数据)持久化方式则会记录每一个服务器收到的写操作。
- 两种持久化方案对比:https://www.jianshu.com/p/2b5793039531
- AOF 丢失数据少,但是恢复慢;RDB丢失数据多,但是恢复快。丢失数据多少,取决于他们持久化的频率,因为最差情况丢失的就是从上次持久化之后的数据。因为AOF只是追加,因此频率较高,但是RDB保存的是镜像,因此频率较低。
- Redis的持久化是可以禁用的,就是说你可以让数据的生命周期只存在于服务器的运行时间里。两种方式的持久化是可以同时存在的,但是当Redis重启时,AOF文件会被优先用于重建数据。使用的建议是Master关闭持久化,只有slave进行持久化。slave建议链式连接。
- 2 Redis的数据安全问题
- Redis在持久化的时候,不像Mysql和RocketMq那样,保证相关数据持久化到磁盘后,才返回成功。而是先将结果放到内存,隔一段时间再将数据持久化到磁盘,这样的处理,保证了其高效性。但是如果此时机器宕机,那么就会丢失上次持久化之后到宕机期间变更的数据。从这个角度看,持久化的频率越高,数据的安全性就越高。AOF默认1s就行一次持久化,而RDB可以根据时间,也可以根据指定时间范围内写指令次数决定持久化时间点,普遍会大于1s。如果数据量较大RDB方式下的持久化操作会对redis有较大影响。(AOF缓存区除了记录写入指令外,还记录最近一次同步磁盘时间。主线程在向AOF缓冲区写入数据的时候,会判断距离上次刷盘时间是否超过2s,如果超过,就阻塞写命令。因此AOF在everysec刷盘策略下可能会丢失2s的数据)
- 3 Redis的服务可用性问题
Redis基础数据类型
Redis是一个基于内存同时具备数据持久化能力的高性能、低时延的KV数据库。key只能是String字符串结构,value的数据结构可以是String,List,HashMap,set(集合),Zset(有序集合)。
压缩列表(zipList)
- 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构;具体结构相对比较复杂,略。与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。
- 压缩列表不仅用于实现列表,也用于实现哈希、有序列表;使用非常广泛。
字符串
- 字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串。字符串长度不能超过512MB。
- Redis的字符串没有直接使用C语言的字符串,而是用了简单动态字符串SDS(Simple Dynamic String)。SDS相较于C字符串的优点:
- 获取字符串长度:记录了字符串长度,可以快速获取字符串长度。
- 缓冲区溢出:因为记录了字符串长度和可用长度,可以自动分配内存,避免溢出。
- 内存预分配策略和惰性释放内存,有效减少了内存重新分配的次数。
- 存储二进制数据:因为C字符串用空字符作为结尾,因此不能存储二进制数据。
列表
- Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。
- 列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。
- 双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度
- 只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于512个;列表中所有字符串对象都不足64字节。如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。
哈希
- 哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有序结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,在本文后面当使用“内层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为Key-Value数据库所使用的数据结构。
- 内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种;Redis的外层的哈希则只使用了hashtable。压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。
- 只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于512个;哈希中所有键值对的键和值字符串长度都小于64字节。如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。
SET
- 集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的(这个顺序是指插入顺序),因此不能通过索引来操作元素;集合中的元素不能有重复。
- 集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。
- 只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于512个;集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。
ZSET(有序集合)
- 有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。
- 有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。具体结构相对比较复杂,略。
- 只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。
Redis渐进式ReHash
- 如果hash负载较高,需要进行rehash。重新申请更大的存储空间(大于当前键值对数量的最近一个2的指数大小),将之前的数据拷贝到新申请的hash表中。为满足ReHash,在基础的数据结构层面提供了以下两点支持:
- Redis hash字典,包含了两个hash表,ht[0]是正常使用的,ht[1]只在扩容时使用。
- rehashidx用来标识是否在进行rehash,以及rehash进度。rehashidx = -1,表示没有在进行ReHash。
- Redis为什么采用渐进式ReHash,在添加键值对后,判读是否需要进行扩容,如果需要扩容,则进入扩容流程。如果像JAVA的hashMap扩容一样,一次性将所有数据迁移完成,操作才返回,如果hash表比较大,耗时会较长,那么对应的插入操作会阻塞较长时间。Redis是单线程的,较长的阻塞对性能影响较大,因此采用ReHash,将迁移分散到后续的操作中,每次操作只迁移一个hash桶。
- 问题在于,如果对应hashmap访问频率较低,导致在很长的一段时间都无法完成迁移,也就是ht[0]和ht[1]在较长的时间共存,如果这样的情况较多,会占用较多的内存,可能会造成redis数据的驱逐。因此可以开启一个线程定时协助迁移。
ReHash触发的条件
- 为了让哈希表的负载因子(load factor)维持在一个合理的范围内,会使用rehash(重新散列)操作对哈希表进行相应的扩展或收缩。负载因子 = 键值对数量/hash表大小
- Redis服务器目前没有在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。(引起rehash普遍的场景)
- Redis服务器目前在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
- 当哈希表的负载因子小于0.1时,对哈希表执行收缩操作。
ReHash的过程
- Redis rehash采取渐进式rehash,在对hash表执行增删改查操作之前,先根据rehashidx判断是否在进行rehash(rehashidx 正常情况下为 -1),如果在进行rehash,则首先对rehashidx对应的hash桶进行迁移,再执行正常的增删改查。
- 此处有一个优化点,根据rehashidx进行桶迁移的时候,可能出现对应桶为空的情况,需要处理下一个,如果某个区间连续很多桶均为空,会在这个地方浪费较多时间,导致较长的阻塞,因此限制最大桶为空的情况,如果超过限制,桶还是为空,则直接结束本次桶迁移,继续后续的操作。
- 如果迁移完成,将ht[0]指针指向ht[1]指向的数组,并将ht[1]置为null,rehashidx置为-1。
ReHash过程中操作的处理
- ReHash过程中,存在两个hash表,查询、删除、修改操作均先访问ht[0],如果不存在,访问ht[1];新增加入到ht[1]中。
Redis的数据淘汰策略
- Redis提供6种数据淘汰策略:
- 1、volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- 2、volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- 3、volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- 4、allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- 5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- 6、no-enviction(驱逐):禁止驱逐数据
- Redis 确定驱逐某个键值对后,会删除这个数据,并将这个数据变更消息发布到本地(AOF 持久化)和从机(主从连接)。
Redis 为什么快(https://www.zhihu.com/search?type=content&q=redis 单线程)
- 完全基于内存
- 采用单线程
- 多线程主要是为了有效利用CUP资源,如果将一个操作分成CPU执行和IO执行两部分,多线程适用于IO操作比较的场景,因为一个线程IO阻塞的时候,可以通过执行其他线程,有效利用CPU。而Redis是基于内存的存储,CPU操作密集的应用,因此多线程对其性能提升有限,反而需要解决同步和竞争,增加复杂度和资源消耗。
- 单线程指的是处理网络请求的是单线程,一个redis实例肯定包含多个线程
- 使用多路I/O复用模型,非阻塞IO。
- (确认是否也实现了零拷贝)
- 自定义的数据结构
缓存穿透、缓存击穿、缓存雪崩
- 缓存作用于业务请求和数据库中间,流程是先查询缓存,如果缓存没有再去查数据库,如果数据库查到的话,更新缓存,下次就会直接查询缓存了。缓存穿透、缓存击穿、缓存雪崩描述的都是缓存无效,直接查询数据库所引发的问题,区别在于场景。
- 缓存穿透
- 描述:
- 如果查询的数据在数据库里面不存在,那么就无法有效更新缓存,因此每次查询都会直接穿透缓存,直接查询数据库。
- 解决方案:
- 如果不存在,也写入一个无效缓存,但是注意加上过期时间,避免数据库已经存在,但是缓存里面还是一个无效值(这不仅是用这种方式解决缓存穿透存在的问题,所有缓存形式都面临这个问题)。
- 布隆过滤器,将数据库中存在的全量key存储在布隆过滤器里面。这种方案是用内存将全量信息都存储下来了,可以前置到redis之前,过滤掉所有无效的key请求。采用这种方案的时候可能会存在一个疑问?使用布隆过滤器要维护布隆过滤器和数据库的一致性,如果这样,干嘛不直接维护Redis和数据库的一致性。这里要理解布隆过滤器和redis之间作用的差异性,布隆过滤器仅存储key,因此仅关注key的删除和新增,不关注value,而key仅会新增和少量删除,因此不用考虑key的淘汰策略,可以认为一个key在布隆过滤器添加了,就会在一次运行期间或是永久(需要持久化)有效;redis存储的是key和对应的value,value往往是易变的,因此一般要设置过期策略,以保持和数据库数据的一致性。
- 布隆过滤器过滤器的原理是,用一个二进制数组作为基础的数据结构,通过一系列随机映射函数,将一个key映射到部分二进制位上。布隆过滤器存在的问题是,二进制位各个key是可以复用的,可能存在误匹配的问题。例如:key1 是b1 b2, key2 是b3 b4,而key3是b2 b3,如果布隆过滤器中存在了key1、key2,即便不存在key3也会被误认为存在。
- 描述:
- 缓存击穿
- 描述:
- 缓存击穿是指某一key失效,并发的请求先查询redis,发小无有效数据,都直接请求数据库。
- 解决方案:
- 如果发现缓存失效,先阻塞获取一个互斥锁,然后再查询缓存,然后再查询数据库,更新缓存。(这样处理可以使得只有一个线程会最终请求到数据库,而其他均会读取Redis)
- 描述:
- 缓存雪崩
- 描述:
- 同一时刻,大量的redis key失效,直接请求数据库。(除了key统一过期外,redis节点宕机,也会发生此类问题)。
- 解决方案:
- 针对key统一过期:
- 设置随机的过期时间
- 用有限的并发请求数据库,例如用单线程的线程池处理请求
- 限制访问数据库的流量
- 针对Redis节点失效:
- 事前:使用本地缓存,保证Redis失效时,系统也是可用的(请求-本地缓存-Redis-数据库);使用集群分散热点数据;使用哨兵+主从结构,实现故障探测和转移,保证高可用;使用持久化保证数据安全,以便快速恢复。
- 用有限的并发请求数据库,例如用单线程的线程池处理请求
- 限制访问数据库的流量
- 针对key统一过期:
- 描述:
缓存更新
正常的业务场景:请求->redis->mysql ,请求先resdis,如果redis存在,直接返回;如果redis不存在,然后查询数据库,如果查询数据库成功,则更新redis。
Redis作为业务层和数据库之间的缓存,除了缓存穿透、缓存击穿、缓存雪崩等问题外,还有一个是如何保持redis和数据库的数据的一致性,特别是数据更新的情况下。
最佳实践是先更新数据库,然后删除缓存。除此之外,还有:先删除缓存,再更新DB;先更新DB,再更新缓存。各个方案存在的问题如下:
- 先删除缓存,再更新DB
1. 线程B先于线程A在t1时刻淘汰缓存
2. 线程A在t2时刻读缓存未命中,在t3时刻读DB(此时线程B还未开始更新DB,故从DB读到的数据为旧数据),并将从DB读到的旧数据写入缓存
3. 线程B在t4时刻完成更新DB操作(此时DB里的数据是最新的,但缓存里面却是旧的,只要缓存未被清除,后续读请求取到的全是缓存里的旧数据)
备注:更新DB相比读DB是一个更重的操作,在删除缓存和更新DB的时间间隙,很可能发生读缓存未命中后再读DB这一事件。故采用“先删除缓存,再更新数据库”导致缓存脏数据并非小概率事件!
- 先更新DB,再更新缓存
1. 线程A在t1时刻更新DB
2. 线程B在t2时刻更新DB,t3时刻更新缓存
3. 线程A在t4时刻更新缓存,此次更新会覆盖掉了线程B在线程A之后写入的缓存,导致后续读请求(如线程C)读到的是缓存里的脏数据,且可能持续脏下去
- 正确示范:先更新DB,再删除缓存
1. 线程A在t1时刻更新DB
2. 线程B在t2时刻更新DB,t3时刻删除缓存
3. 线程A在t4时刻删除缓存,后续读请求(如线程C)读缓存肯定读不到,会从数据库读取到最新数据,因此不会出现读取到脏数据的情形
存在的问题1:
1. 线程B在线程A更新DB但还未执行删除缓存操作的间隙,读取缓存并直接返回 —— 此次请求返回的数据为脏数据
2. 线程C在线程A执行完删除缓存操作后,读取数据,并正确更新了缓存 —— 此次返回的是最新数据,并且在这之后,其他线程读到的也将是最新数据
备注:删除缓存是一个很轻量级操作,执行很快。故更新DB成功和删除缓存之间的时间间隙应该极短,实际场景中这种极短时间的数据不一致是可容忍的。可以通过对缓存加锁,消除这种短暂的不一致,代价是有短暂的不可用。
存在的问题2:
1. 线程A在t1时刻读取缓存未命中,在t2读DB
2. 线程B在t3时刻更新DB,并在t4完成删除缓存操作
3. 线程A在t5时刻将读取DB成功返回的数据写入缓存, 此时写入缓存的数据即为脏数据,并且持续脏下去。
备注:上述场景出现的概率非常低,需要下列三个条件同时满足才可能发生:
1. 前提条件: 发生缓存读取未命中,且同时并发着一个写缓存操作
2. 读线程要比写线程早进入数据库操作
3. 读线程要比写线程晚执行缓存操作
Codis
架构
- Codis fe:集群管理界面,多个Codis集群可以共用一个Codis FE,通过配置文件管理后端的codis-dashboard。
- Codis Dashboard(配置中心):集群管理工具,支持Codis Proxy、Codis Server管理以及slot分配、迁移等操作。对于一个Codis集群,Dashboard最多部署一个。
- Storage(存储):集群状态的外部存储,目前支持ZooKeeper、Etcd、Fs三种。存储slot和codis server的映射关系,以及一些集群状态,例如迁移状态。
- Codis Proxy(接入层,无状态的):实现Redis协议,除部分不支持跨slot的命令外,表现与原生Redis无差异。Codis Proxy的路由信息不是从存储获取的,而是从Dashboard获取的。
- Codis Group(由codis server组成):一个Redis主从集群,充当存储节点。新加key到slot的映射结构,以及slot有关的操作及数据迁移指令。和普通的Redis服务器相比,最大的不同是,Codis 对 Redis 进行了扩展,支持 Redis服务器之间同步数据。
- redis-sentinel(集群哨兵):用于实现redis集群的故障探测、故障转移。
动态扩缩容
- 扩缩容最为关键的一点就是slot的迁移
- 在使用自动扩缩容功能时,dashboard 会计算出需要迁移的slot。
- dashboard 将slot的最新信息通知给各个Codis Proxy,信息包括slot的id、原 Redis Group ID(我们叫它From),新Redis Group ID(我们叫它 Target),通过这些信息告知Proxy这个slot正在迁移数据。如果Codis Proxy收到对应的信息,就会将对应的slot状态改为pre_migrate,处在pre_migrate状态的slot读写都是阻塞的(部分说只禁读,后面会讨论只禁读存在的问题)。
- 如果dashboard收到所有的Codis Proxy的回复,确保所有Codis Proxy都收到了slot的迁移指令。就会进入migrate阶段,主要做两件事情:a.向所有的Codis Proxy下发migrate指令,成功收到指令的Codis Proxy会将对应slot的状态标记为migrate;b.向相关codis server下发迁移指令,将From节点待迁移slot下的所有key都迁移到Target节点。如果Codis Proxy收到了对处在migrate状态的slot的key的请求,处理分为两步:向From下发对应keyslotmigratekey(key迁移指令),确保key转移到了Target节点上;将请求转发给Target节点,按正常的redis命令处理。
- Redis Group完成数据迁移之后,Codis Dashboard 会将新的slot信息发送给各个 Proxy,告知 Proxy 迁移工作已经完成,可以按照普通的请求代理逻辑来处理这个slot的请求了。
备注: slot迁移过程中的,两阶段提交思考
- 1、直接进入migrate状态,存在什么问题?
- 如果部分Codis Proxy没有收到,或是延迟收到,就会存在不一致的情况。主要表现在,集群A中的一个key已经迁移到了B(无论是Codis Proxy主导的迁移,还是codis server主导的迁移),但是没有进入migrate状态的Codis Proxy依旧会将这个key路由到集群A,集群A会判断没有这个key,这与实际情况不符。
- 2、进入pre_migrate状态,不禁止读存在什么问题?
- 部分文章认为slot进入pre_migrate状态,仅禁止对应的写请求就好。实际上读也会存在问题,如果部分Codis Proxy进入了migrate状态,但是部分Proxy还处在pre_migrate状态,那么读也会出现不一致。因为pre_migrate状态依旧会走老路径,但是key已经被迁移走了。
数据可靠&高可用&容灾&故障转移
- Redis 的数据可靠性是通过下面几点实现的:
- 单机数据高可靠(持久化)
- 远程数据热备(主从复制)
- 定期冷备归档(RDB数据)
- 高可用&容灾&故障转移主要是依赖codis的集群方案:
- 哨兵集群(Sentinel)保证redis高可用。由一个或多个Sentinel实例组成的Sentinel系统,可以监视任意多个主服务器,以及这些主服务器属下的所有的从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由主服务器代替已下线的主服务器继续处理命令请求。同时将其他从服务器修改为新的主服务的从节点。
- 哨兵集群(Sentinel)保证redis高可用,核心两点是怎么进行故障探测,怎么进行故障转移(剔除无法服务的机器,寻找可替代的从节点)。
migrate(MIGRATE 127.0.0.1 6380 key2 0[过期时间,0为永不过期] 1000[迁移过程超时时间])
- 序列化(相当于dump) (source)
- 网络传输 (source -> target)
- 反序列化(相当于restore)(target)
- 返回(target -> source)
- 删除之前的key(source)
Redis 主从复制
- 主从复制是从节点发起的,所有操作都是从节点主导的
1.连接建立
- 从节点保存主节点IP和端口
- 建立socket连接,并使用ping命令进行连接测试
- 从节点向主节点发送监听端口
2.数据同步
- 连接建立后,从节点查看是否执行过数据同步(未执行过slaveof命令,刚执行过slaveof no one)。如果之前没有执行过数据同步,则发起全量复制。如果已经同步过,执行部分复制。
- 主节点收到从节点的全量复制命令后,执行bgsave命令,在后台生成RDB文件,并将之后的写命令放置到复制缓冲区里面。
- 主节点收到从节点的部分复制命令后,查看从节点的传输的复制偏移量(offset)是否在复制积压缓冲区内,如果在,将之后的内容传输给从节点执行部分复制,如果不在说明复制积压缓冲区已经淘汰了部分数据,执行全部复制。
3.全量复制
- 从节点发起全量复制以及从节点发起部分复制,但是主节点发现不满足部分复制,均会执行全量复制。有两种场景不满足部分复制,会进入全量复制:
- 每个redis节点都有一个随机ID(runid),初次复制的时候,主节点会将自己的runid发送给从节点,从节点进行部分复制的时候,主节点发现传过来的runid不是自己的,只能进入全量复制。
- 如果从节点的复制偏移不在复制积压缓冲区内,只能进入全量复制。
- 全量复制流程:
- 从节点发起全量复制或是不满足条件的部分复制
- 主节点收到从节点发起的全量复制,或是收到从节点发起的部分复制但是发现复制偏移量不在复制积压缓冲区内,执行bgsave命令(fork一个子进程执行,异步执行,不影响主节点响应命令),生成当前的内存快照RDB文件,并将之后的写命令放到复制缓冲区内。
- 主节点bgsave命令执行完成后,将RDB文件发送给从节点
- 从节点收到RDB文件后,先清除自己的数据,然后将RDB文件载入,此时从节点和主节点执行bgsave命令时保持一致
- 主节点将缓冲区数据发送给从节点,从节点执行这些命令
- 全量复制是一个非常重的操作,主要的时间消耗:
- bgsave时间
- 主节点将RDB文件通过网络传输给从节点
- 从节点清空数据
- 从节点载入RDB文件
- 可能的AOF重写时间
4.部分复制
- 由于全量复制在主节点数据量较大时效率太低,因此Redis2.8开始提供部分复制,用于处理网络中断时的数据同步。
- 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
5.命令传播
- 在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。心跳机制对于主从复制的超时判断、数据安全等有作用。
- 在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个节点对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。
待学习点:续租概念