【万字长文】带你搞懂Redis中的所有知识点

目录

Redis为何这么快

数据结构

string(字符串)

list(列表)

hash(字典)

set(集合)

zset(有序集合)

持久化

RDB(Redis DataBase)

AOF(Append Only File)

过期策略

设置过期时间

3种过期策略

内存淘汰策略

8种淘汰策略

置换策略

集群

主从模式

哨兵模式

全量复制

增量复制

Cluster集群


Redis为何这么快

  • 基于内存
  • 单线程
    • Redis 的数据结构并不全是简单的 Key-Value,还有 List,Hash 等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
    • kv数据操作中有大量数据读写操作,使用单线程可以避免锁的竞争及线程切换的开销
  • IO多路复用
    • 为了让单线程(进程)的服务端应用同时处理多个客户端的事件,Redis采用了IO多路复用机制。使用了linux的epoll,非阻塞的多路复用网络模型。
  • 数据结构(如 SDS、Hash以及跳表等)。

数据结构

  • string(字符串)

    • Redis最简单也是使用最广泛的数据结构,它的内部是一个字符数组。
    • 存储方式
      • int
        • Redis中规定假如存储的是「整数型值」,比如set num 123这样的类型,就会使用 int的存储方式进行存储,在redisObject的「ptr属性」中就会保存该值。
      • SDS
        • 假如存储的「字符串是一个字符串值并且长度大于32个字节」就会使用SDS(simple dynamic string)方式进行存储,并且encoding设置为raw;若是「字符串长度小于等于32个字节」就会将encoding改为embstr来保存字符串。
        • SDS称为「简单动态字符串」,对于SDS中的定义在Redis的源码中有的三个属性int len、int free、char buf[]
          • len保存了字符串的长度
          • free表示buf数组中未使用的字节数量
          • buf数组则是保存字符串的每一个字符元素
        • 空间预分配原则是:「当修改字符串后的长度len小于1MB,就会预分配和len一样长度的空间,即len=free;若是len大于1MB,free分配的空间大小就为1MB」。这样是为了「减少连续的执行字符串增长带来内存重新分配的次数」。
    • 优点
      • 常数复杂度获取字符串长度
        • 由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。
        • 杜绝缓冲区溢出
        • 减少修改字符串的内存重新分配次数
      • 常用命令
        • set [key] [value] 给指定key设置值(set 可覆盖老的值)
        • get [key] 获取指定key 的值
        • incrby [key] [number] 使用incrby命令对整数值 进行增加 number
        • incr [key] 如果value为整数 可用 incr命令每次自增1
        • setnx [key] [value] 如果key不存在则set 创建,否则返回0
        • setex [key] [time] [value] 等价于 set + expire 命令组合
        • expire [key] [time] 给指定key 设置过期时间 单位秒
        • mget [key1] [key2] ...... 批量取key
        • mset [key1] [value1] [key2] [value2] ...... 批量存键值对
        • exists [key] 判断是否存在指定key
        • del [key] 删除指定key
  • list(列表)

    • Redis中的列表在3.2之前的版本是使用ziplist和linkedlist进行实现的。在3.2之后的版本就是引入了quicklist。
    • zipList
      • 压缩列表(ziplist)是一组连续内存块组成的顺序的数据结构,主体结构为数组。压缩列表能够节省空间,压缩列表中使用多个节点来存储数据。
      • 压缩列表是列表键和哈希键底层实现的原理之一,「压缩列表并不是以某种压缩算法进行压缩存储数据,而是它表示一组连续的内存空间的使用,节省空间」
      • 一个ziplist可以包含任意多个entry,而每一个entry又可以保存一个字节数组或者一个整数值,ziplist不存储指向上一个节点和下一个节点的指针,存储的是上一个节点的长度和当前节点的长度,牺牲了部分读写性能来换取高效的内存利用率,是一种时间换空间的思想,ziplist适用于字段个数少和字段值少的场景。
      • 内存结构
        • zlbytes:4个字节的大小,记录压缩列表占用内存的字节数。
        • zltail:4个字节大小,记录表尾节点距离起始地址的偏移量,用于快速定位到尾节点的地址。因为支持双向遍历所以才有这个字段,用于定位到最后一个元素,然后倒着遍历
        • zllen:2个字节的大小,记录压缩列表中的节点数。
        • entry:表示列表中的每一个节点。
        • zlend:表示压缩列表的特殊结束符号 '0xFF'。【万字长文】带你搞懂Redis中的所有知识点_第1张图片

 quickList

  • linkedlist和quicklist的底层实现是采用链表进行实现,在c语言中并没有内置的链表这种数据结构,Redis实现了自己的链表结构。
    • 每一个节点都有指向前一个节点和后一个节点的指针。
    • 头节点和尾节点的prev和next指针指向为null,所以链表是无环的。
    • 将多个 ziplist 使用双向指针串联起来,这样既能满足快速插入、删除的特性,又节省了一部分存储空间。
  • 而当数据量较大时,Redis 列表就会是用 quicklist(快速链表)存储元素。Redis 之所以采用两种方法相结合的方式来存储元素。这是因为单独使用普通链表存储元素时,所需的空间较大,会造成存储空间的浪费。因此采用了链表和压缩列表相结合的方式。
  • 链表有自己长度的信息,获取长度的时间复杂度为O(1)。
  • 应用场景
    • 栈、队列
  • 常用操作
    • rpush [key] [value1] [value2] ...... 链表右侧插入
    • rpop [key] 移除右侧列表头元素,并返回该元素
    • ltrim [key] [start_index] [end_index] 保留区间内的元素,其他元素删除(时间复杂度为 O(n))
    • lrange [key] [start_index] [end_index] 获取list 区间内的所有元素 (时间复杂度为 O(n))
    • lindex [key] [index] 获取list指定下标的元素 (需要遍历,时间复杂度为O(n))
    • lrem [key] [count] [value] 删除列表中与value相等的元素,count是删除的个数。 count>0 表示从左侧开始查找,删除count个元素,count
    • llen [key] 返回该列表的元素个数
    • lpop [key] 移除左侧列表头元素,并返回该元素
  • hash(字典)

    • Hash对象的实现方式有两种分别是ziplist、hash表,其中hash表的key和value只能为String类型。
    • 满足以下条件时使用ziplist存储
      • 所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节)
      • 哈希对象保存的键值对数量小于 512 个。
    • 一个哈希对象超过配置的阈值(键和值的长度有>64byte,键值对个数>512 个)时,会转换成哈希表(hashtable)。
    • hashtable(dict)
      • 在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。
      • Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上,值得注意的是在 Redis 的 Hash 中 value 只能是字符串
    • 渐进式rehash
      • 一种扩容操作,当哈希表中的元素数量增加到一定程度时,为了保持哈希表的性能,Redis会自动进行扩容操作。
      • redis会为哈希表分配一个新的空间,新空间的大小是当前哈希表大小的两倍。
      • Redis会将原哈希表中的所有元素逐步迁移到新的哈希表中。在迁移过程中,Redis会使用一个后台线程来完成迁移工作,而不会阻塞对哈希表的读写操作。
      • 当所有元素都迁移到新的哈希表后,Redis会将读写操作全部切换到新的哈希表上,原哈希表会被释放。
      • 在迁移过程中,当有读写操作访问哈希表时,Redis会同时访问原哈希表和新哈希表,确保数据的一致性。新的写操作会同时写入原哈希表和新哈希表,而读操作会先在新哈希表中查找,如果找不到再在原哈希表中查找。
    • 应用场景
      • 购物车:hset [key] [field] [value] 命令, 可以实现以用户Id,商品Id为field,商品数量为value,恰好构成了购物车的3个要素。
      • 存储对象:hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
    • 常用操作
      • hset [key] [field] [value] 新建字段信息
      • hget [key] [field] 获取字段信息
      • hincrby [key] [field] [number] 对字段值增加number
      • hincr [key] [field] 对字段值自增
      • hmset [key] [field1] [value1] [field2] [value2] ...... 批量创建
      • hgetall [key] 获取指定key 字典里的所有字段和值 (字段信息过多,会导致慢查询 慎用:亲身经历 曾经用过这个这个指令导致线上服务故障)
      • hlen [key] 保存的字段个数
      • hdel [key] [field] 删除字段
  • set(集合)

    • Redis中的set和Java中的HashSet有些类似,它内部的键值对是无序的、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
    • Set的底层实现是hash表和intset
      • inset也叫做整数集合,用于保存整数值的数据结构类型,它可以保存int16_t、int32_t 或者int64_t 的整数值。
    • 应用场景
      • 好友、关注、粉丝、感兴趣的人集合:
      • sinter命令可以获得A和B两个用户的共同好友;
      • sismember命令可以判断A是否是B的好友;
      • scard命令可以获取好友数量;
      • 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
      • 首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。
      • 小程序抽奖活动 ,因为有去重功能,可以保证同一个用户不会中奖两次。
    • 常用操作
      • sadd [key] [value] 向指定key的set中添加元素
      • smembers [key] 获取指定key 集合中的所有元素
      • srem [key] [value] 删除指定元素
      • spop [key] 弹出一个元素
      • scard [key] 获取集合的长度
      • sismember [key] [value] 判断集合中是否存在某个value
  • zset(有序集合)

    • Set是有序集合,从上面的图中可以看到ZSet的底层实现是ziplist和skiplist
    • skiplist
      • skiplist也叫做「跳跃表」本质上是一个有序链表,根据插入的元素分值进行从小到大的排序,Zset默认使用的是压缩列表结构,当数据量到达一定程度时会自动转换为跳表结构(可根据配置修改)。缺点:浪费一定的空间,用空间换时间。【万字长文】带你搞懂Redis中的所有知识点_第2张图片
      • skiplist与平衡树、哈希表的比较
        • skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
        • 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
        • 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
        • 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
        • 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
        • 从算法实现难度上来比较,skiplist比平衡树要简单得多。
      • 应用场景
        • 可以用做排行榜,但是和list不同的是zset它能够实现动态的排序,例如: 可以用来存储粉丝列表,value 值是粉丝的用户 ID,score 是关注时间,我们可以对粉丝列表按关注时间进行排序。
        • 还可以用来存储学生的成绩, value 值是学生的 ID, score 是他的考试成绩。 我们对成绩按分数进行排序就可以得到他的名次。
      • 常用操作
        • zadd [key] [score] [value] 向指定key的集合中增加元素
        • zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
        • zscore [key] [value] 获取元素的score
        • zrem [key] [value] 删除元素
        • zrangebyscore [key] [score1] [score2] 输出score范围内的元素列表
        • zrank [key] [value] 获取元素再集合中的排名
        • zcard [key] 获取集合列表的元素个数
        • zrevrange [key] [start_index] [end_index] 获取范围内的元素列表 ,按score排序 逆序输出

持久化

  • RDB(Redis DataBase)

    • RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式。也就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。
    • 触发方式
      • save
        • 同步备份,期间不能执行其他操作
      • bgsave
        • 后台异步,可以正常响应
      • 自动配置
        • 表示900 秒内如果至少有 1 个 key 的值变化,则保存
        • 300 秒内如果至少有 10 个 key 的值变化,则保存
        • 60 秒内如果至少有 10000 个 key 的值变化,则保存save 60 10000
    • 过期key处理
      • 从内存数据库持久化数据到RDB文件
      • 持久化key之前,会检查是否过期,过期的key不进入RDB文件
      • 从RDB文件恢复数据到内存数据库
        • 数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
    • 优点
      • RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
      • 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
      • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
    •  缺点
      • fork操作会阻塞主线程,⽽且主线程的内存越⼤,阻塞时间越⻓
      • 频繁将全量数据写⼊磁盘,会给磁盘带来很⼤压⼒
      • RDB 快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑 。且在快照持久化期间修改的数据不会被保存,可能丢失数据。
  • AOF(Append Only File)

    • 以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
    • 文件重写
      • AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用命令bgrewriteaof
      • 将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条子进程来将文件重写。
      • 由于fork操作运用写时复制技术, 子进程只能共享fork操作时的内存数据。
      • 由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部分新数据, 防止新AOF文件生成期间丢失这部分数据。此时父进程的所有读写都在AOF重写缓冲区中进行
      • 新AOF文件写入完成后,父进程把AOF重写缓冲区的数据写入到新的AOF文件。
      • Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发【万字长文】带你搞懂Redis中的所有知识点_第3张图片​​
    • 触发机制
      • 每修改同步always:同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好
      • 每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
      • 不同no:从不同步
    • 过期key处理
      • 从内存数据库持久化数据到AOF文件:
      • 当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
    • AOF重写
      • 重写时,会先判断key是否过期,已过期的key不会重写到aof文件 
    • 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
  • 优点
    • AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
    • AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
    • AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
    • AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flush all命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
  • 缺点
    • 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
    • AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
    • 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
  • RDB和AOF一般都会混合使用
    • 每隔一段时间RDB全量备份一次(一周、一月)..
    • 在这个间隔内使用AOF增量备份数据

过期策略

  • 设置过期时间

    • expire key time (以秒为单位),这是最常用的方式
    • setex(String key, int seconds, String value),字符串独有的方式
    • 除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠 expire 方法来设置时间,如果没有设置时间,那缓存就是永不过期。 如果设置了过期时间,使用 persist key 让缓存永不过期。
  • 3种过期策略

    • 定时删除
      • 在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
      • 优点
        • 保证内存被尽快释放
      • 缺点
        • 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
        • 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生)性能影响严重
          • 基本没人用
    • 定期删除
      • 优点
        • 每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
        • 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
        • 定期删除过期key--处理"惰性删除"的缺点
      • 缺点
        • 时间周期不好设定
    • 惰性删除
      • 优点
        • 删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
        • key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
      • 缺点
        • 若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
        • 可以通过配置redis.conf的max memory最大值,当已用内存超过max memory限定时,就会触发主动清理策略。
        • 过期 key 是不会写入 RDB 和 AOF 文件,同时数据恢复时也会做过期验证。

内存淘汰策略

  • Redis在内存空间不足的时候,为了保证命中率,就会选择一定的数据淘汰策略
  • 8种淘汰策略

    • volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
    • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
    • volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
    • allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
    • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
    • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
    • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
    • no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
  • 置换策略

    • 为了避免频繁的触发淘汰策略,每次会淘汰掉一批数据,淘汰的数据的大小其实是和置换的大小来确定的,如果置换的数据量大,淘汰的肯定也多。
    • 客户端执行一条新命令,导致数据库需要增加数据(比如set key value)Redis会检查内存使用,如果内存使用超过max memory,就会按照置换策略删除一些key

集群

  • 主从模式

    • 和 MySQL需要主从复制的原因一样,Redis虽然读写速度非常快,但是也会产生性能瓶颈,特别是在读压力上,为了分担压力,Redis 支持主从复制。Redis 的主从结构一主一从,一主多从或级联结构,复制类型可以根据是否是全量而分为全量同步和增量同步。
    • 同步策略
      • 主从同步刚连接的时候进行全量同步;全量同步结束后开始增量同步。如果有需要,slave在任何时候都可以发起全量同步,其主要策略就是无论如何首先会尝试进行增量同步,如果步成功,则会要求slave进行全量同步,之后再进行增量同步。
      • 注意:如果多个slave同时断线需要重启的时候,因为只要slave启动,就会和master建立连接发送SYNC请求和主机全量同步,如果多个同时发送SYNC请求,可能导致master IO突增而发送宕机。
    • 级联模式
      • 可以给从节点在建立从节点,相当于主->从->从结构,对于最下层的从节点,它的上层就是它的主节点,这样可以分担主节点的写压力
    • 特点
      • 采用异步复制,可以一主多从;
      • 主从复制对于master来说是非阻塞的,也就是说slave在进行主从复制的过程中,master依然可以处理请求;
      • 主从复制对于slave来说也是非阻塞的,也就是说slave在进行主从复制的过程中也可以接受外界的查询请求,只不过这时候返回的数据不一定是正确的。为了避免这种情况发生,可以在slave的配置文件中配置,在同步过程中阻止查询;
      • 每个slave可以接受来自其他slave的连接;
      • 主从复制提高了Redis服务的扩展性,避免单节点问题,另外也为数据备份冗余提供了一种解决方案;
      • 为了降低主redis服务器写磁盘压力带来的开销,可以配置让主redis不在将数据持久化到磁盘,而是通过连接让一个配置的从redis服务器及时的将相关数据持久化到磁盘,不过这样会存在一个问题,就是主redis服务器一旦重启,因为主redis服务器数据为空,这时候通过主从同步可能导致从redis服务器上的数据也被清空;
  • 哨兵模式

    • 在主从复制实现之后,如果想对 master 进行监控,Redis 提供了一种哨兵机制,哨兵的含义就是监控 Redis 系统的运行状态,若哨兵集群中的多数哨兵节点都报告某一master没响应,系统才认为该master彻底死亡。会通过投票机制,从 slave 中选举出新的 master 以保证集群正常运行。还可以启用多个哨兵进行监控以保证集群足够稳健,这种情况下,哨兵不仅监控主从服务,哨兵之间也会相互监控。
    • 监控
      • 哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行
      • 监控主库:主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程
      • 监控从库:从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”
      • 选主
        • 首先是从主服务器的从服务器中选出一个从服务器作为新的主服务器。选点的依据依次是
          • 过滤:
            • 当前在线状态
            • 之前的网络连接状态
          • 选优:
            • 从库优先级:可以通过 slave-priority 配置项配置
            • 从库复制进度
            • 从库ID号
        • 让从库执行 replicaof 命令,和新主库建立连接,并进行数据复制
        • 把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上
      • 缺点
        • 当主服务器宕机后,从服务器切换成主服务器的那段时间,服务是不可用的。
        • 主从服务器的数据要经常进行主从复制,这样会造成性能下降。
  • 全量复制

    • Redis全量复制一般发生在slave的初始阶段,这时slave需要将master上的数据都复制一份,具体步骤如下:
      • slave连接master,发送SYNC命令;
      • master接到SYNC命令后,执行BGSAVE命令生产RDB文件并使用缓冲区记录此后执行的所有写命令;
      • master的BGSAVE执行完成后,向所有的slave发送快照文件,并在发送过程中继续记录执行的写命令;
      • slave收到快照后,丢弃所有的旧数据,载入收到的数据;
      • master快照发送完成后就会开始向slave发送缓冲区的写命令;
      • slave完成对快照的载入,并开始接受命令请求,执行来自master缓冲区的写命令;
      • slave完成上面的数据初始化后就可以开始接受用户的读请求了。
  • 增量复制

    • 增量复制实际上就是在slave初始化完成后开始正常工作时master发生写操作同步到slave的过程。增量复制的过程主要是master每执行一个写命令就会向slave发送相同的写命令,slave接受并执行写命令,从而保持主从一致。
  • Cluster集群

    • 从复制架构中是读写分离的,我们可以通过增加slave节点来扩展主从的读并发能力,但是写能力和存储能力是无法进行扩展的,就只能是master节点能够承载的上限。所以,当你只需要存储4G的数据时候的,基于主从复制和基于Sentinel的高可用架构是完全够用的。但是如果当你面临的是海量的数据的时候呢?16G、64G、256G甚至1T呢?现在互联网的业务里面,如果你的体量足够大,我觉得是肯定会面临缓存海量缓存数据的场景的。
    • redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster
    • 特点
      • Redis集群的分片特征在于将空间拆分为16384个槽位,某一个节点负责其中一些槽位;
        • 这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。
      • Redis集群提供一定程度的可用性,可以在某个节点宕机或者不可达的情况继续处理命令;
      • Redis集群不存在中心节点或代理节点,集群的其中一个最重要的设计目标是达到线性可扩展性;
    • 分布算法
      • 通过CRC16算法计算key的值跟16384取模,取模之后得到的值就是对应的槽位,然后根据对应的槽位找到master节点
    • hash槽让node的增加和移除很简单,增加一个master,就将其他master的hash槽移动部分过去,减少一个master,就将它的hash槽移动到其他master上去移动hash槽的成本是非常低的
    • 故障发现转移
      • 当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis 集群内节点通过 ping/pong 消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的。
      • Redis 集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障。当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播,通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
      • 故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
    • 一致性hash
      • 一致性哈希则是对2^32取模,也就是值的范围在[0, 2^32 -1]。一致性哈希将其范围抽象成了一个圆环,使用CRC16算法计算出来的哈希值会落到圆环上的某个地方。
      • 然后我们的Redis实例也分布在圆环上,我们在圆环上按照顺时针的顺序找到第一个Redis实例,这样就完成了对key的节点分配。
      • 假设我们有A、B、C三个Redis实例按照如图所示的位置分布在圆环上,此时计算出来的hash值,取模之后位置落在了位置D,那么我们按照顺时针的顺序,就能够找到我们这个key应该分配的Redis实例B。同理如果我们计算出来位置在E,那么对应选择的Redis的实例就是A。
      • 即使这个时候Redis实例B挂了,也不会影响到实例A和C的缓存。
    • 虚拟节点机制
      • 在圆环中,增加了对应节点的虚拟节点,然后完成了虚拟节点到真实节点的映射。假设现在计算得出了位置D,那么按照顺时针的顺序,我们找到的第一个节点就是C #1,最终数据实际还是会落在节点C上。
      • 在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
    • 节点选举
      • 集群配置纪元是一个自增计数器,它的初始值为0;
      • 当集群里的某个节点开始一次故障转移时,集群配置纪元的值会被增加1
      • 对于每个配置纪元,集群里的每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
      • 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播消息:要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
      • 如果一个主节点具有投票权,并且这个主节点尚未投票跟其它从节点,那么主节点将要求投票的从节点返回一条ACK消息,表示支持该从节点成为新的主节点。
      • 每个主节点只有一次投票机会,所有有N个主节点的话,那么具有大于N/2+1张支持票的从节点只有一个。
      • 如果在一个配置纪元里没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
    • 缺点:
      • Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅 JedisCluster 相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
      • 数据通过异步复制,不保证数据的强一致性。
      • 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
      • Slave 在集群中充当“冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 Slave 资源的利用率。
      • Key 批量操作限制,如使用 mset、mget 目前只支持具有相同 slot 值的 Key 执行批量操作。对于映射为不同 slot 值的 Key 由于 Keys 不支持跨 slot 查询,所以执行 mset、mget、sunion 等操作支持不友好。
      • Key 事务操作支持有限,只支持多 key 在同一节点上的事务操作,当多个 Key 分布于不同的节点上时无法使用事务功能。
      • Key 作为数据分区的最小粒度,不能将一个很大的键值对象如 hash、list 等映射到不同的节点。
      • 不支持多数据库空间,单机下的 redis 可以支持到 16 个数据库,集群模式下只能使用 1 个数据库空间,即 db 0。
      • 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
      • 避免产生 hot-key,导致主库节点成为系统的短板。
      • 避免产生 big-key,导致网卡撑爆、慢查询等。重试时间应该大于 cluster-node-time 时间。
      • Redis Cluster 不建议使用 pipeline 和 multi-keys 操作,减少 max redirect 产生的场景。
    • 优点:
      • 无中心架构;数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布
      • 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除
      • 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升;降低运维成本,提高系统的扩展性和可用性。

你可能感兴趣的:(Redis,redis,数据库,缓存,数据结构,skiplist,hash-index)