一文读懂Redis

Redis与NoSQL概述

Nosql的优势

  • 使用nosql解决cpu与内存压力
  • 使用nosql解决I/O压力

Nosql数据库的概述

  • NoSql= Not Only SQL
  • 采用key-value模式存储
  • 不遵循SQL标准
  • 性能远超过SQL

使用场景

  • 数据的高并发读写
  • 海量数据读写
  • 数据可扩展性

Redis概述

  • 开源的key-value系统
  • 支持String、List、Set、zset、hash等数据类型
  • 数据库支持push/pop/add/remove操作
  • 支持不同方式的排序
  • 可写入内存也可以持久化
  • 主从同步功能

Redis基本数据类型

首先对redis来说,所有的key(键)都是字符串。
我们在谈基础数据结构时,讨论的是存储值的数据类型。

  1. String
    字符串,一个key对应一个value,是二进制安全的,是Redis最基本的数据类型。value最多512M,底层为动态字符串,ArrayList

  2. List
    单键多值的字符串列表,可以按照插入顺序排序,底层为双向链表(zipList(数据少时) -> quickList(数据多时))

  3. Set
    String类型的无序集合,底层其实是一个value为null的hash表(Redis中的集合都是通过哈希表实现的,所以添加,删除,查找的复杂度都是o(1)),value自动排重且无序

  4. Hash
    String类型的field和value的映射表,hash适合用来存储对象。类似java中Map<String,Object>,底层为zipList(数据量少)或 hashtable(数据量较多)

  5. Zset
    有序的,类似set,没有重复元素,关联了score并可以进行排序,底层架构类似Map<String,value>,Zset底层为hash以及跳跃表

Redis特殊数据类型

  1. Bitmaps(位存储)
    实现对字符串的位的操作的字符串。是一个以位为单元的数组,数组每个单元只能存储0与1,下标与偏移量,与set相比节省空间

  2. HyperLogLoog(基数统计)
    适用于独立IP数、搜索记录等需要去重和计数时。

  3. Geospatial(地理位置)
    redis3.2后增加GEO类型,即地理信息的缩写,提供了经纬度的设置、查询、范围查询、举例查询、经纬度hash等

redisObjet

Redis的每种对象其实都由对象结构(redisObject)对应编码的数据结构组合而成,而每种对象类型若干编码方式,不同的编码方式所对应的底层数据结构是不同的。
image

为什么Redis会设计redisObject对象???

在redis的命令中,用于对键进行处理的命令占了很大一部分,而对于键所保存的值的类型(键的类型),键能执行的命令又各不相同。
如:LPUSH 和 LLEN 只能用于列表键, 而 SADD 和 SRANDMEMBER 只能用于集合键, 等等; 另外一些命令, 比如 DEL、 TTL 和 TYPE, 可以用于任何类型的键;
但是要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样。

Redis必须让每个键都带有类型信息,使得程序可以检查键的类型,并为它选择合适的处理方式。

比如说,集合类型就可以由字典和整数集合两种不同的数据结构实现,但是,当用户执行 ZADD 命令时, 他/她应该不必关心集合使用的是什么编码,只要Redis能按照ZADD命令的指示,将新元素添加到集合就可以了。
这说明操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理

为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:

  • redisObject 对象
  • 基于 redisObject 对象的类型检查
  • 基于 redisObject 对象的显式多态函数
  • 对 redisObject 进行分配、共享和销毁的机制

redisObject数据结构

redisObject 是 Redis 类型系统的核心, 数据库中的每个键、值, 以及 Redis 本身处理的参数, 都表示为这种数据类型.
其中type、encoding和ptr是最重要的三个属性
type记录了对象所保存的值的类型(字符串、列表、集合、有序集、哈希表)
encoding记录了对象所保存的值的编码(ZIPMAP、ZIPLIST、QUICKLIST等)
ptr是一个指针,指向实际保存值的数据结构
lru属性记录了对象最后一次被命令程序访问的时间

redis如何处理一条命令

当执行一个处理数据类型命令的时候,redis执行以下步骤

  1. 根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;

  2. 检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;

  3. 根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;

  4. 返回数据结构的操作结果作为命令的返回值。
    image

Redis底层数据结构

简单动态字符串-sds
压缩列表-ZipList
快表-QuickList
字典/哈希表-Dict
整数集-IntSet
跳表-ZSipList

sds

redis的字符串表示为sds,而不是C字符串(以\0结尾的char*), 它是Redis 底层所使用的字符串表示,它被用在几乎所有的Redis 模块中。
image

一般来说,SDS 除了保存数据库中的字符串值以外,SDS 还可以作为缓冲区(buffer):包括 AOF 模块中的AOF缓冲区以及客户端状态中的输入缓冲区。

ZipList

ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。
他能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关。

QuickList

它是一种以ziplist为结点的双端链表结构. 宏观上, quicklist是一个链表, 微观上, 链表中的每个结点都是一个ziplist。

字典/哈希表 - Dict

本质上就是哈希表

整数集 - IntSet

整数集合(intset)是集合类型的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。

跳表 - ZSkipList

跳跃表结构在 Redis 中的运用场景只有一个,那就是作为有序列表 (Zset) 的使用。
跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这就是跳跃表的长处。
跳跃表的缺点就是需要的存储空间比较大,属于利用空间来换取时间的数据结构。

skiplist与平衡树、哈希表的比较

skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。

在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。

平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。

从算法实现难度上来比较,skiplist比平衡树要简单得多。

Redis的事务操作

Redis事务定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Multi、Exec、Discard、Watch、UnWatch

输入Multi命令开始:输入的命令都会一次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。

WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
UNWATCH:取消WATCH对所有key的监视。

为什么 Redis 不支持回滚?

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

Redis事务特性:

事务的ACID查看博文从本地事务到分布式事务,浅谈事务的分布式一致性算法。

  1. 单独的隔离操作
    事务中的所有命令都会序列化、按顺序执行,事务在执行的过程中,不会被其他客户端发送来的命令所打断。
  2. 没有隔离级别的概念
    队列中的命令没有提交之前不会实际被执行,因为事务提交前任何执行都不会被实际执行
  3. 不保证原子性(???)
    事务中如果有一条命令执行失败,其中的命令仍然会被执行,没有回滚。
    运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。
    Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。
  4. 不保证持久性
    redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

Redis持久化

Redis是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。
通常的解决方案是从后端数据库恢复这些数据,但后端数据库有性能瓶颈,如果是大数据量的恢复,首先会对数据库带来巨大的压力,其次数据库的性能不如Redis。导致程序响应慢。
所以对Redis来说,实现数据的持久化,避免从后端数据库中恢复数据,是至关重要的。

两种持久化方式

  1. RDB(Redis DataBase):内存中数据直接写入文件中
  2. AOF(Append Of File):以追加的行为把内容写入文件中

RDB

  1. 概念
    指定的时间间隔内讲内存中的数据集快照写入磁盘,它恢复时可以将快照文件直接读到内存里

  2. 触发RDB持久化
    触发rdb持久化的方式有两种,分别是手动触发和自动触发

    • 手动触发
      save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用
      bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
      具体流程如下:
    1. redis客户端执行bgsave命令或者自动触发bgsave命令;
    2. 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
    3. 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
    4. 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;
    5. 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)。
    • 自动触发
      在以下四种情况时会自动触发
    1. redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;
    2. 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;
    3. 执行debug reload命令重新加载redis时也会触发bgsave操作;
    4. 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;
  3. Q&A

    1、由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢?

    RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。
    在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情,这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。
    另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。

    2、在进行快照操作的这段时间,如果发生服务崩溃怎么办?
    很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。
    如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。
    Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。

    3、 可以每秒做一次快照吗?
    如果频繁地执行全量快照,也会带来两方面的开销:
    一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
    另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。

  4. RDB优缺点
    优点

  • RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;

  • Redis加载RDB文件恢复数据要远远快于AOF方式;

    缺点

  • RDB方式实时性不够,无法做到秒级的持久化;

  • 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;

  • RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;

  • 版本兼容RDB文件问题;

AOF

  1. 概念
    以日志形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数。

  2. 写后日志
    AOF日志采用写后日志,即先写内存,后写日志。

为什么采用写后日志?
Redis要求高性能,采用写日志有两方面好处:

  • 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
  • 不会阻塞当前的写操作
    但这种方式存在潜在风险:
  • 如果命令执行完成,写日志之前宕机了,会丢失数据。
  • 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。
  1. 如何实现AOF
    AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。
  • 命令追加
    当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
  • 文件写入和同步
    关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:
    Always同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    Everysec每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    No操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

三种写回策略的优缺点
上面的三种写回策略体现了一个重要原则:trade-off,取舍,指在性能和可靠性保证之间做取舍。

  1. Q&A
  • AOF重写会阻塞吗?

AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
所以aof在重写时,在fork进程时是会阻塞住主线程的。

  • AOF日志何时会重写?

有两个配置项控制AOF重写的触发:
auto-aof-rewrite-min-size:表示运行AOF重写时文件的最小大小,默认为64MB。
auto-aof-rewrite-percentage:这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值,再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。

  • 重写日志时,有新数据写入咋整?

重写过程总结为:“一个拷贝,两处日志”。
在fork出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个aof日志内存缓冲区中。
如果AOF写回策略配置的是always,则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件)

在bgrewriteaof子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过Linux管道技术让aof重写期间就能同时进行回放,这样aof重写结束后只需回放少量剩余的数据即可。

最后通过修改文件名的方式,保证文件切换的原子性。

在AOF重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。

总结操作:
1. 主线程fork出子进程重写aof日志
2. 子进程重写日志完成后,主线程追加aof日志缓冲
3. 替换日志文件

  • 主线程fork出子进程的是如何复制内存数据的?

fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。
fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量,数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。

  • 在重写日志整个过程时,主线程有哪些地方会被阻塞?
  1. fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。
  2. 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。
  3. 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞。
  • 为什么AOF重写不复用原AOF日志

两方面原因:

  1. 父子进程写同一个文件会产生竞争问题,影响父进程的性能。
  2. 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用

Redis高可用

主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
主从复制的作用主要包括:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从库之间采用的是读写分离的方式。

  • 读操作:主库、从库都可以接收;
  • 写操作:首先到主库执行,然后,主库将写操作同步给从库。

主从复制原理

全量复制
全量复制的三个阶段如下:
第一阶段,主从库间建立连接、协商同步
第二阶段,主库将所有数据同步给从库
第三阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库

增量复制
为什么会设计增量复制?
如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

replication buffer 和 repl_backlog_buffer
repl_backlog_buffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。而在repl_backlog_buffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。

replication buffer:Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。

Q&A

  1. 如果在网络断开期间,repl_backlog_size环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?
    对于这个问题来说,有两个关键点:

    1. 一个从库如果和主库断连时间过长,造成它在主库repl_backlog_buffer的slave_repl_offset位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
    2. 每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。
  2. 为什么主从全量复制使用RDB而不使用AOF?

    1. RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量复制的成本最低。
    2. 假设要使用AOF做全量复制,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。

哨兵机制

Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移。

哨兵实现了什么功能呢?下面是Redis官方文档的描述:
监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
通知(Notification):哨兵可以将故障转移的结果发送给客户端。

哨兵集群的选举

为什么必然会出现选举/共识机制?
为了避免哨兵的单点情况发生,所以需要一个哨兵的分布式集群。作为分布式集群,必然涉及共识问题(即选举问题);同时故障的转移和通知都只需要一个主的哨兵节点就可以了。

哨兵的选举机制是什么样的?
哨兵的选举机制其实很简单,就是一个Raft选举算法: 选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举
Raft选举细节查看博文从本地事务到分布式事务,浅谈事务的分布式一致性算法。

Redis应用问题解决

缓存穿透

  1. 现象
  • 应用服务器压力变大

  • redis命中率降低

  • 一直查询数据库

  1. 造成原因
  • redis查询不到数据库

  • 出现很多非正常url访问

  1. 解决方案
  • 对空值进行缓存:缓存空结果null值
  • 设置访问白名单:使用bitmaps类型定义一个可以访问的名单,每次访问时进行拦截
  • 布隆过滤器:(Bloom Filter)1970年布隆提出的,它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器用于检索一个元素是否在一个集合,但是也存在误识别的情况
  • 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,设置黑名单

缓存击穿

  1. 现象
    数据库的访问压力瞬时增加、redis里面没有出现大量key过期、redis正常运行

  2. 造成原因
    redis某个key过期了,大量访问使用这个key

  3. 解决方案

  • 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis内,加大这些热门数据key的时长
  • 实时调整:现场监控哪些数据热门,实时调整key的过期时长
  • 使用锁的方式:设置排它锁:在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠一段时间后重试

缓存雪崩

  1. 现象
    数据库压力变大、服务器崩溃

  2. 造成原因
    在极少的时间段,查询大量key的集中过期情况

  3. 解决方案

  • 构建多级缓存架构:nginx缓存+redis缓存+其他缓存(ehcache等)

  • 使用锁或队列:用加锁或者队列保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量并发请求落到底层存储系统上,不适用于高并发情况

  • 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台更新实际key的缓存

  • 将缓存失效时间分散开:可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,很难引发集体失效的事件

缓存污染

  1. 现象
    缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。

    缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。
    这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。

    缓存污染(缓存写满)是不可避免的, 所以需要数据淘汰策略。

缓存淘汰策略

Redis共支持八种淘汰策略:

  • 不淘汰

    • noeviction (v4.0后默认的)
      该策略是Redis的默认策略。在这种策略下,一旦缓存被写满了,再有写请求来时,Redis不再提供服务,而是直接返回错误。
      这种策略不会淘汰数据,所以无法解决缓存污染问题。一般生产环境不建议使用。
  • 对设置了过期时间的数据中进行淘汰

    • 随机:volatile-random
      在设置了过期时间的键值对中,进行随机删除。
      因为是随机删除,无法把不再访问的数据筛选出来,所以可能依然会存在缓存污染现象,无法解决缓存污染问题。

    • ttl:volatile-ttl
      这种算法判断淘汰数据时参考的指标比随机删除时多进行一步过期时间的排序。Redis在筛选需删除的数据时,越早过期的数据越优先被选择。

    • lru:volatile-lru
      LRU算法
      LRU算法全称是Least Recently Used,按照最近最少使用的原则来筛选数据。这种模式下会使用LRU算法筛选设置了过期时间的键值对。
      Redis优化的LRU算法实现
      Redis会记录每个数据的最近一次被访问的时间戳。
      在Redis在决定淘汰的数据时,第一次会随机选出N个数据,把它们作为一个候选集合。
      接下来,Redis会比较这N个数据的lru字段,把lru字段值最小的数据从缓存中淘汰出去。
      通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能。

    • lfu:volatile-lfu
      会使用LFU算法选择设置了过期时间的键值对。
      LFU算法
      LFU缓存策略是在LRU策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。
      如果两个数据的访问次数相同,LFU策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
      Redis的LFU算法实现
      当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。
      Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样在访问快速的情况下,如果每次被访问就将访问次数加一,很快某条数据就达到最大值255,可能很多数据都是255,那么退化成LRU算法了。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可以通过配置项,来控制计数器增加的速度。

  • 全部数据进行淘汰

    • 随机:allkeys-random
      从所有键值对中随机选择并删除数据
    • lru:allkeys-lru
      使用LRU算法在所有数据中进行筛选
    • lfu:allkeys-lfu
      使用LFU算法在所有数据中进行筛选
posted @ 2021-09-25 16:46  cos晓风残月  阅读(73)  评论(0编辑  收藏  举报