Object's Blog

Redis由浅入深深深深深剖析

字数统计: 6.2k阅读时长: 22 min
2019/09/16 分享

前言

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

主流应用架构

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

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

  • Memcache:代码层类似Hash

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

  • Redis

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

为什么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)),不允许重复。
    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

      程序

      1
      2
      3
      4
      5
      6
      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融合在一起执行,具体做法如下。

      1
      SET KEY value [EX seconds] [PX milliseconds] [NX|XX]

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

      有了SET我们就可以在程序中使用类似下面的代码实现分布式锁了:

      1
      2
      3
      4
      5
      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模式的缺点
    1.消息的发布是无状态的,无法保证可达。对于发布者来说,消息是“即发即失”的,此时如果某个消费者在生产者发布消息时下线,重新上线之后,是无法接收该消息的,要解决该问题需要使用专业的消息队列,如kafka…此处不再赘述。

    Redis持久化

  • 什么是持久化

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

  • Redis如何做持久化

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

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

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

    1
    2
    3
    4
    5
    6
    7
    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文件中。

    1
    2
    3
    4
    5
    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、1、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

原文作者:Object

原文链接:http://blog.objectspace.cn/2019/09/16/Redis由浅入深深深深深剖析/

发表日期:2019 September 16th, 9:56:50 pm

更新日期:2019 November 5th, 8:10:11 pm

版权声明:未经作者授权请勿转载

目录
  1. 1. 前言
  2. 2. 主流应用架构
  3. 3. 缓存中间件 – Memcache和Redis的区别
    1. 3.1. Memcache:代码层类似Hash
    2. 3.2. Redis
  4. 4. 为什么Redis能这么快
  5. 5. Redis的数据类型
    1. 5.1. String
    2. 5.2. Hash
    3. 5.3. List
    4. 5.4. Set
    5. 5.5. Sorted Set
    6. 5.6. 更高级的Redis类型
  6. 6. 从海量Key里查询出某一个固定前缀的Key
    1. 6.1. 假设redis中有十亿条key,如何从这么多key中找到固定前缀的key?
      1. 6.1.1. 方法1:使用KEYS [pattern]:查找所有符合给定模式pattern的key
      2. 6.1.2. 方法2:使用SCAN cursor [MATCH pattern] [COUNT count]
  7. 7. 如何通过Redis实现分布式锁
    1. 7.1. 分布式锁
    2. 7.2. 如何使用redis实现分布式锁
      1. 7.2.1. 使用SETNX实现
  8. 8. 如何实现异步队列
    1. 8.1. 使用Redis中的List作为队列
    2. 8.2. 使用BLPOP key [key…] timeout
    3. 8.3. pub/sub:主题订阅者模式
  9. 9. Redis持久化
    1. 9.1. 什么是持久化
    2. 9.2. Redis如何做持久化
    3. 9.3. RDB(快照)持久化:保存某个时间点的全量数据快照
      1. 9.3.1. RDB的创建与载入
      2. 9.3.2. 自动化触发RDB持久化的方式
      3. 9.3.3. BGSAVE的原理
      4. 9.3.4. RDB持久化方式的缺点
    4. 9.4. AOF(Append-Only-File)持久化:保存写状态
    5. 9.5. 开启AOF持久化
    6. 9.6. 日志重写解决AOF文件不断增大的问题
    7. 9.7. AOF和RDB的优缺点
    8. 9.8. RDB-AOF混合持久化方式
  10. 10. Redis数据的恢复
    1. 10.1. RDB和AOF文件共存情况下的恢复流程
  11. 11. Pineline
  12. 12. Redis的同步机制
    1. 12.1. 主从同步原理
      1. 12.1.1. 全同步过程
      2. 12.1.2. 增量同步过程
    2. 12.2. Redis Sentinel(哨兵)
  13. 13. Redis集群
    1. 13.1. 原理:如何从海量数据里快速找到所需?
      1. 13.1.1. 分片
      2. 13.1.2. 一致性Hash算法
    2. 13.2. Hash环的数据倾斜问题
  14. 14. 结语