Redis由浅入深深深深深剖析

前言

经常使用的SQL数据库的数据都是存在磁盘中的,虽然在数据库底层也作了对应的缓存来减小数据库的IO压力,但因为数据库的缓存通常是针对查询的内容,并且粒度也比较小,通常只有表中的数据没有发生变更的时候,数据库的缓存才会产生做用,但这并不能减小业务逻辑对数据库的增删改操做的IO压力,所以缓存技术应运而生,该技术实现了对热点数据的高速缓存,能够大大缓解后端数据库的压力。react

主流应用架构

主流应用架构
客户端在对数据库发起请求时,先到 缓存层查看是否有所需的数据,若是缓存层存有客户端所需的数据,则直接从缓存层返回,不然进行 穿透查询,对 数据库进行查询,若是在数据库中查询到该数据,则将该数据回写到缓存层,以便下次客户端再次查询可以直接从缓存层获取数据。

缓存中间件 -- Memcache和Redis的区别

  • Memcache:代码层相似Hash

    1.支持简单数据类型
    2.不支持数据持久化存储
    3.不支持主从
    4.不支持分片
    linux

  • Redis

    1.数据类型丰富
    2.支持数据磁盘持久化存储
    3.支持主从
    4.支持分片
    redis

为何Redis能这么快

Redis的效率很高,官方给出的数据是100000+QPS(query per second),这是由于:
算法

1.Redis彻底基于内存,绝大部分请求是纯粹的内存操做,执行效率高。
2.Redis使用单进程单线程模型的(K,V)数据库,将数据存储在内存中,存取均不会受到硬盘IO的限制,所以其执行速度极快,另外单线程也能处理高并发请求,还能够避免频繁上下文切换和锁的竞争,若是想要多核运行也能够启动多个实例。
3.数据结构简单,对数据操做也简单,Redis不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,相似于HashMap,HashMap最大的优势就是存取的时间复杂度为O(1)。
4.Redis使用多路I/O复用模型,为非阻塞IO(非阻塞IO会另写一篇解释,能够先行百度)。数据库


:Redis采用的I/O多路复用函数:epoll/kqueue/evport/select
选用策略:
1.因地制宜,优先选择时间复杂度为O(1)的I/O多路复用函数做为底层实现。
2.因为select要遍历每个IO,因此其时间复杂度为O(n),一般被做为保底方案。
3.基于react设计模式监听I/O事件。
后端


Redis的数据类型

  • String

    最基本的数据类型,其值最大可存储512M,二进制安全(Redis的String能够包含任何二进制数据,包含jpg对象等)。 设计模式

    redis存String
    注:若是重复写入key相同的键值对,后写入的会将以前写入的覆盖。

  • Hash

    String元素组成的字典,适用于存储对象。 缓存

    redis存Hash

  • List

    列表,按照String元素插入顺序排序。其顺序为后进先出。因为其具备栈的特性,因此能够实现如“最新消息排行榜”这类的功能。 安全

    redis存List

  • Set

    String元素组成的无序集合,经过哈希表实现(增删改查时间复杂度为O(1)),不容许重复。 bash

    redis存Set
    另外,当咱们使用smembers遍历set中的元素时,其顺序也是不肯定的,是经过hash运算事后的结果。Redis还对集合提供了求交集、并集、差集等操做,能够实现如同共同关注,共同好友等功能。

  • Sorted Set

    经过分数来为集合中的成员进行从小到大的排序。

    redis存SortedSet

  • 更高级的Redis类型

    用于计数的HyperLogLog、用于支持存储地理位置信息的Geo。

从海量Key里查询出某一个固定前缀的Key

  • 假设redis中有十亿条key,如何从这么多key中找到固定前缀的key?

    • 方法1:使用KEYS [pattern]:查找全部符合给定模式pattern的key

      使用keys [pattern]指令能够找到全部符合pattern条件的key,可是keys会一次性返回全部符合条件的key,因此会形成redis的卡顿,假设redis此时正在生产环境下,使用该命令就会形成隐患,另外若是一次性返回全部key,对内存的消耗在某些条件下也是巨大的。 例:
      keys test* //返回全部以test为前缀的key
    • 方法2:使用SCAN cursor [MATCH pattern] [COUNT count]

      cursor:游标 MATCH pattern:查询key的条件 count:返回的条数 SCAN是一个基于游标的迭代器,须要基于上一次的游标延续以前的迭代过程。SCAN以0做为游标,开始一次新的迭代,直到命令返回游标0完成一次遍历。此命令并不保证每次执行都返回某个给定数量的元素,甚至会返回0个元素,但只要游标不是0,程序都不会认为SCAN命令结束,可是返回的元素数量大几率符合count参数。另外,SCAN支持模糊查询。 例:
      SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key

如何经过Redis实现分布式锁

  • 分布式锁

    分布式锁是控制分布式系统之间共同访问共享资源的一种锁的实现。若是一个系统,或者不一样系统的不一样主机之间共享某个资源时,每每须要互斥,来排除干扰,知足数据一致性。
    分布式锁须要解决的问题以下:
    1.互斥性:任意时刻只有一个客户端获取到锁,不能有两个客户端同时获取到锁。
    2.安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
    3.死锁:获取锁的客户端由于某些缘由而宕机继而没法释放锁,其它客户端再也没法获取锁而致使死锁,此时须要有特殊机制来避免死锁。
    4.容错:当各个节点,如某个redis节点宕机的时候,客户端仍然可以获取锁或释放锁。

  • 如何使用redis实现分布式锁

    • 使用SETNX实现

      SETNX key value:若是key不存在,则建立并赋值。该命令时间复杂度为O(1),若是设置成功,则返回1,不然返回0。

      redis分布式锁
      因为SETNX指令操做简单,且是原子性的,因此初期的时候常常被人们做为分布式锁,咱们在应用的时候,能够在某个共享资源区以前先使用SETNX指令,查看是否设置成功,若是设置成功则说明前方没有客户端正在访问该资源,若是设置失败则说明有客户端正在访问该资源,那么当前客户端就须要等待。可是若是真的这么作,就会存在一个问题,由于SETNX是长久存在的,因此假设一个客户端正在访问资源,而且上锁,那么当这个客户端结束访问时,该锁依旧存在,后来者也没法成功获取锁,这个该如何解决呢?

      因为SETNX并不支持传入EXPIRE参数,因此咱们能够直接使用EXPIRE指令来对特定的key来设置过时时间。

      用法EXPIRE key seconds

      expire指令.png

      程序

      RedisService redisService = SpringUtils.getBean(RedisService.class);
      long status = redisService.setnx(key,"1");
      if(status == 1){
        redisService.expire(key,expire);
        doOcuppiedWork();
      }
      复制代码

      这段程序存在的问题:假设程序运行到第二行出现异常,那么程序来不及设置过时时间就结束了,则key会一直存在,等同于锁一直被持有没法释放。出现此问题的根本缘由为:原子性得不到知足
      解决:从Redis2.6.12版本开始,咱们就可使用Set操做,将Setnx和expire融合在一块儿执行,具体作法以下。

      SET KEY value [EX seconds] [PX milliseconds] [NX|XX]
      复制代码

      EX second:设置键的过时时间为second
      PX millisecond:设置键的过时时间为millisecond毫秒
      NX:只在键不存在时,才对键进行设置操做。
      XX:只在键已经存在时,才对键进行设置操做。
      :SET操做成功完成时才会返回OK,不然返回nil。

      有了SET咱们就能够在程序中使用相似下面的代码实现分布式锁了:

      RedisService redisService = SpringUtils.getBean(RedisService.class);
      String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
      if("OK.equals(result)"){
        doOcuppiredWork();
      }
      复制代码

如何实现异步队列

  • 使用Redis中的List做为队列

    使用上文所说的Redis的数据结构中的List做为队列 Rpush生产消息,LPOP消费消息。

    使用Redis做为异步队列
    此时咱们能够看到,该队列是使用rpush生产队列,使用lpop消费队列。在这个生产者-消费者队列里,当lpop没有消息时,证实该队列中没有元素,而且生产者尚未来得及生产新的数据
    缺点:lpop不会等待队列中有值以后再消费,而是直接进行消费。
    弥补:能够经过在应用层引入Sleep机制去调用LPOP重试。

  • 使用BLPOP key [key...] timeout

    BLPOP key [key ...] timeout:阻塞直到队列有消息或者超时。

    两个客户端模拟A

    两个客户端模拟B

    两个客户端模拟C
    缺点:按照此种方法,咱们生产后的数据只能提供给各个单一消费者消费

    可否实现生产一次就能让多个消费者消费呢?

  • pub/sub:主题订阅者模式

    发送者(pub)发送消息,订阅者(sub)接收消息。 订阅者能够订阅任意数量的频道

    发布订阅者模式
    pub/sub模式的缺点
    消息的发布是无状态的,没法保证可达。对于发布者来讲,消息是“即发即失”的,此时若是某个消费者在生产者发布消息时下线,从新上线以后,是没法接收该消息的,要解决该问题须要使用专业的消息队列,如kafka...此处再也不赘述。

Redis持久化

  • 什么是持久化

    持久化,即将数据持久存储,而不因断电或其它各类复杂外部环境影响数据的完整性。因为Redis将数据存储在内存而不是磁盘中,因此内存一旦断电,Redis中存储的数据也随即消失,这每每是用户不指望的,因此Redis有持久化机制来保证数据的安全性。

  • Redis如何作持久化

    Redis目前有两种持久化方式,即RDBAOF,RDB是经过保存某个时间点的全量数据快照实现数据的持久化,当恢复数据时,直接经过rdb文件中的快照,将数据恢复。

  • RDB(快照)持久化:保存某个时间点的全量数据快照

    RDB持久化会在某个特定的间隔保存那个时间点的全量数据的快照。 RDB配置文件: redis.conf:

    save 900 1 #在900s内若是有1条数据被写入,则产生一次快照。
      save 300 10 #在300s内若是有10条数据被写入,则产生一次快照
      save 60 10000 #在60s内若是有10000条数据被写入,则产生一次快照
      stop-writes-on-bgsave-error yes 
      #stop-writes-on-bgsave-error :
      若是为yes则表示,当备份进程出错的时候,
      主进程就中止进行接受新的写入操做,这样是为了保护持久化的数据一致性的问题。
    复制代码
    • RDB的建立与载入

      SAVE:阻塞Redis的服务器进程,直到RDB文件被建立完毕。SAVE命令不多被使用,由于其会阻塞主线程来保证快照的写入,因为Redis是使用一个主线程来接收全部客户端请求,这样会阻塞全部客户端请求。
      BGSAVE:该指令会Fork出一个子进程来建立RDB文件,不阻塞服务器进程,子进程接收请求并建立RDB快照,父进程继续接收客户端的请求。子进程在完成文件的建立时会向父进程发送信号,父进程在接收客户端请求的过程当中,在必定的时间间隔经过轮询来接收子进程的信号。咱们也能够经过使用lastsave指令来查看bgsave是否执行成功,lastsave能够返回最后一次执行成功bgsave的时间。

    • 自动化触发RDB持久化的方式

      1.根据redis.conf配置里的SAVE m n 定时触发(实际上使用的是BGSAVE)
      2.主从复制时,主节点自动触发。
      3.执行Debug Reload
      4.执行Shutdown且没有开启AOF持久化。

    • BGSAVE的原理

      启动
      1.检查是否存在子进程正在执行AOF或者RDB的持久化任务。若是有则返回false。
      2.调用Redis源码中的rdbSaveBackground方法,方法中执行fork()产生子进程执行rdb操做。

      rdb原理
      3.关于fork()中的Copy-On-Write
      fork()在linux中建立子进程采用Copy-On-Write(写时拷贝技术),即若是有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给调用者,而其它调用者所见到的最初的资源仍然保持不变

    • RDB持久化方式的缺点

      1.内存数据全量同步,数据量大的情况下,会因为I/O而严重影响性能。
      2.可能会由于Redis宕机而丢失从当前至最近一次快照期间的数据。

  • AOF(Append-Only-File)持久化:保存写状态

    AOF持久化是经过保存Redis的写状态来记录数据库的。相对RDB来讲,RDB持久化是经过备份数据库的状态来记录数据库,而AOF持久化是备份数据库接收到的指令。
    1.AOF记录除了查询之外的全部变动数据库状态的指令。
    2.以增量的形式追加保存到AOF文件中。

  • 开启AOF持久化

    1.打开redis.conf配置文件,将appendonly属性改成yes。
    2.修改appendfsync属性,该属性能够接收三种参数,分别是always,everysec,no,always表示老是即时将缓冲区内容写入AOF文件当中,everysec表示每隔一秒将缓冲区内容写入AOF文件,no表示将写入文件操做交由操做系统决定,通常来讲,操做系统考虑效率问题,会等待缓冲区被填满再将缓冲区数据写入AOF文件中。

    appendonly yes
    
      #appendsync always
      appendfsync everysec
      # appendfsync no
    复制代码
  • 日志重写解决AOF文件不断增大的问题

    随着写操做的不断增长,AOF文件会愈来愈大。假设递增一个计数器100次,若是使用RDB持久化方式,咱们只要保存最终结果100便可,而AOF持久化方式须要记录下这100次递增操做的指令,而事实上要恢复这条记录,只须要执行一条命令就行,因此那一百条命令实际能够精简为一条。Redis支持这样的功能,在不中断前台服务的状况下,能够重写AOF文件,一样使用到了COW(写时拷贝)。重写过程以下:
    1.调用fork(),建立一个子进程。
    2.子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件。
    3.主进程持续将新的变更同时写到内存和原来的AOF里。
    4.主进程获取子进程重写AOF的完成信号,往新AOF同步增量变更。
    5.使用新的AOF文件替换掉旧的AOF文件。

  • AOF和RDB的优缺点

    RDB优势:全量数据快照,文件小,恢复快。
    RDB缺点:没法保存最近一次快照以后的数据。
    AOF优势:可读性高,适合保存增量数据,数据不易丢失。
    AOF缺点:文件体积大,恢复时间长。

  • RDB-AOF混合持久化方式

    redis4.0以后推出了此种持久化方式,RDB做为全量备份,AOF做为增量备份,而且将此种方式做为默认方式使用。
    在上述两种方式中,RDB方式是将全量数据写入RDB文件,这样写入的特色是文件小,恢复快,但没法保存最近一次快照以后的数据,AOF则将redis指令存入文件中,这样又会形成文件体积大,恢复时间长等弱点。
    RDB-AOF方式下,持久化策略首先将缓存中数据以RDB方式全量写入文件,再将写入后新增的数据以AOF的方式追加在RDB数据的后面,在下一次作RDB持久化的时候将AOF的数据从新以RDB的形式写入文件。这种方式既能够提升读写和恢复效率,也能够减小文件大小,同时能够保证数据的完整性。在此种策略的持久化过程当中,子进程会经过管道从父进程读取增量数据,在以RDB格式保存全量数据时,也会经过管道读取数据,同时不会形成管道阻塞。能够说,在此种方式下的持久化文件,前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。此种方式是目前较为推荐的一种持久化方式。

Redis数据的恢复

  • RDB和AOF文件共存状况下的恢复流程

    RDB和AOF共存
    从图可知,Redis启动时会先检查AOF是否存在,若是AOF存在则直接加载AOF,若是不存在AOF,则直接加载RDB文件。

Pineline

Pipeline和Linux的管道相似,它可让Redis批量执行指令。
Redis基于请求/响应模型,单个请求处理须要一一应答。若是须要同时执行大量命令,则每条命令都须要等待上一条命令执行完毕后才能继续执行,这中间不只仅多了RTT,还屡次使用了系统IO。Pipeline因为能够批量执行指令,因此能够节省屡次IO和请求响应往返的时间。可是若是指令之间存在依赖关系,则建议分批发送指令。

Redis的同步机制

  • 主从同步原理

    Redis通常是使用一个Master节点来进行写操做,而若干个Slave节点进行读操做,Master和Slave分别表明了一个个不一样的RedisServer实例,另外按期的数据备份操做也是单独选择一个Slave去完成,这样能够最大程度发挥Redis的性能,为的是保证数据的弱一致性最终一致性。另外,Master和Slave的数据不是必定要即时同步的,可是在一段时间后Master和Slave的数据是趋于同步的,这就是最终一致性

    Redis主从同步

    • 全同步过程

      1.Slave发送sync命令到Master。
      2.Master启动一个后台进程,将Redis中的数据快照保存到文件中。
      3.Master将保存数据快照期间接收到的写命令缓存起来。
      4.Master完成写文件操做后,将该文件发送给Slave。
      5.使用新的AOF文件替换掉旧的AOF文件。
      6.Master将这期间收集的增量写命令发送给Slave端。
    • 增量同步过程

      1.Master接收到用户的操做指令,判断是否须要传播到Slave。
      2.将操做记录追加到AOF文件。
      3.将操做传播到其它Slave:1.对齐主从库;2.往响应缓存写入指令。
      4.将缓存中的数据发送给Slave。
  • Redis Sentinel(哨兵)

    主从模式弊端:当Master宕机后,Redis集群将不能对外提供写入操做。Redis Sentinel可解决这一问题。
    解决主从同步Master宕机后的主从切换问题
    1.监控:检查主从服务器是否运行正常。
    2.提醒:经过API向管理员或者其它应用程序发送故障通知。
    3.自动故障迁移:主从切换(在Master宕机后,将其中一个Slave转为Master,其余的Slave从该节点同步数据)。

Redis集群

  • 原理:如何从海量数据里快速找到所需?

    • 分片

      按照某种规则去划分数据,分散存储在多个节点上。经过将数据分到多个Redis服务器上,来减轻单个Redis服务器的压力。

    • 一致性Hash算法

      既然要将数据进行分片,那么一般的作法就是获取节点的Hash值,而后根据节点数求模,但这样的方法有明显的弊端,当Redis节点数须要动态增长或减小的时候,会形成大量的Key没法被命中。因此Redis中引入了一致性Hash算法。该算法对2^32 取模,将Hash值空间组成虚拟的圆环,整个圆环按顺时针方向组织,每一个节点依次为0、一、2...2^32-1,以后将每一个服务器进行Hash运算,肯定服务器在这个Hash环上的地址,肯定了服务器地址后,对数据使用一样的Hash算法,将数据定位到特定的Redis服务器上。若是定位到的地方没有Redis服务器实例,则继续顺时针寻找,找到的第一台服务器即该数据最终的服务器位置。

      一致性Hash算法

  • Hash环的数据倾斜问题

    Hash环在服务器节点不多的时候,容易遇到服务器节点不均匀的问题,这会形成数据倾斜,数据倾斜指的是被缓存的对象大部分集中在Redis集群的其中一台或几台服务器上。

    数据倾斜
    如上图,一致性Hash算法运算后的数据大部分被存放在A节点上,而B节点只存放了少许的数据,长此以往A节点将被撑爆。
    针对这一问题,能够引入虚拟节点解决。简单地说,就是为每个服务器节点计算多个Hash,每一个计算结果位置都放置一个此服务器节点,称为虚拟节点,能够在服务器IP或者主机名后放置一个编号实现。
    虚拟节点
    例如上图:将NodeA和NodeB两个节点分为Node A#1-A#3 NodeB#1-B#3。

结语

这篇准(tou)备(lan)了至关久的时间,由于有些东西总感受本身拿不许不敢往上写,差点自闭,就算如今发出来了也感受有不少地方是须要改动的。若是有同窗以为哪里写的不对劲的,评论区或者私聊我...嗯,我不要你以为,我要我以为。

本文图片来自网络,侵删。

欢迎你们访问个人我的博客:Object's Blog

相关文章
相关标签/搜索