Redis学习记录(一)

2022/2/22 19:26:55

本文主要是介绍Redis学习记录(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

  1. Redis整体解读

    Redis是一个高性能(每秒处理超过10万次读写操作)的key-value型非关系型数据库,C语言编写开源、支持网络、基于内存、可选持久化

    键值对数据库的实现:实质就是基于哈希表,在Reids中key就是字符串对象,value就是可以是Redis支持的任意数据类型(String.list.hash.set.zset),结构图如下:
    Redis键值对结构图.png

    rehash:使用到两张哈希表,[哈希表2]为[哈希表1]空间的两倍,把[哈希表1]的数据rehash到[哈希表2]中,并把[哈希表2]设置为[哈希表1]以供Redis做查询、插入等操作,rehash的触发条件则是通过【负载因子 = 已保存节点数 / 哈希表大小】判断的(① >1 没有进行RDB快照和AOF重写时rehash ② >5 强制rehash
    注意:Redis的rehash过程是渐进式rehash,也就是在将[哈希表1]中的数据迁移到[哈希表2]的过程中,由于数据量较大,这个数据迁移操作不是一次性完成的,而是在Redis正常对[哈希表1]进行新增、删除等操作时,每次迁移一部分数据到[哈希表2],这个过程保证[哈希表1]中的数据量越来越少,当然这个时候的查询操作需要查询两个哈希表来得到结果

    渐进式rehash的思想在于将rehash键值对所需的计算工作分散到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的阻塞问题

  2. Redis数据类型与数据结构的关系图
    Redis数据类型与数据结构的对应关系.png

  3. Redis主要几种数据类型
    String:Redis的String底层实现区别于C语言的底层实现

         C语言采用char*字符数组来实现字符串,该实现存在部分缺点:
             ① 在获取字符串长度时,需要遍历整个数组,复杂度为O(n)  
             ② 采用'\0'作为字符串的结尾标识,因此不能存储包含有'\0'字符的数据,也就不能保存二进制数据  
             ③ 字符串操作存在缓冲区溢出风险-不安全,而且操作同样需要遍历char*数组-低效
         Redis的String对象实现则是采用SDS(simple dynamic string),实现方式是使用三个元数据来保存字符串的相关信息,结构为:  
             len:字符串长度
             alloc:分配的空间长度,自动扩展
             flags:SDS类型,不同类型SDS数据结构中len和alloc的数据类型不一样
             buf[]:字节数组,存放实际数据
         优点:
             ① O(1)时间复杂度取字符串长度,提升效率
             ② 不采用'\0'来标识字符串结尾,二进制的形式处理数据,同时兼容C语言标准库函数
             通过alloc和len判断剩余可用空间大小,自动扩展SDS内存空间(<1MB时-翻倍扩容,>1MB时-每次扩容1MB),防止缓冲区溢出,保证安全
             ③ 编译时,使用__attribute__ ((packed)),优化对对齐方式,按结构体中数据实际占用字节数对齐  
    

    list:链表数据类型的实现底层用到了双向链表压缩列表,Redis3.2版本之后底层使用quicklist
    双向链表:使用到了两个数据结构实现,分别是链表节点(listNode)和链表结构体(list)
    listNode:包含前置节点、后置节点和节点值
    list:包含链表头结点、尾节点、链表长度以及一些操作函数节点复制函数、节点释放函数和节点比较函数等,二者对应关系如下:
    Redis链表.png

    因为链表节点之间不是连续内存以及链表节点结构体需要消耗额外内存的原因,在数据量较少的时候list会采用压缩列表作为底层实现

    压缩列表:设计为一种内存紧凑型的数据结构,List 对象、Hash 对象、Zset 对象包含的元素数量较少,或者元素值不大的情况才会使用压缩列表作为底层数据结构
    结构设计 --- 表头新增三个字段zlbytes(记录压缩列表占用的字节数)、zltail(记录列表其实节点到尾部节点的偏移量)、zllen(记录压缩列表包含的节点数量),中间部分存放节点(entry)数据,表尾部zlend(结束点,固定值0xFF)
    entry中prevlen(前一个节点长度)、encoding(当前节点类型和长度)、data(实际数据)
    redis压缩列表.png

         压缩列表的一个明显缺点就是,在新增或修改某个元素的时候,如果空间不够,内存空间就需要重新分配,消耗性能,当新增大元素时,导致后续节点的prevlen占用的空间发生变化,并持续影响后续节点,导致每个节点都要做空间重新分配,这也就是导致了连锁更新,这是就相当消耗系统性能的,Redis3.2之后的quicklist和5.0之后的listpack两种数据结构都是为了在保持压缩列表节省内存的基础上解决连锁更新
    

    quicklist:Redis3.2版本之后,list对象底层由quicklist数据结构实现,quicklist本质就是[双向链表+压缩列表]的组合,quicklist整体就是一个链表,其中链表节点就是一个压缩列表,将压缩列表尽可能缩小,尽可能避免连锁更新,但是这并没有解决连锁更新问题

    hash:Redis对象的一种底层实现是压缩列表,5.0之后使用listpack,另外一种底层实现就是哈希表
    链式哈希解决哈希冲突:键值key通过hash函数得到哈希值取模得到在就哈希表中的位置,哈希表实际就是一个数组,数组中每一项被称作一个哈希桶,通过上面求得的值找到具体的哈希桶==链表,访问链表得到想要的数据

    Redis5.0之后新设计了数据结构listpack,目的是替代压缩列表,listpack的设计entry中去除了表示前一个节点长度的字段(就是因为这个字段才会产生连锁更新)
    Redis数据结构listpack.png

    set:类似list提供一个人列表的功能,只是在list的基础之上进行了去重处理,它的底层实现有两种:

         ① 基于**哈希表**实现,key-value键值对的形式保存,其中value赋null,set中的数据对应就是key,通过hash函数来实现去重
         ② 当set对象中只包含整数值时,采用**整数集合**数据结构作为set的底层实现  
    

    整数集合的结构体定义包括 ==> encoding(编码方式),length(set对象中数据量),contents,其中encoding用来决定contents数组的数据类型(int16_t | int32_t | int64_t),可以自动进行类型转换,也叫升级操作,当新增元素类型(int32_t)大于数组中所有元素的类型(int16_t)时,扩展数组空间,将数组的所有元素分配空间类型转换为int32_t,encoding也发生相应变化

    sorted set:类似于set对象,zset的区别在提供一个优先级(score)参数来作为成员排序,支持自动排序,有序集合的成员是唯一的,但分数(score)却可以重复

    zset底层使用了两个数据结构来实现,一个是跳表,一个是哈希表,这样做好处就是既可以支持高效范围查询也能进行高效单点查询

    跳表是在链表基础上改进过来的,实现了一种多层的有序链表
    Redis数据结构-跳表.png
    跳表节点结构如下:

    typedef struct zskiplistNode {
    //Zset 对象的元素值
    sds ele;
    //元素权重值
    double score;
    //后向指针
    struct zskiplistNode *backward;
    
    //节点的level数组,保存每层上的前向指针和跨度,每个元素代表跳表的一层
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
    } zskiplistNode;
    

    跳表结构体:

    typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
    } zskiplist;
    

  1. Redis & Memcached
    Memcachede:多线程的非阻塞IO复用的网络模型,只保证数据的原子性,不支持事务,使用Slab(小内存分配器)的内存管理方式,只支持String一种数据类型,数据库层级只支持分布式拓展,因为没有复杂的内存管理机制和数据类型,所以memcachede在缓存小型静态数据方面效率比较高,过期数据删除策略惰性删除(只有key被用到时才会对数据进行过期检查)

    Redis:分布式内存数据库,单线程的多路IO复用模型,支持丰富的数据类型,支持事务处理,支持数据持久化,实现了灾难恢复机制,支持在服务器端进行数据操作,过期数据删除策略(惰性删除 & 定期删除)

    Redis的单线程运行模式:Redis并不是单纯的单线程服务模型,一些辅助工作比如持久化刷盘、惰性删除等任务是由BIO(Blocking IO)线程来完成的,这里说的单线程主要是说与客户端交互完成命令请求和回复的工作线程,Redis为何要使用单线程 => ①使用多线程的目的就是充分利用多核CPU,但是Redis的性能瓶颈不是CPU而是内存 ②Redis丰富的数据结构,采用多线程模式需要加锁进行同步,容易造成死锁消耗性能

  2. Redis的过期数据删除策略 & 内存淘汰机制

    随着数据库数据量的不断增加,同时系统内存是有限的情况下,必然会导致OOM的发生, 这也就是设置过期数据的原因,当数据过期时就要对数据进行删除操作,这也就是下面提到的两种删除策略,过期数据的使用其一是防止OOM,还有一点就是平时在使用token、验证码类似需要添加有效时间的功能时,就很容易实现了

    删除策略:

    • 惰性删除 => 只有在key被拿到的时候才会进行过期检查和删除操作

    • 定期删除 => 间隔一定时间抽取就部分key进行删除过期key操作,控制执行间隔来减小对CPU执行效率的影响

        虽然Redis使用[惰性删除+定期删除]可以在保证在CPU有不错执行效率的前提下删除大部分过期的key,但是仍会有很多过期数据没有被删除掉,这个时候为了防止这部分过期数据持续堆积,Redis就用到了另外一种机制 ==> 内存淘汰机制
      

    Redis关于内存淘汰机制提供了八种策略,分别是:

    ①volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
    ②volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
    ③volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
    ④allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
    ⑤allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 ⑥no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧
    ⑦volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
    ⑧allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key # Redis

  3. Redis单进程单线程模式下解决并发问题
    采用队列模式将并行访问转变为串行访问,Redis本身没有锁概念,但是Jedis对Redis进行访问时会发生连接超时、数据转换错误、阻塞等问题,要规避连接混乱问题,有两个解决办法:

    • 客户端角度控制与Redis的通信,对读写操作进行加锁(synchronized | lock),对连接进行池化

    • 在服务端角度,利用setnx(set if not exists)实现锁,处理缓存逻辑的时候获取锁(通过setnx拿到key),处理结束删除锁(删除key),如果处理逻辑出现意外导致程序退出,可能导致锁一直存在,这时需要对这个key增加一个过期时间expire,同时通过新增一个随机值来解决过期时间小于处理逻辑耗时的情况

    // 获取锁、设置过期时间、获取随机值
    $rs = $redis->set($key, $random, array('nx', 'ex' => $ttl));
    if ($rs) {
        //处理更新缓存逻辑
        // ......
        //先判断随机数,是同一个则删除锁
        if ($redis->get($key) == $random) {
            $redis->del($key);
        }
    }
    
  4. Redis => 异步消息队列
    采用list对象作为队列,rpush生产消息,lpop消费消息,但是当队列中没有消息时,需要sleep一会再重试,还可以使用blpop阻塞等待消息

    使用pub/sub主题订阅者模式,可以实现1:N的消息队列

    延迟队列的实现可以使用sortedset,用时间戳作为score,消费者使用zrangebyscore获取N秒前的数据来进行处理

  5. Redis中的Reactor模式
    Reactor模式经典类图:
    Reactor模式.png

    Redis当中的事件模型:
    Redis事件处理模型.png

  6. Redis数据持久化

    • RDB 将数据库快照以二进制的方式保存到磁盘中---隔段时间执行一次,故障宕机时将会丢失上次持久化到宕机时刻的数据,无法保证数据完整性
    • AOF 以协议文本方式,将所有对数据库进行过写入的命令和参数记录到 AOF 文件,从而记录数据库状态---存储指令序列,数据恢复比较耗时且文件数据量更大

    Redis混合型数据持久化:创建同时包含RDB数据和AOF数据的AOF文件,它们储存了服务器开始执行重写操作时的数据库状态,至于那些在重写操作执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾

  7. 缓存读写策略
    ① 旁路缓存模式(Cache Aside Pattern):写操作先更新DB,然后删除cache;读操作直接读cache,没有则读DB再更新cache

    先删除cache,再更新DB的缺点:请求1删除cache -> 请求2从DB读取数据并更新cache -> 请求1再拿DB中的数据更新
    先更新DB,再删除cache的缺点:请求1从DB读数据A -> 请求2写更新数据 A 到数据库并把更新cache中的A数据 -> 请求1将数据A写入cache
    

    ② 读写穿透(Read/Write Through Pattern):
    》>Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责

    ③ 异步缓存写入(Write Behind Pattern):和Read/Write Through Pattern策略类似,只是cache更新到DB时是采用批量操作

  8. 哨兵(Sentinel)是一种运行模式,其具备的能力如下:

    监控(Monitoring):持续监控Redis主节点、从节点是否处于预期的工作状态
    通知(Notification):哨兵可以把Redis实例的运行故障信息通过API通知监控系统或者其他应用程序
    自动故障恢复(Automatic failover):当主节点运行故障时,哨兵会启动自动故障恢复流程:某个从节点会升级为主节点,其他从节点会使用新的主节点进行主从复制,通知客户端使用新的主节点进行
    配置中心(Configuration provider):哨兵可以作为客户端服务发现的授权源,客户端连接到哨兵请求给定服务的Redis主节点地址。如果发生故障转移,哨兵会通知新的地址。这里要注意:哨兵并不是Redis代理,只是为客户端提供了Redis主从节点的地址信息。

参考内容
https://www.cnblogs.com/xiaolincoding/p/15628854.html
https://javaguide.cn/database/redis/redis-questions-01/
https://mp.weixin.qq.com/s/lxMP4-Z3DzQg5fRqLs9XNA
https://mp.weixin.qq.com/s/6NobACeeKCcUy98Ikanryg
https://www.runoob.com/redis/redis-sorted-sets.html



这篇关于Redis学习记录(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程