什么是Redis-简介

  • 简介
    • Redis (Remote Dictionary Server远程字典服务器 )是遵守BSD协议完全开源免费的数据库。使用ANSI C语言编写。
      • ANSI C语言:ANSI C是由美国国家标准协会(ANSI)及国际标准化组织(ISO)推出的关于C语言的标准。ANSI C 主要标准化了现存的实践, 同时增加了一些来自 C++ 的内容 (主要是函数原型) 并支持多国字符集 (包括备受争议的三字符序列)。 ANSI C 标准同时规定了 C 运行期库例程的标准。--来自百度百科
    • 可用作数据库存储key-value类型数据,支持字符串、哈希表、列表、集合、有序集合、位图、地理空间信息等数据,也可以用作高速缓存和消息队列处理。 
    • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
    • Redis运行与独立的进程,通过网络协议和用户交互,将数据保存在内存中并且可以将数据持久化进入磁盘。Redis具有跨服务器的水平差分、复制的分布式特性。基于内存的特点,使得Redis与传统的关系型数据库相比有极高的吞吐量和响应性能。
  • 优点:
    • 支持多种数据结构,如 string(字符串)、 list(双向链表)、dict(hash表)、set(集合)、zset(排序set)、hyperloglog(基数估算)等
    • 支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失 的手段。
    • 支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制,master-slave机制是Redis进行HA的重要手段。
    • 单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题。
    • 支持pub/sub消息订阅机制,可以用来进行消息订阅与通知。
    • 支持简单的事务需求,但业界使用场景很少,并不成熟。
  • 缺点
    • Redis只能使用单线程,性能受限于CPU性能,故单实例CPU最高才可能达到5-6wQPS每秒(取决于数据结构,数据大小以及服务器硬件性能,日常环境中QPS高峰大约在1-2w左右)。
    • 支持简单的事务需求,但业界使用场景很少,并不成熟,既是优点也是缺点。
    • Redis在string类型上会消耗较多内存,可以使用dict(hash表)压缩存储以降低内存耗用。
  • 数据结构
    • Redis没有传统的table模型,在同一个db中,key作为其顶层模型,其值是扁平化的(即db本身就是key值的命名空间),他们通常用“:”链接,作为Redis中当前db下的key值,即“demo:123”作为key值,表示在demo这个命名空间下id为123的元素的key,类似于关系型数据库中的demo表主键为123的行。
    • 业务的多样性需要存储系统具有更加丰富的数据结构,Redis将这样的功能放在单条key-value内部,用结构化的value对象满足业务的多样的要求。另外,redis中key为二进制数,因此字符串和图片都可以作为key。
    • Redis常用的value包含5种数据结构:string、list、set、map(hash)、sort-set
  1. string
    • String是最常用的一种数据类型,普通的key/value存储都可以归为此类,string在Redis中可以表达三类值
      • 字符串
      • 整数
      • 浮点数
    • redis可以根据场景对这三类值进行相互之间的转换,并且根据需要选取底层承载的方式。在Redis内部作为字符串承载的String型value内部以int、SDS(simple dynamic string) 作为存储结构。int用来存放整形数据,SDS用来存放字节串/字符串/浮点型数据。Redis作为基于c实现的系统,sds相对于c的标准字符串,封装了更多的信息用来提升基本操作的性能。
    • 整数和浮点数类型的value具备自增、自减、减少给定值等数字型操作。字符/字节串也有相关的字符字节型操作。
  2. list
    • 列表对象,用来存储String序列。
    • Redis的list类型是通过来牛表来完成的,其value对象内部以linklist或者ziplist进行承载。当List的元素个数和单个元素的长度较小时,Redis会使用ziplist来进行承载,可以减少内存的使用,否则,采用linklist来承载。
      • linklist实现:内部实现采用的为双向链表,所以操作有左右插入与左右取值等操作,在list(linklist的结构体)中定义了头尾元素的指针和列表的长度,使得POP/PUSH操作、LENG(求长度的操作)的时间复杂度为O(1)。然而对于求任意值的操作时间复杂度还是O(n)。
      • ziplist实现:该链表在内存中是连续存储的。采用的以为数组的方式进行存储。
    • list可以用于多个场合,比如消息服务,通过rpush(右插入)追加消息,其他客户端通过lpop(左取值)或者rpop读取list的信息。
    • 假设现在有 job 、 command 和 request 三个列表,其中 job 不存在, command 和 request 都持有非空列表。考虑以下命令:
      • BLPOP job command request 30 #阻塞30秒,0的话就是无限期阻塞,job列表为空,被跳过,紧接着command 列表的第一个元素被弹出。
        • "command" # 弹出元素所属的列表
        • "update system..." # 弹出元素所属的值
      • 为什么要阻塞版本的pop呢,主要是为了避免轮询。举个简单的例子如果我们用list来实现一个工作队列。执行任务的thread可以调用阻塞版本的pop去获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。
  3. map
    • map型的value在redis中又叫做hash,底层实现可以是ziplist或者hashtable。
      • Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式(ziplist底层数据类型)来紧凑存储以节约内存。
      • 当成员数量增大时会自动转成真正的HashTable。
    • redis的value可以为map说明,redis本身是key-value结构,它的value也可以是map了类型即key-value类型,但是map里面的value不可以是key-value结构了,只能是String型所能表达的内容:整形、浮点型、字符字节串。
    • 对于map的基本操作和大多数程序语言操作类似,由于map的value可以存储数字类型数据,所以redis支持对特定key的value作数字专有的操作。
    • 我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
    •  * 用户ID,为查找的key,
      1. 存储的value用户对象包含姓名name,年龄age,生日birthday 等信息,
      2. 如果用普通的key/value结构来存储,主要有以下2种存储方式:
        1. 第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,
          1. 如:set u001 "李三,18,20010101"
          2. 这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
        2. 第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,
          1. 如:mset user:001:name "李三 "user:001:age18 user:001:birthday "20010101"
          2. 虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
          3. 那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap, 并提供了直接存取这个Map成员的接口,
          4. 如:hmset user:001 name "李三" age 18 birthday "20010101"
          5. 也就是说,Key仍然是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值,
          6. 这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过
          7. key(用户ID) + field(属性标签) 操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
        3. 这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
  4. set
    • set元素不重复的以String(整数,浮点数,字符/字节串)为类型的无序集合,redis的set除了元素的添加删除操作之外,还具有集合的并集、交集等功能,可以用于统计网站所有的ip或者统计网站作者共有的粉丝等操作。
    • Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
    • set在redis中以intset或者hashtable来存储。
      • hashtable在前面已经介绍,应该注意的是,在redis中的set用hashtable实现时hashtable的value的永远为NULL
      • intset的结构体中的核心是一个字节数组。下面会详细介绍。
  5. sort set
    • Sort set是redis特有的数据结构,使用场景与set类似,区别是set不会自动排序,而sort se通过用户提供的一个额外的优先级(score)的参数进行排序,并且插入有序的即自动排序。
    • Sort set类似map是一个key-value键值对,key是不重复的唯一的,value存放的为前面所提到的score,内部按照score从大到小进行排序。
    • sort set的value对象内部以ziplist或者skiplist+hashtable来实现的。这两种底层数据结构下面会详细介绍。

Redis对象底层八种结构的实现

  • 列表如下

编码常量

编码所对应的底层数据结构

REDIS_ENCODING_INT

long 类型的整数

REDIS_ENCODING_EMBSTR (embsir)

embstr 编码的简单动态字符串

REDIS_ENCODING_RAW

简单动态字符串

REDIS_ENCODING_HT

字典

REDIS_ENCODING_LINKEDLIST ( linkedlist)

双端链表

REDIS_ENCODING_ZIPLIST

压缩列表

REDIS_ENCODING_INTSET

整数集合

REDIS_ENCODING_SKIPLIST

跳跃表和字典

  • 详细解释如下
  • embstr
    • ​embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为objet分配对象,embstr省去了第一次)。
    • 相对地,释放内存的次数也由两次变为一次。
    • ​embstr的objet和sds放在一起,更好地利用缓存带来的优势。
    • embstr是只读的形式 对embstr的修改实际上是先转换为raw再进行修改。
    • ziplist
      • 压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。
      • 当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。
    • intset
      •  intset是一个有序集合,查找元素时因为其有序所以采用了二分法进行查询,所以其时间复杂度为O(logN),但插入时不一定为O(logN),因为有可能涉及到升级操作。比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。但intset不支持降级操作。
    • skiplist
      • skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。
  • 总结
    • ​* String对象的编码可以是int、raw或者embstr。
    • * List对象的编码可以是ziplist或者linkedlist。
    • * Map(Hash)对象的底层实现可以是ziplist(当对象数目不多且内容不大时,这种方式效率是很高的)或者hashtable。
    • * Set对象的编码可以是intset或者hashtable。
    • * Sort Set的编码可能两种,一种是ziplist,另一种是skiplist与hashtable的结合。
    • 前面说到:有序集合Sort Set是有跳跃表和hashtable共同形成的。
      • * 为什么要用这种结构呢。试想如果单一用hashtable,那可以快速查找、添加和删除元素,但没法保持集合的有序性。如果单一用skiplist,有序性可以得到保障,但查找的速度太慢O(logN)。
  •    事实上,Redis的高效与灵活性正是得益于存在多种的存储结构与多种底层数据结构与对于同一个和对象采取不同的底层数据结构存储,并且在必要时对而这进行转换,以及各种底层结构对内存的合理利用。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券