成长之路-Redis

Redis

[TOC]

基础篇

数据类型

  • String (字符串)
  • List (列表)
  • Hash (字典)
  • Set (集合)
  • ZSet (有序集合)

String(字符串)
  1. String 类型是二进制安全的,意思是 Redis 的 string 可以包含任何数据,比如图片或者序列化的对象;
  2. 一个 redis 中字符串 value 最多可以是 512M;
  3. 特殊的Key-Value操作即INCR/DECR,可以利用Redis自动帮助我们对一个Key对应的Value进行加减;

List(列表)
  1. 简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表;

Hash(字典)
  1. hash 是一个键值对集合,是一个 string 类型的 key和 value 的映射表,key 还是key,但是value是一个键值对(key-value)。类比于 Java里面Map> 集合;
  2. 哈希对象的编码可以是 zipList 或者 hashTable;
    1. 当使用ziplist,也就是压缩列表作为底层实现时,新增的键值对是保存到压缩列表的表尾;
    2. 当同时满足下面两个条件时,使用zipList(压缩列表)编码:
      1. 列表保存元素个数小于512个;
      2. 每个元素长度小于64字节;
    3. 不能满足这两个条件的时候使用 hashTable 编码。第一个条件可以通过配置文件中的 set-max-intset-entries 进行修改;


      profile哈希对象的压缩对象底层实现

      HashTable编码的hash profile对象.png

Set(集合)
  1. 集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合;
  2. 意集合和列表的区别:
    1. 集合中的元素是无序的,因此不能通过索引来操作元素;
    2. 集合中的元素不能有重复;
  3. 集合对象的编码可以是 intSet 或者 hashTable;
    1. intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中;
    2. hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null;
intset编码的numbers集合对象

hashtable编码的fruits的集合对象

ZSet(有序集合)
  1. 有序集合对象是有序的;
  2. 与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据;
  3. 有序集合的编码可以是 ziplist 或者 skiplist:
    1. ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。


      有序集合元素在压缩列表中按分值从大到小排列
    2. skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:

    3. 字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。

    4. 两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费;

typedef struct zset {
     //跳跃表
     zskiplist *zsl;
     //字典
     dict *dice;
} zset;    
  1. 其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合;

  2. 编码转换,当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

    1. 保存的元素数量小于128;
    2. 保存的所有元素长度都小于64字节;
    3. 不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改;

五大数据类型的应用场景

  1. 对于string 数据类型,因为string 类型是二进制安全的,可以用来存放图片,视频等内容,另外由于Redis的高性能读写功能,而string类型的value也可以是数字,可以用作计数器(INCR,DECR),比如分布式环境中统计系统的在线人数,秒杀等;
  2. 对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息;
  3. 对于 list 数据类型,可以实现简单的消息队列,另外可以利用lrange命令,做基于redis的分页功能;
  4. 对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能;
  5. 对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取TopN操作等;

进阶篇

内存回收和内存共享

内存回收
  • Redis 的每个对象都是由 redisObject 结构表示;
typedef struct redisObject{
     //类型
     unsigned type:4;
     //编码
     unsigned encoding:4;
     //指向底层数据结构的指针
     void *ptr;
     //引用计数
     int refcount;
     //记录最后一次被程序访问的时间
     unsigned lru:22;
 } robj
  • 创建一个新对象,属性 refcount 初始化为1;
  • C 语言不具备自动回收内存功能,那么该如何回收内存呢?于是 Redis自己构建了一个内存回收机制,通过在 redisObject 结构中的 refcount 属性实现。这个属性会随着对象的使用状态而不断变化;
    1. 创建一个新对象,属性 refcount 初始化为1;
    2. 对象被一个新程序使用,属性refcount加1;
    3. 对象不再被一个程序使用,属性refcount减1;
    4. 当对象的引用计数值变为 0 时,对象所占用的内存就会被释放;


      修改对象引用计数的API
内存共享
  • refcount 属性除了能实现内存回收以外,还能用于内存共享;

  • 比如通过如下命令 set k1 100,创建一个键为 k1,值为100的字符串对象,接着通过如下命令 set k2 100 ,创建一个键为 k2,值为100 的字符串对象

    1. 将数据库键的值指针指向一个现有值的对象;
    2. 将被共享的值对象引用refcount 加 1:
    3. 被共享的字符串对象
  • Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2);

  • 共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用);

一致性Hash

Redis效率


Redis 为什么这么快
  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  3. 响应队列:Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从write_fds里面移出来。等到队列有数据了,再将描述符放进去。避免select系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU;
  4. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  5. 使用多路I/O复用模型,非阻塞IO;
  6. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
    自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

单进程多线程模型
  1. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
多路 I/O 复用模型
  • Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的;


    多路IO复用
  • redis的io模型主要是基于epoll实现的,不过它也提供了 select和kqueue的实现,默认采用epoll;

    1. epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max 察看;
    2. 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll;
    3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了
  • 多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作;

  • 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量;

  • 概括:

    1. 大量请求进入Redis,(读,写操作);
    2. 分析大量的请求并分类(按读写类型);
    3. 监察多个流的 I/O 事件的能力(准备读,准备写,准备连接);
    4. 把所有的请求根据读写类型分发至各个I/O流线程;
    5. 批量请求,并等待所有请求完成后返回;
    6. 每个用户请求获取对应的请求返回数据;
    7. 将所有的用户请求汇聚在一起并根据请求类型所需I/O连接分类,一个I/Oq请求批量批量执行多个用户请求(减少网络 IO 的时间消耗),达到多线程的效果;
  • 扩展

    • Redis 和 I/O 多路复用
    • Redis IO多路复用技术以及epoll实现原理

数据存储结构

部署方式

Redis 部署选型方案


单机模式
单机模式
  • 单机版三个问题
    1. 内存容量有限;
    2. 处理能力有限;
    3. 无法高可用;

多机版模式
多机模式
  • 特性

    1. 复制(Replication);
    2. 哨兵 (Sentinel);
    3. 集群 (Cluster);
  • Redis多机版部署特性功能

    1. 复制:扩展系统对于读的能力;
    2. 哨兵:为服务器提供高可用特性,减少故障停机出现;
    3. 集群:扩展内存容量,增加机器,提高性能读写能力和存储以及提高可用特性;

复制模式
复制模式

Redis的复制(replication)功能允许用户根据一个Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品,则为从服务器(slave),master负责读写,slave一般只负责读。只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步给从服务器,从而一直保证主从服务器的数据相同。

  • 特点:
    1. master/slave 角色;
    2. master/slave 数据相同;
    3. 降低master读的压力,在转交从库
  • 问题
    1. 无法保证高可用;
    2. 没有解决master写的压力

哨兵模式
哨兵模式

Redis sentinel 是一个分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移,其中三个特性:
监控(Monitoring):Sentinel 会不断的检查你的主服务器和从服务器是否运作正常;
提醒(Notification):当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知;
自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel会开始一次自动故障前一操作;

  • 特点:
    1. 保证高可用;
    2. 监控各个节点;
    3. 自动故障迁移
  • 缺点:
    1. 主从模式,切换需要时间丢数据;
    2. 没有解决master写的压力

集群proxy模式
集群proxy模式
    TwemproxyTwemproxy 是一个Twitter开源的一个redis和memcache 快速/轻量级代理服务器。Twemproxy是一个快速的单线程代理程序,支持Memcached ASCII协议和redis协议。
  • 特点:
    1. 多种hash算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkis;
    2. 支持失败节点自动删除;
    3. 后端Sharding分片逻辑对业务透明,业务方的读写方式和操作单个Redis一致;
  • 缺点:
    1. 增加了新的proxy,需要维护其高可用;
    2. failover逻辑需要自己实现,其本身不能支持故障的自动转移;
    3. 可扩展性差,进行扩缩容都需要手动干预;

直连版(三主三从)(cluster)单节点模式
直连版(三主三从)(cluster)单节点模式
  • 特点:
    1. 无中心架构(不存在哪个节点影响性能瓶颈),少了proxy层;
    2. 数据按照slot存储分布在多个节点,节点时间数据共享,可动态调用整数分布;
    3. 可扩展性,可线性扩展到1000个节点,节点可动态添加或删除;
    4. 高可用性,部分节点不可用时,集群仍可用,通过增加Slave做备份数据副本;
    5. 实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升;
  • 缺点:
    1. 资源隔离性较差,容易出现相互影响的情况;
    2. 数据通过异步复制,不保证数据的强一致性;

你可能感兴趣的:(成长之路-Redis)