Redis

介绍

​ Redis是一个基于内存且支持持久化的以key-value键值对形式存储的NoSQL数据库。他可以支持很多的数据类型例如字符串(string)、散列(hashe)、列表(list)、集合(set)、有序集合(sorted sets)等等。Redis可以用作数据库、缓存、消息中间件等等。不过Redis通常我们用的更多的是让他作为一个缓存数据库用于对数据进行缓存

数据类型

常用的数据类型分别是:

  • string
    • string 是最基本的类型,而且 string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如数值、 jpg 图片或者序列化的对象。
  • hash
    • hash是一个键值(key-value)的集合。他是一个string的key和value的映射表。hash特别适合存储对象。相对于将对象的每个字段存成单个 string 类型。将一个对象存储在 hash 类型中会占用更少的内存,并且可以更方便的存取整个对象
  • list
    • list是一个有序集合,通过双向链表实现。他会按照插入顺序排序,并且他可以从头部或尾部添加数据、弹出数据以及获取列表中的某段数据。所以可以用作消息队列
  • set
    • set是一个无序集合,通过hash表实现。set中的元素是没有顺序的,而且不会重复,所以set中的元素会自动去重
  • sorted set
    • sorted set 是有序集合,它在 set 的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,指定后,会自动重新按新的值调整顺序。

持久化

Redis的持久化策略有两种,分别是:

  • RDB:他会把内存中的数据全部写入到磁盘的二进制文件(dump.rdb)中。他是全量持久化。默认使用RDB的方式进行持久化
    • 当Redis进行RDB持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时文件中。当子进程写完后,会将原来的二进制文件替换掉。
    • 在执行fork的时候操作系统会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令 ),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的RDB文件存储的是执行fork那一刻的内存数据。
    • 优点:
      • 通过RDB的方式整个Redis数据库将只包含一个文件,所以我们可以在指定时间段让数据库创建备份,这样当出现问题可以快速回滚到某个版本(可以通过服务器的定时任务来将备份文件复制到某个位置从而进行数据的冷备份)。并且可以将备份文件转移到其它存储介质上,用于灾难恢复
      • 通过RDB恢复数据的方式远远快于AOF的方式
    • 缺点:
      • RDB的方式没办法做到实时持久化,所以当出现服务器故障的时候,就会丢失某段时间的数据
  • AOF:他会把所有的对Redis的写命令都存到一个文件中。他是增量持久化。当 redis 重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。默认不开启,所以需要手动开启(配置文件添加appendfsync yes
    • 由于AOF是追加操作日志,所以他是可以做到实时持久化的。我们可以通过配置不同的同步策略来控制缓冲区中命令同步到AOF文件的频率。同步的策略由appendfsync参数控制,他的取值有三个
      • no:AOF文件的同步由操作系统控制。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证
      • always:命令写入缓冲区后立即同步到AOF文件。这种情况下安全性最高。不过由于写操作都要进行IO操作,所以会影响写操作的性能
      • everysec:每秒同步一次,这个相当于是一种折中的方法。不过这种方式如果服务器发生故障会丢失1秒的数据
    • 如果AOF的文件太大,我们可以开启文件重写。来减少AOF文件的体积。在重写的时候他会让过期的数据以及无效的命令不再写入文件,并且会将多个命令合并为一个。在重写时父进程也会fork一个子进程来执行重写的操作。重写会生成新的AOF文件,最后会将新的AOF文件替换旧的文件
    • 当AOF开启时,Redis启动时会优先加载AOF文件来恢复数据。只有当AOF关闭时,才会载入RDB文件恢复数据。

AOF需要指定同步的频率是因为:为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;

持久化策略选择

  • 如果redis仅仅用于做关系型数据库的缓存或者Redis中的数据并不重要那么可以不用开启持久化
  • 如果可以允许数据丢失十几分钟,那么可以使用RDB。如果值接受秒级的数据丢失或不能接受数据丢失那么就需要使用AOF。当然也可以两种都开启

主从复制

介绍

Redis默认是支持主从复制的。通过主从复制我们会将数据复制到不同的数据库中,从而可以实现数据的热备份、故障恢复、读写分离等等。

开启主从复制很简单,只需要在从节点中通过slaveof <masterip> <masterport>指定主节点的信息就可以了,主节点并不需要配置什么。建立主从关系后可以通过slaveof no one断开。断开后从节点不会删除已有的数据,只是不再接受主节点新的数据变化。

主从复制的过程

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段;

  • 连接建立阶段
    • 从节点执行slaveof <masterIP> <masterPort>后会保存主节点的信息,然后再根据主节点的ip和port跟主节点建立socket连接。之后从节点发送Ping信号,主节点返回Pong,以此来确认两边能互相通信。
  • 数据同步阶段
    • 主从建立连接后就可以开始进行同步工作了,这个阶段相当于从节点的数据初始化。
    • 在同步的时候从节点会向主节点发送psync [runId] [offset]命令(Redis2.8以前是sync命令)进行同步。他们的区别在于,sync命令仅支持全量复制过程,psync支持全量和部分复制
    • 在初次复制或无法进行部分复制的情况下就会使用全量复制,全量复制是一个非常重型的操作。在全量复制中,主节点会fork一个子进程进行RDB持久化,然后将RDB文件发送给从节点,之后从节点在重新载入RDB文件
    • 由于全量复制数据量太大导致效率低下,所以在Redis2.8就提供了部分复制用于处理网络中断时的数据同步。在部分复制时会涉及到3个概念分别是:
      • 复制偏移量(offset):主节点和从节点会分别维护一个复制偏移量(offset),用于表示主节点向从节点传递的字节数,每次同步数据时都会增加对应的offset。通过offset可以判断主从节点的数据库状态是否一致。
      • 复制积压缓冲区:复制积压缓冲区是由主节点维护的、他的长度固定是一个先进先出(FIFO)的队列,默认大小1MB。当主节点有从节点时创建。他用于记录主节点最近执行过的命令以及每个命令对应的offset,由于他是先进先出固定长度的队列,所以旧的命令会随着命令执行被丢弃。同步数据时从节点会将offset发送给主节点后,主节点会根据offset和缓冲区大小决定能否执行部分复制。如果能够在复制积压缓冲区里找到从节点发过来的offset偏移量,表名从节点需要同步的数据并不多,所以执行部分复制;否则执行全量复制。如果需要提高使用部分复制的概率我们可以通过配置(repl-backlog-size)来增大复制积压缓冲区的大小
      • 服务器运行ID(runid):每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样)。主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制。如果从节点发过来的runid跟当前主节点的runid一致则说明之前同步过,然后根据复制偏移量和复制积压缓冲区判断是否可以进行部分复制。如果runid不一致,则说明之前同步的主节点不是当前主节点,所以需要进行全量复制
  • 命令传播阶段
    • 数据同步完成后主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。并且在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制,用于判断节点是否出现超时,以及命令是否存在丢失,每隔一段时间,主节点会向从节点发送ping命令,而从节点会向主节点发送replconf ack 命令并携带offset。因为在维护心跳的时候从节点会发送了自身的offset,主节点会与自己的offset对比。如果不一致就会通过offset在复制积压缓冲区中找到对应的缺失数据进行部分复制
    • 主从之间命令传播是异步的,即主节点发送写命令后并不会等待从节点的回复。所以从节点的数据就可能会出现延迟。不过这种延迟通常不会很长,如果业务可以接受那么不用处理。如果必须要要求主从强一致,目前并没有找到很好的解决方案,如果非要强一致可以直接读主库。
      • 或者通过min-slaves-max-lag设置最大的延迟时间(单位秒),数据复制和同步时延迟了超过指定时间,那么master就不会再接客户端的请求了,这样就会有效减少大量数据丢失的发生

哨兵机制

使用主从复制之后,如果主节点发生故障。那么我们需要手动的将某个从节点提升为主节点,然后让所有的从节点去同步新的主节点。同时需要修改应用方主节点的地址。这一系列的操作还是挺麻烦的,所以Redis提供了哨兵机制(Redis Sentinel)来处理这个问题。

哨兵提供的功能包括:

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将主从关系中的其中一个从节点升级为新的主节点,并让其他从节点改为同步新的主节点。这样就避免了人工干预
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
    • 注意这里并不是代理,而是提供配置,所以客户端会通过哨兵来获取redis主节点的信息然后再自己去连接主节点。并不是由哨兵代理连接
  • 通知(Notification):当发生故障转移时,哨兵会将故障转移的结果发送给客户端。从而可以让客户端知道新的主节点

哨兵节点其实是一个特殊的redis节点,并且在哨兵节点中我们只需要指定所监控的主节点就可以了,他会自动发现其他的哨兵节点和从节点。

基本原理

  • 定时任务:每个哨兵节点维护了3个定时任务。定时任务的功能包括:通过向主从节点发送info命令获取最新的主从结构;通过发布订阅功能获取其他哨兵节点的信息;通过向其他节点发送ping命令进行心跳检测,判断是否下线。
  • 主观下线:在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。主观下线的意思是一个哨兵节点“主观地”判断这个节点下线。此时并不一定会真正的下线
  • 客观下线:哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。否则主观下线状态会被移除
    • 需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。
  • 选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者哨兵节点对其进行故障转移操作。
  • 故障转移:在故障转移操作中,会根据规则选择新的主节点,然后再让其他节点成为这个主节点的从节点,并且下线后的主节点也会成为新的主节点的从节点,最后在通知客户端故障转移的结果

Redis集群

虽然主从复制和哨兵机制可以实现读写分离以及高可用,不过无法实现写的负载均衡,并且存储能力受单机存储的限制。为了处理这种问题我们就需要使用Redis集群。

Redis在3.0的版本开启引入集群。集群由多个节点(Node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。他们之间就是主从关系,并且还支持主节点的自动故障转移(与哨兵类似)。当任意主节点发生故障时,集群仍然可以对外提供服务。

搭建一个集群分为四步:

  • 启动节点:将节点以集群模式启动,此时节点是独立的,并没有建立联系
  • 节点握手:让独立的节点连成一个网络
  • 分配槽:将16384个槽分配给各个主节点
  • 指定主从关系:为从节点指定主节点

其中主要的就是数据的分配,在Redis集群中采用带虚拟节点的一致性hash进行数据的分配。而虚拟节点在redis集群中称为槽。每个实际节点包含一定数量的槽。而每个槽包含一定范围内的hash值。存储数据时只需要计算数据的hash值然后根据一致性hash找到到对应的槽,然后再根据槽与节点的映射关系找到具体的节点,最后将数据存储到对应的节点中。使用了槽的一致性哈希中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对数据的影响很小,因为增删节点的时候只需要将部分槽重新分配给节点即可,不会影响其他节点的数据产生很大的影响

客户端访问集群

  • 当使用redis-cli这一类客户端访问时,请求会被某个节点接收,然后节点在判断当前请求的key所对应的槽是否在当前节点中,如果在则直接在当前节点执行命令并返回结果。如果不在则通过请求的key获取对应槽所在节点的地址并包装到MOVED错误中返回给redis-cli。redis-cli收到MOVED错误后,根据返回的地址重新发送请求
  • 但是用应用程序客户端访问时,例如JedisCluster访问时。应用程序客户端中初始化时,会在内部维护slot(槽)和node(节点)对应关系的缓存。槽和节点的关系通过cluster slots命令获取。并且会为每个节点创建连接池。向redis集群请求的时候,直接通过key获取对应的slot,然后再通过slot获取对应的node,最后直接对node进行请求

一致性hash

对于数据的分区方案有很多中,例如顺序分区、hash分区。其中hash分区具有很好的随机性,所以需要将数据分散的时候是很常用的。hash分区的基本思路是:对数据的特征值(如key)进行hash,然后根据hash值决定数据落在哪个节点。常见的hash分区包括:hash取模、一致性hash、带虚拟节点的一致性hash等。

  • hash取模
    • hash取模的思路很简单就是对数据进行hash计算后在跟分区数取模。从而得到数据应该映射到那个分区
  • 一致性hash
    • 一致性hash是为了解决hash取模存在的问题。当使用hash取模的时候,如果新增或删除分区,那么所有的数据都得重新计算映射关系。
    • 在一致性hash中。他会将所有的hash值组成一个虚拟的圆环(范围为:0 - 2^32-1)。然后对每个分区计算其hash值,这些hash值会分布在圆环中。存储数据的时候对每个数据计算hash值,然后确定hash值在圆环的位置,之后沿着圆环顺时针走。找到的第一个分区,就是当前值应该映射到的分区。
    • 这样当新增或删除分区的时候只会影响相邻的分区数据。不会对其他分区数据造成影响
  • 带虚拟节点的一致性hash
    • 带虚拟节点的一致性hash用于解决一致性hash存在的问题。因为在一致性hash中如果通过分区计算的hash值分布并不均匀。例如某个分区就占了一半圆环,那么就会导致分区中数据分配不均匀。
    • 为了让分区在圆环中分配均匀,所以就引入了虚拟节点。这些虚拟节点均匀的分布在圆环上。然后每个分区跟某些虚拟节点对应(分区对应的虚拟节点在环上的分布是被打散了的)。这样在存储数据的时候只需要根据数据的hash值在圆环中找到第一个虚拟节点然后再根据这个虚拟节点找到对应的分区,那么就可以获取到当前值应该分配在那个分区中了。这样就解决了一致性hash数据可能分配不均的问题

Redis事务

默认情况下当我们向Redis发送一条命令后,Redis会立即执行这条命令。除了这种情况redis还允许我们执行一组命令,当需要执行一组命令的时候就需要用到事务,这样才能保证这一组命令正确执行。不过事务一般用的不多因为需要事务的场景很少,通常一次只执行一条命令

在Redis中一个事务包括开始事务、命令入队、执行事务三个阶段。开始事务通过multi命令来开启,执行multi命令后,后续的命令不会被直接执行而是会被放入队列中。任务入队后我们可以通过exec命令执行事务。当然也可以通过discard命令取消事务。

当命令入队时发生错误,例如语法错误(执行不存在的命令或命令语法写错了),那么执行事务时会直接报错,然后取消事务。当错误发生在事务运行阶段(例如,对某个非数值执行incr自增),那么错误的命令会直接跳过,其他的命令依旧正常执行。并且在执行事务前我们可以通过watch命令监视某个key。如果在事务执行时这个key被其他客户端修改了那么就会报错并取消事务。

Redis存在的问题

缓存和数据库的一致性问题

一致性问题是分布式常见问题,可以再分为最终一致性和强一致性。由于数据库和缓存无法同时操作,所以就会出现一致性问题。

如果要求最终一致,我们通常的做法是先更新数据库,然后删除缓存。如果删除缓存失败,那么过一段时间进行重试。或将更新数据库和删除缓存的操作放在一个事务中,如果删除缓存失败回滚事务并提示异常。

如果要求强一致,那么可以考虑直接从数据库中读取,不使用缓存。如果不直接从数据库中读取需要通过加锁实现,这样性能可能更差。

缓存穿透

缓存穿透就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,从而导致数据库连接异常。当受到外部恶意攻击的时候就可能会出现这个问题。不过对于后台系统基本不存在这个问题,因为你得登录了之后才能访问呀。都不登录没法攻击,如果登录被攻破了,谁还在意你的缓存

解决方法是:

  • 缓存无效的key,下次请求直接返回。当然如果每次请求时的key不同,那么这个方法没什么作用
  • 添加过滤器,例如BloomFilter(布隆过滤器)。
    • 布隆过滤器由位数组和一系列散列函数组成。初始状态位数组中所有的值都是0。当有变量被加入位数组时,会将通过X个散列函数将这个变量映射成位数组中的 X 个点。并把他们的值改为1。检索的时候只需要根据检索值在执行X个散列函数,然后找到位数组中对应的点,然后看这些位置的值是否为1就可以了。如果为1则被检索值可能存在,否则一定不存在

缓存击穿

缓存击穿就是,当某个key是一个请求非常频繁的数据。当这个key失效的时候,那么大量并发请求就会直接请求到数据库中,这样就在这个key上击穿了缓存。

解决方法是:

  • 对于请求非常频繁的数据可以设置永不过期,当更新数据的时候直接更新缓存即可
  • 对数据的查询操作加互斥锁,这样同一时刻就只能有一个请求去读取数据库了,之后在结果缓存起来,从而避免对数据库的影响。

缓存雪崩

缓存雪崩,就是缓存在同一时间大面积的失效,这个时候当请求来的时候就会都请求到数据库上,从而导致数据库连接异常。

解决方案是:

  • 给缓存的失效时间,加上一个随机值,避免集体失效
  • 设置热点数据永不过期,当更新数据的时候直接更新缓存即可

缓存雪崩和缓存击穿的区别是,缓存雪崩是由于大量的key失效导致的问题,而缓存击穿是由于某个热点key失效导致的问题

面试题

  • 单线程的redis为什么这么快

    • 因为他是纯内存操作所以操作的时候会很快。由于他是基于内存操作的,所以CPU 不会成为 Redis 的瓶颈(Redis 的瓶颈最有可能是机器内存的大小或者网络带宽)。既然CPU不会成为瓶颈所以Redis就采用了单线程的方式进行读写操作。这样可以避免上下文切换引起的 CPU 消耗,以及线程之间锁的问题,从而提高效率。并且Redis 采用 I/O 多路复用机制,并发处理连接。
      • 多路复用机制。简单来说就是很多网络连接(多路),共(复)用少数几个(甚至是一个)线程。连接很多的时候,不能每个连接都创建一个线程为其服务,这样会耗尽系统内存。线程也不能阻塞在任何一个连接上,等新的数据来,这样就不能及时响应其他连接发来的数据了;也不能用非阻塞方式,轮询所有的连接,这会浪费掉大量CPU时间;只能告诉系统,我对哪些连接感兴趣,有消息来的时候,通知我处理,通过事件进行驱动。这样Redis就可以同时和多个客户端连接并处理请求,从而提升并发性。
    • Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。
  • redis的过期策略以及内存淘汰机制

    • 过期策略,redis采用的是定期删除+惰性删除策略。其中定期删除就是redis会每隔一段时间对库中的数据进行检查,如果发现有过期的数据就进行删除。不过检查的时候是随机抽取进行检查,如果全局扫描哪太影响效率了。由于是随机的检查所以可能会出现有些过期的数据没有检查到。所以就会用到惰性删除,所谓惰性删除就是当获取数据的时候,redis会对数据进行检查看是否过期了没有,如果过期了就直接删除。
      • 不用定时删除策略的原因:定时删除就是用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略。
    • 内存淘汰机制。使用定期删除+惰性删除的策略后,如果数据量很大,当定期删除没有删到,而且数据不怎么访问,那么惰性删除也没有起作用。这时就可能出现内存不足的情况。这个时候就需要用到redis的内存淘汰机制,在redis中我们可以通过maxmemory-policy来指定内存淘汰的策略。他的值包括:
      • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错
      • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key
      • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key
      • allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
      • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key
      • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
      • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
      • volatile-lfu:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最不经常使用的 key
      • allkeys的删除策略和volatile的删除策略的区别就是是否在设置了过期的key中操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值