zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

Redis的介绍以及底层原理的剖析

Redis原理 介绍 以及 剖析 底层
2023-06-13 09:15:40 时间

什么是Redis,

Notsql=no only sql(不仅仅是sql)

  1. 关系型数据库:列+行,同一个表下数据的结构是一样的
  2. 非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展

NotSql泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难应对Web2.0大数据时代,尤其是超大规模的高并发的情况,暴露出来很多难以克服的问题,NoSql在当今大数据环境下发展的十分迅速,Redis是发展最快的。

传统的RDBMS和NoSql

RDBMS
  1. 组织化结构
  2. 固定SQL
  3. 数据和关系都存在单独的表中
  4. DML(数据操作语言)、DDL(数据定义语言)等
  5. 严格的一致性(ACID):原子性、一致性、隔离性、持久性
  6. 基础的事务
NoSQL
  1. 不仅仅是数据
  2. 没有固定的查询语言
  3. 键值对存储(Redis)、列存储(Hbase)、文档存储(MongoDb)、图形数据库(不是存图形,放的是关系),等等
  4. 最终一致性(BASE):基本可用、软状态、柔性事务、最终一致性

Redis是什么

Redis=Remote Dictionary Server,远程字典服务

Redis是一个开源的使用ANSIC语言编码的、支持网络、可以基于内存的可持久化的日志型、Key-Value数据库,并且提供多种语言的API。与memcache一样,为了保证效率,数据都是缓存在内存中。区别是Redis会周期性的吧更新的数据写入到磁盘或者把修改操作写入追加记录文件,并且在此基础上实现了master-slave(主从同步)

Redis五大基本类型

Redis是一个开源、内存存储的数据结构服务器,可用做数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合、位图等数据类型。内置复制、Lua脚本、LRU收回事务以及不同级别磁盘持久化功能,同时通过Redis Sentinnel提供高可用,通过Redis Cluster提供自动分区

由于Redis类型大家很熟悉,并且指令的介绍比较多,下面主要介绍一下常用的

String字符串

String类型是Redis的最基础的数据结构,也是最经常使用的类型,而且其他的四种类型多多少少都在字符串类型的基础上构建的,所以String类型是Redis的基础。String类型的值最大能存储512MB,这里的String类型可以是简单的字符串、复杂的xml、json字符串,二进制图像或者音频的字符串,以及可以是数字的字符串。

应用场景
  1. 缓存功能,String字符串是最常用的数据类型、不仅仅是Redis,各个语言都是基本类型,因此,利用Redis作为缓存,配合其他数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度,以及降低后端数据库的压力
  2. 计数器,许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其他存储介质当中进行永久保存
  3. 统计多单位的数量,eg、uid、gongming、count:0,根据不同的uid更新count数量
  4. 共享用户session,用户重新刷新一次界面,可以需要访问一下数据进行重新登录,或者访问页面缓存cookie,这两种方式做有一定的缺点
    1. 每次都是重新登录效率低下
    2. cookie保存在客户端,有安全隐患
    3. 可以使用Redis将用户的session集中管理,在这种模式只需要保证Redis的高可用,每次用户session的更新和获取都可以快速完成,可以大大的提高效率
List列表

List类型是用来存储多个有序的字符串,列表当中的每一个字符看作一个元素,一个列表当中可以存储有一个或者多个元素,Redis的list支持存储2的32次方-1个元素。

Redis可以从列表的两端进行插入Pubsh和弹出pop元素,支持读取指定范围的元素集,或者读取指定下标的元素等操作。Redis列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色

Redis列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的,意味着他可以根据链表的下标获取指定的元素和某个范围内的元素集

应用场景
  1. 消息队列,Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据
  2. 文章列表或者数据分页展示的应用,举例:博客网站的文章,当用户量越来越多的时候,而且每一个用户都有自己的文章时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能,大大提高查询效率。
Set集合

Redis集合,Set类型和和列表类型类似,都可以用来存储多个字符串元素集合。但是和List不同的是Set集合当中不允许重复的元素。而且Set集合当中元素是没有顺序的,不存在元素的下标。

Redis的Set类型是使用哈希表构造的,因此复杂度是O(1),它支持集合内的增删改查,并且支持多个集合间的交集、并集、差集的操作。可以利用这些集合操作,解决程序开发当中很多数据集合间的问题。

应用场景
  1. 标签,博客网站经常使用到的兴趣标签,把有着相同爱好,关注类似内容的用户利用标签把他们进行归并。
  2. 共同好友功能,共同爱好,或者好友之类的扩展应用
  3. 统计网站的独立IP,利用Set集合当中元素不唯一性,可以快速实现统计访问网站的独立IP
  4. 数据结构,Set的底层结构相对复杂,使用Intset和hashtable两种数据结构存储,intset可以理解为数组。
Sorted set有序集合

Redis有序集合也是集合类型的一部分,所以保留了集合中元素不能重复的特性,但是不同的是,有序集合给每个元素多设置了一个分数

Redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,有序集合给每个元素多设置了一个分数,利用该分数作为排序的依据

应用场景

  1. 排行榜,有序集合使用场景,例如视频网站需要用户上传视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的点赞数等
  2. 用Sorted Set做带权重的队列,比如说普通消息的source为2,然后工作线程可以选择按score的倒叙来获取工作任务,让重要的工作优先执行
hash(哈希)

Redis hash数据结构是一个键值对(key-value)集合,它是一个String类型的field和value的映射表,Redis本身是一个key-value类型的数据库,因此hash数据结构相当于在value中套用了一个key-value类型的数据,所以Redis数据结构特别适合存储关系型对象。

应用场景
  1. 由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是Redis中哈希类型中最常见的场景,一条记录作为一个key-value,把每个列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。
  2. 经常被用来存储用户相关信息,优化用户信息的获取,不需要重复从数据库当中读取,提供系统性能

基本类型底层结构

Redis内部整体的存储结构是一个大的HashMap,内部是数组实现的hash,key冲突通过挂链表去实现,每个dictEntry,每个dictEntry为一个key、value对象,value为定义的RedisObject

结构图如下:

dictEntry是存储key-value的地方,dictEnty的结构体

/*

  • 字典 */ typedef struct dictEntry { // 键 void *key; // 值 union { // 指向具体 RedisObject void *val; // uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next; } dictEntry
  1. RedisObject

/*

  • Redis 对象 */ typedef struct RedisObject { // 类型 4bits unsigned type:4; // 编码方式 4bits unsigned encoding:4; // LRU 时间(相对于 server.lruclock) 24bits unsigned lru:22; // 引用计数 Redis 里面的数据可以通过引用计数进行共享 32bits int refcount; // 指向对象的值 64-bit void *ptr; } robj;

ptr指向具体的数据结构的地址,type表示该对象的类型,即String、List、Hash、Set、ZSet中的一个,但是为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种,encoding表示对象底层使用的编码

  1. Redis对象底层的八种数据结构

REDIS_ENCODING_INT(long 类型的整数)

REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串)

REDIS_ENCODING_RAW (简单动态字符串)

REDIS_ENCODING_HT (字典)

REDIS_ENCODING_LINKEDLIST (双端链表)

REDIS_ENCODING_ZIPLIST (压缩列表)

REDIS_ENCODING_INTSET (整数集合)

REDIS_ENCODING_SKIPLIST (跳跃表和字典)

通过RedisObject可以具体的指向Redis数据类型,每种数据类型都使用数据的结构

String数据结构

String类型的转换顺序,1. 当保存的值为整数并且值的大小不超过long的范围,使用整数存储;2. 当字符串长度不超过44字节时,使用EMBATR编码。

它只是分配了一次空间,RedisObject和sds是连续的内存,查询效率会快很多,也正是因为RedisObject和sds是连续在一起的,便随了一些缺点;当字符串增加的时候,长度会增加,这个时候又需要重新分配内存,导致的结果就是整个RedisObject和sds都需要重新分配空间,这样会影响性能的,所以Redis用embStr实现一次分配,只允许读功能,如果修改数据,会转成raw编码,不再使用emStr编码了。

由于Redis底层使用C语言实现的,为啥没有使用C语言的字符串,而是使用了SDS结构体。

  1. 低复杂度获取字符串长度,由于len存在,可以直接查询出来字符串的长度,复杂度O(1);如果使用C语言字符串的查询字符串长度需要遍历整个字符串才行,复杂度为O(n)
  2. 避免缓冲区溢出,进行两个字符串拼接C语言可以使用Strcat函数,如果没有足够的内存空间,会造成缓冲区的溢出;而使用SDS在进行合并的时候会先用len检查内存空间是否满足需求,如果不满足,进行空间的扩展,不会造成缓冲区的溢出
  3. 减少修改字符串内存重新分配次数,C语言字符串不记录字符串的长度,如果要修改字符串要重新分配内存,如果不进行重新分配会造成内存缓冲区泄露
Redis SDS实现了空间预分配和惰性空间释放两种策略
  1. 如果SDS修改后,SDS长度将于1mb,那么分配与len相同大小的未使用空间,此时len与free的值相同,例如修改之后字符串长度100字节,那么会分配100字节未使用空间,最终SDS空间实际为100+100+1.
  2. 如果大于等于1mb,每次分配1mb未使用空间惰性空间释放,对字符串进行缩短操作时,程序不立即使用内存重新分配来回缩短后多于的字节,而是使用free属性将这些字节的数量记录下来,等待后续使用(SDS也提供API,可以手动触发字符串缩短)
  3. 二进制安全,因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件,如图片,内容可能包括空字符串,因此C字符串无法正确的存取,而所有的空字符串来判断是否结束,而是len属性表示的长度来判断字符串是否结束
  4. 遵从每个字符串都是空字符串结尾的情况,这样可以用C语言库<string.h>中的一部分函数
List存储结构
  1. Redis3.2之前的底层实现方式,压缩列表ziplist或者双向循环链表linkedlist。当list存储的数据量比较少且同时满足两个条件时,list就使用ziplist存储数据
    1. list中保存的每个元素的长度小于64字节
    2. 列表中的数据个数少于512个字节
  2. Redis3.2以及之后的底层实现方式,quicklist,quick是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点
    1. ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表元素的不大的时候,每个元素也不大的时候,就采用ziplist存储,但是当数据量过大的时候,就不是那么好用了。因为为了保证存储的内容在内存中的连续性,插入的复杂度是O(N),即每个插入都会重新进行realloc,重新分配。
    2. 如下图所示,RedisObject对象结构中ptr所指向的就是一个ziplist。整个ziplist只是需要malloc一次,他们 在内存中是一块连续的区域
    1. zlbytes:用于记录整个压缩列表占用的内存字节数
    2. zltail记录要列表尾部节点距离压缩列表的起始地址有多个字节。
    3. zllen:记录了压缩列表包含的节点梳理
    4. entry