秋春招总结之Redis

文章目录

基础

什么是Redis

首先大体理解:Redis是一个基于内存,通过键值对(key-value)的形式来存储数据的NoSQL数据库。和其相相对应的是MySQL,一种SQL类型的数据库。

它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些[数据类型]都支持push/popadd/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。

在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中(所以也就避免了对硬盘的频繁操作)。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

为什么要使用Redis

前面提到了对于Redis来说一是支持的类型比较多,二是由于将信息存储在内存上,所以速度上会比SQL类型的数据库快上很多。

实际上,对于Redis来说读取速度能够达到110000次/s,写入的速度是81000次/s,能够满足我们更高频率的请求与承担更高的并发,在一些大型网站,也都会使用到redis进行数据的缓存处理,无论是采用到原生的数据类型(使用到redis自带的一些方法),或是封装成自己需要的缓存结构(进行二次的封装,这里可以参见我的博客Redis数据封装)ps:后续会进行修正与完善,在面对当下的高并发请求,都能够起到比较好的效果。

下面从两个具体的方面进行讲解:

缓存提高访问速度

一般我们对于存储在数据库中的数据的请求,都是前端页面发送请求信息,后端接收请求,并对数据进行封装和处理,最后再调用SQL语句,执行sql语句取到具体的信息。但是其实对于MySQL来说数据都是存储在硬盘上,进行数据的读取势必会消耗一定时间,但是计算机的运行速度比我们想象的要快很多,很多时候数据都是在极短的时间里面就能够查询出来,并返回给前端页面。

但是我们需要思考的是对于用户的每一次请求都需要从数据库中进行读取,当请求过于庞大时候,就会导致数据的读取和页面的相应不能够成正比,最后的结果就是,在一些特殊的情况下,根本就不能够在短时间内获取到信息。

对于Redis而言,主要就是读取速度快,而且数据类型丰富,所以就可以使用Redis来做缓存,主要过程见下图:这个时候,在下一次缓存还没过期时间内再此进行同样的请求就可以减少检索的时间,提高访问的速度。

image-20200728141537638

缓存支持高并发

缓存不仅能够提高访问的速度还有一点就是能够帮助我们减轻高并发情况下的数据库压力:

image-20200728142816623

一般用在什么地方(不考虑五种数据结构)

  1. 用作对MySQL数据库中数据的缓存,在请求到达时候,先判断缓存中是否有对应的数据,没有数据时候再请求到数据库中,然后将数据同步到Redis中,供下一次的请求到达时候,能够在数据不更改时候直接请求到Redis缓存,来减轻数据库的压力,例如对于某明星的具体个人信息,可以将其存取在缓存中,对于此类数据不需要每次都去请求数据库,可以将对数据库的访问留给跟紧急的数据。
  2. 在秒杀场景下,通过进行设置,将商品的信息预先加载到缓存中,通过异步加载的方式,先将数据库中商品的剩余数目加载出来,展示到页面上,同时也能够防止超卖的情况出现。
  3. 可以进行对整个页面的预先加载和缓存到Redis中,在用户进行访问时候,先将页面整体信息加载出来,然后加载数据。

数据类型与各自使用场景

数据类型

你只要说你看过Redis,或者是用过Redis,面试官一般都是问这个问题来检测你是只会数据的SetGet,还是对Redis的数据都有所了解与学习,所以在这个问题尽管看起来很简单,将有几种数据结构回答出来,然后各自的使用场景举例出来,但是要回答具体且对于有些特殊数据类型的底层实现进行了解和阐述还是有一定难度的,所以对于这个问题要重视起来。

首先是五种的数据类型: 包括StringListSetZsetHash。下面是具体情况的具体操作。

数据类型 底层的数据类型 可以存储的值 可以进行的操作 使用在哪些场景下面
String SDS(后续讲解) 字符串,整数,浮点数 对字符串或者其中的一部分进行系类的操作处理 使用最为频繁,一般就是用作简单的键值对的缓存处理作为key值1. 可以用于保存单个字符串或是JSON字符串类型。2. 因为string类型是二进制安全的所以可以把一个图片文件的内容 作为字符串来存储。3. 计算器进行使用时候,对微博数,粉丝数目进行增和减。4. increment 本身就具有原子操作的特性。
List 压缩列表 列表 1. 从两端压入或者弹出元素。2. 对单个或者多个元素进行修建,只保留一个返回内的元素 存储一些列表类型的数据结构。1. 消息队列 可以使用reids的lpush 与 brpop 来实现阻塞队列,保证负载均衡与高可用。2. 文章列表,每个用户都有数据自己的文章列表,需要分页展示文章列表,使用list 不但有序 还可以支持按照索引范围获取元素。最新消息排行等。
Set insert 无序不重复集合 添加、获取、移除单个元素检查一个元素是否存在于集合中计算交集、并集、差集从集合里面随机获取元素 1, 对于两个集合之间的数据进行交集,并集差集运算等。 2. 方便实现对于共同好友的采集,共同关注,二度好友等,对于以上说的集合操作,还可以使用不同的命令将结果返回给客户端或则是存放在一个新的集合里面。 3. 利用唯一性, 统计访问网站的所有独立IP信息。
Hash 压缩列表 键值对集合 添加、获取、移除单个键值对获取所有键值对检查某个键是否存在 1. 用于存储一个对象2. 存储用户用户的具体信息,年龄,生日等数据
Zset 跳表(后续讲解) 有序不重复集合 添加、获取、删除元素根据分值范围或者成员来获取元素计算一个键的排名 1.排行榜,比如在进行文章发布时候,可以将其发布时间作为score进行存储。这样时候获取时候就是自动按照时间排序好。2 可以在进行班级成绩排序时候,不需要再进行排序函数的排序,而是将学号作为value 将成绩作为score,这样就是自然的为我们排好序。3 进行权重的权衡 例如对于 微博热搜榜等

所以综上所述,在回答此问题时候,先说明具体的五种数据类型,也可以说出比较有特点的String和Zset的底层数据结构,然后表明具体都有什么特点最后再说明具体的使用的位置

String:

最简单的键值对的存储的键值,底层采用到sds实现,提高使用的效率,主要用在一些单点的数据上。

List:

有序的列表,对于文章的展示,或者是说国家的展示和省份的存储,利用其有序的特点。

Hash:

主要是用在对象的存储上,存储商品的信息,个人的具体住址,电话等。

Set:

无序的集合,主要使用在一些交集的查找,有哪些的共同好友,或者说是并集的使用,查询共同的关注,共同的兴趣爱好等。

Zset:

有序的集合,底层采用跳表的实现方式,拥有和set功能的功能的同时还能够及时的进行排序处理,或者说是在使用的时候,就可以利用其有序的特点进行省略排序的步骤

事务

对于事务来说,是数据库都不能够绕开的点,但是不同于MySQL中事务特点,对于Redis中的事务确实截然不同的。

Redis中的事务是什么样子

对于Redis中的事务来说:所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

与其他事务一样也是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

如何实现

在Redis中通过几个特殊的命令来实现事务,分别是MULTIEXECWATCHDISCARD,在事务执行的过程中,能够执行多条指令,但是一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。所以总结来说就是对于在Redis中的事务是顺序执行下去的一个队列命令。

实现的过程

首先来具体了解一下每条语句所对应的含义:

MULTI: 表示事务的开始,在redis中执行这条语句以后,表示事务的开启,这个时候,所输入的命令并不会立马执行下去,相反,在未出现EXEC特殊字符时候,所有命令的执行都会进入一个队列中。

EXEC:表示对进入到队列的语句进行一个执行操作,执行的是先进先出的原则。

WATCH: 表示监听,可以监听一个或多个健,是一个乐观锁,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令

DISCARD: 表示清空事务队列,前面我们提到了事务在未被执行的过程中,都会进入到一个队列中,此条操作就会情况事务队列,并放弃执行事务。

所以执行的三个阶段:

  1. 事务开始 MULTI
  2. 命令入队
  3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队,如果收到以上的请求,就会跳出继续进入队列,进行执行相关联的语句。

遇到问题时候

  1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行

ACID

原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)
事务前后数据的完整性必须保持一致。

隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行

持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

具有的特性

尽管Redis的事务也叫做事务,但是Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有持久性。

Redis持久化的两种方法

对于这个问题在面试的情况也是切身经历过,对于这个问题考察还是蛮有深度的,因为不仅仅是死死记住就行,需要自己切实理解其中的缘由与原理,因为这个题目衍生出来的问题比较多,所以只有自己全都是理解,才能够做到在面试时候不会被卡住:这部分想要具体详细了解可以参看《Redis设计与实现》。

什么是

Redis是一个支持持久化的内存数据库,通过持久化可以把在内存中的数据同步到硬盘上,来保证数据的持久化,当Redis在重启时候,可以通过加载硬盘文件重新加载数据到内存中,达到数据恢复的目的:

RDB

是Redis默认的持久化方法,按照一定的时间周期策略把位于内存中的数据保存为RDB文件(是一个二进制的文件)有两个命令可以进行RDB文件的生成:savaBGsave在执行save命令时候会阻塞Redis服务器进程,在执行过程中,Redis服务器不能够处理任何其他的请求。但是BGsave不同的是,在执行时候会派生出一个子进程由子进程完成RDB文件的创建,服务器父进程继续完成其他的响应。 bgrewriteaof
执行时候状态:

在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件。

其次,在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。最后,BGREWRITEAOF和BGSAVE两个命令不能同时执行:

❑如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。

❑如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。因为BGREWRITEAOF和BGSAVE两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑——并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,就很容易导致问题的出现。

优缺点

优点

1、只有一个文件 dump.rdb,方便持久化。
2、容灾性好,一个文件可以保存到安全的磁盘。
3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
4.相对于数据集大时,比 AOF 的启动效率更高。

缺点

1.是RDB是生产快照文件的 五分钟会生成一个快照文件,但是若是在这五分钟内出现了故障就会导致 数据的丢失会丢失五分钟之内的数据, 对于 AOF来说 最多 丢失一秒的数据

  1. 在生成数据快照的时候 如果文件很大 ,客户端可能会暂停执行几毫秒 此时若是正好在做秒杀活动的时候 fork一个子进程去生成一个大的快照 就会出现问题。

AOF

AOF持久化是通过保存redis服务器所指向的写命令来记录数据库状态。通过Write函数追加到文件的最后,当redis在重启时候会通过重新执行文件中保存的写命令来再内存中重建整个数据库内容。当两个同时开启时候,优先选择AOF进行数据库的恢复。对于 AOF来说是追加的方式,所以没有磁盘的寻址开销 会更快,像 mysql中 binlog。

优缺点

优点

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点

一样的数据 时候 AOF 文件比 RDB 还要大,在AOF开启以后 Reids 支持写入的 APS会比 RDB支持下的要低 异步刷新一次日志 fsync 。

注意
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

如何选择

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

  • 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。

  • 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。

  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

缓存异常情况见解

缓存穿透

定义:量请求的key根本就不存在于缓存中,此时就会去请求数据库,没有经过Redis的缓存。若是这样的请求过于庞大,就会导致数据库的请求过于频繁,若是一个黑客想要对数据库进行攻击,就可以制造大量不存在的key去访问数据库,造成数据库访问压力过大,最后导致宕机。

解决办法:因为请求的是数据库不存在的信息,所以避免的就是对于哪些请求原本就不存在在数据库里面的信息的请求过滤掉。

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

缓存雪崩

定义:是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方法:可以看到不同于穿透来说,缓存原来是存在的,后来失效了,所以就会导致请求都打在了数据库,所以需要做的就是,尽量避免在短时间里面所有的key都失效。

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存击穿

定义:是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁

缓存预热

定义:系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

内存问题

Redis 的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis主要消耗的是什么资源

因为Redis那么快的一个主要的原因就是基于内存,所以消耗的也主要是内存资源

在内存用完之后会发生什么

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis为什么那么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路 I/O 复用模型,非阻塞 IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

多路 I/O 复用模型

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

重要的数据结构

布隆过滤器

前面提到可以使用到布隆过滤器来解决缓存穿透的问题

什么是布隆过滤器

布隆过滤器我们可以把他看做由二进制向量(或者说是数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时所使用一些基础的数据结构而言例如 SET, LIST, MAP,它更加快捷,占用的空间更小,但是效率也更高。也有缺点就是其返回的结果是概率性的,不能够完完全全保证结果的准备性,理论上来说,加入更多的元素其误差性也就越大。而且,加入到布隆过滤器中的数据很难够删除。
在布隆过滤器中的每一个元素都只占用1bit,或者说,每一个元素在其中的存在都是0: 不存在,1: 存在。

元素加入

当一个元素想要加入布隆过滤器时候,需要进行以下的操作:

  • 使用 布隆过滤器中的哈希函数对元素值进行计算,得到不同的哈希值(有几个哈希函数参与计算就会得到几个哈希值)
  • 根据得到的哈希值,在相对应的地址上填上对应的1。

元素判断

想要查看一个元素是否存在于布隆过滤器中:

  • 对此值进行相同的哈希计算,得到几个不同的哈希值。
  • 将得到的哈希值在布隆过滤器中相对应位置进行查找,若是所查找到的所有值都为1 说明该元素在布隆过滤器中是存在的,若是有一个值不为1,说明该元素不存在于布隆过滤器。
    综上所述: 当输入一个值时候,布隆过滤器说其在其中可能会出现误判,有时候会有几率性存在,但是布隆过滤器说其不在其中

使用场景:

1: 判断给定的数据是否存在:可以进行大数据的判断,判断某一数值是否存在于一个很大的数字集合中(亿为单位),防止redis的缓存穿透(判断请求的数据是否有效,能够避免访问绕过缓存直接请求数据库)垃圾邮件的过滤,黑名单的功能等等。
2: 去重: 比如爬给定网址时候对已经爬取过的URL进行去重。

布隆来解决Redis

知道了布隆过滤器的原理和工作特写以后,可以把所有可能会请求到的key 放到布隆过滤器中,不存在就会直接返回错误信息给客户端,若是存在信息才会继续下面的流程。

String的底层实现SDS

image-20200728173456415

是一个SDS 其里面有三个变量 一个

free为0 表示SDS没有分配任何未使用空间
len 表示 存储字符串的长度
buf 表示存储的char 类型的数组 以空字符结尾
好处 :

  1. 获取字符串的长度时候 复查度为1 因为维持了一个长度的变量。

  2. 杜绝了缓存区的溢出 当sds的api操作对其进行修改的时候 其会先检查此时的空间满不满足修改所需要的空间大小,若是 不满足 会自动扩展空间到修改值的大小。

  3. 避免了内存的重分配
    因为对于 c 来说 当增加字符串的时候 要先进行内存的重新分配来扩展底层的数组的空间 不然会出现 缓冲区的溢出。在截取的时候 还是要重新分配内存空间的大小释放不再使用的那部分空间 不然会出现内存的泄露。对于 sds 来说会有内存空间的预分配 会为其分配额外的空间的大小

  4. 惰性空间释放 当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

  5. 相比与基础的数据来说 也可以使用一部分 stdio.h 中的库函数

Zset的底层实现跳表

首先来看一张跳表的底层实现图:

image-20200728194804788

对于上图中最左边叫做zskiplist包含以下的属性

header: 指向跳跃表的表头节点。

tail: 指向跳跃表的表尾节点。

level :记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。

length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

对于右边的叫做zskiplistNode包含下面的属性
level(层): 节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。

**后退(backward)**指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。

分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。

注意表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到,所以图中省略了这些部分,只显示了表头节点的各个层

为什么不用红黑树而是用跳表(面试真题)

  1. 我们都知道 对于红黑树来说 查找的时候 在找到了指定范围的小值以后 还要进行中序遍历的方式顺序继续寻找其他不超过大致的节点 但是对于 调表来说 找到了小值以后 对第一层链表进行若干步的遍历即可实现。
  2. 插入和删除来说 对于 树结构的删除 与 加入就会很有可能导致树的结构的变化 逻辑复杂 但是对于 调表来说 删除 与插入 就是 修改相邻的节点就好。
    算法实现上来说 跳表就相对来说简单了很多 红黑树就会复杂很多

主从一致性

如何保持主从一致性

对于主从复制而言就是说 由于 对于 一台的主服务器既要满足于写入的操作 也要完成 读的操作 压力会比较大,
这个时候就需要从服务器连接上主服务器进行复制操作 ,当启动一个sync 的命令 给master 当这个从服务器第一次连接到主服务器时候 会触发一个全局的复制操作 ,master就会启动一个线程,生成 RDB快照 还会把所有的写请求 都缓存进入到内存中 RDB文件生成以后 master会发送给从服务器 slave 接收到rdb以后 是 先写进本地的磁盘,然后再度加载到内存里面。
以上是使用到 sync时候的情况 但是会有很多的弊端 出现 若是 在命令传播的阶段主从服务器因为网络的问题出现了中断 此时有需要重新开始复制的操作 ,就算之前有点已经复制过来了但是还是要再次进行全部复制 这一点上可以进行优化处理,并且 对于 sync来说 是一个很消耗资源的操作

  1. 生 rdb文件时候 会消耗主服务器大量的cpu 内存以及磁盘的io资源
  2. 主服务器将这些rdb文件发送给从服务器的时候 会消耗主从服务器的网络资源 并且1对于 请求的响应 会有较大的影响。
  3. 接收到 rdb文件以后 在载入的期间时候 从服务器会因为阻塞而无法处理命令的请求。
    这个时候出现了 psync 命令
    有完整重同步 与 部分重同步
    完成的和之前的sync一样 在使用 bgsave 时候 产生 rdb文件 并发送 并且将位于 主缓存总的写命令发送给从服务器来进行同步
    部分同步时候: 当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
    以上我们就可以在使用时候 直接使用到 psync 来解决在出现中断时候的问题

reids 如何保证redis的缓存与数据库的一致性

在我们使用到缓存时候 就可能会涉及到 缓存与 数据库双存储双写的情况,要是出现双写的情况 就可能会出现数据库的一致性问题 这个时候 该怎么解决一致性问题。
最好也是不要使用这个方案: 读请求写请求串行化 串行化到一个内存队列里面去。
使用到串行化可以保证一定不会出现不一致的情况,但是却是得系统的吞吐量大大降低,用比正常情况下的机器去支持一个请求过程。
最经典的缓存+数据库读写的模式
就是 :读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

为什么提到了先删除缓存 而不是更新缓存呢?

就是说 对于一个缓存来说 并不是单单从数据库中直接取出来的
怎么理解呢 就是说有些的缓存已经存在了 然后我们要对其进行一个更改 或者是两个表的共同数据合并的计算才会得到这样的一个缓存的值,但是对于我们来说 更新缓存的代价是很高昂的 。我们每次对于 数据库的更新都要去更新缓存的话 有时候 对于 这个缓存来说 其实访问到的次数并没有那么多 (缓存的作用是1在读取的时候 能够 直接读取到的) 但是对于某些数据 我么你只是对其进行一个修改 读取时候 很少 、这个时候 我们若只是删除缓存,在一段时间里面 存储不过就会从新计算一次 得到一个新的值,用到的时候才去计算 不用频繁更新 不用做复杂的运算 减少开销

Key相关问题

过期淘汰策略

对于已经过期的键来说 reids提供了三种的过期删除策略:

  • 定时删除 在设置键的过期时间的同时,创建一个定时器,让定时器在键值过期时间来临时,立即执行对键的删除操作。
  • 惰性删除: 放任过期的键值不管,只要在从键值空间进行访问的时候,会监察键值是否过期,若是过期进行删除,没有过期 不会管。
  • 定期删除,我们会隔一段时间对数据库进行一个监察,删除里面过期的键值,至于要删除多少的键值,以及监察多少的数据时候 都是算法来决定的。
    下面来讲解好处与不好的地方:

定时:

对内存友好 在键过期以后就会删除 之前占用的内存就会得以释放 但是对CPU来说没那么友好 在过期的键值比较多时候,需要消耗大量的cpu来处理删除这些值。

惰性:

对cpu友好 不会特地消耗cpu在特定的时间里面进行删除 只会在使用的时候进行过期的检查然后删除 但是对于内存没那么友好是因为 有时候 过期的键值 没能够够删除 在很长的一段时间里面 都会占用内存的空间。

定期

前面的两则都有各自的缺点与优点 但是对于定期来说 是一种折中的处理
❑定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
❑除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。
但是不确定的难点在于 执行的时常与评率
删除评率太频繁 或者时常太长 就会退化成为定时删除 消耗cpu 执行太少 执行态度 退化为 惰性 浪费内存。

redis热key问题?如何发现以及如何解决?

首先是如何发现这些热点的key

  1. 凭借业务的经验,例如商品在做秒杀的时候 那个商品的key就被看做是热点key
    2.在客户端进行收集 可以在操作redis之前加入一行的代码进行数据的统计。统计出哪些的key值访问量较大。

  2. 对于有的jreids集群架构来说会有 proxy 在client 与 redis之间 ,可以在1proxy层进行信息的采集 判断哪些的key值是热点值

  3. 使用redis自带的命令
    1、 monitor 命令 而言 可以事实抓取到 redis服务器端收到的命令 这个时候 可以写代码统计 哪些的key值是热点数据,
    也可以使用 分析工具进行分析 如 redis-faina 但是 这个方法在高并发的条件下,有内存报增的隐患,也会降低 redis的性能。

该如何进行解决
一种是 利用二级的缓存来实现
利用一个 ehcache 或者一个hashmap 在发现了热点key以后 将其加载到 系统的jvm中 对于 这样的热点key值的请求 会直接从 jvm中进行取 而不会 走到 redis层。
2 备份热点key值
对于前面来说 我们已经可以获取到了哪些的key值是热点key值,下面要做的就是 将这些的热点key值在多个redis上都保存一份,此时有热点key值进行请求的时候u,在有备份的redis上取值,进行返回 不会导致每次的请求都落在同一台redis上,导致请求不过来的情况发生

什么是一致性Hash算法

就是我们在做数据库分库,分表,或者是分布式缓存时候,都会遇到一个问题就是说: 如何将数据均匀分散在各个节点中,并且尽量的在加减节点时候 使得当前我们的数据受的影响最小?

取模运算

对于取模运算来说也算是我们能够想到的最简单的方法,对于HashMap中值的随机存放也是利用到这种思想(但是实现的方法不同)。

对于取模来说可以将传入的 Key 按照 index=hash(key)%N 这样来计算出需要存放的节点。其中 hash 函数是一个将字符串转换为正整数的哈希映射方法,N 就是节点的数量。

这样可以满足数据的均匀分配,但是这个算法的容错性和扩展性都较差。

比如增加或删除了一个节点时,所有的 Key 都需要重新计算,显然这样成本较高,为此需要一个算法满足分布均匀同时也要有良好的容错性和拓展性。

一致性Hash算法

简而言之就是将所有的Hash值构成一个环,其值的范围在0~2^32-1 :如下图所示,在后续中,所有进行操作过后的值都会分散在这个环上

image-20200728210002967

例如现在我们有A,B,C 三个应用节点(利用IP,或者是hostname)这样的唯一性字段作为Key。

image-20200728210537392

之后我们对数据进行散列,散列在这个环上,为D1,D2,D3。

image-20200728211327729

将数据散列到环上之后,会进行顺时针的查找,顺时针查找出第一个遇到的节点,然后将其映射到遇到的第一个节点上,此时就会出现,D1映射到A节点上,D2映射到B节点上……

容错性

这个时候若是说对于节点B出现了故障,这个时候对于 D2数据与D3数据不会出现问题,也就是说并不会收到任何的影响。但是对于D1来说会映射到B节点上。

image-20200728212106088

扩展性

前面是在一个节点出现宕机的情况,可以发现受影响的也只有D2,其他的没有收到影响,同样,在新添加一个节点的时候:会发现也只有D3收到影响,但是其余部分没有收到影响

image-20200728212556324

虚拟节点

虽然上面的情况,对于该算法都能够完美应对,但是还有一种情况就是节点数量比较少时候,这样很多的数据都会集中在A节点,对于B节点来说数据就会比较少。如下图所示:

image-20200728213253984

这个时候为了能够解决这个问题一致哈希算法引入了虚拟节点。将每一个节点都进行多次 hash,生成多个节点放置在环上称为虚拟节点:在计算时候,在IP后加上特定的编号,来对其进散列,如下图所示,生成虚拟的节点,进行数据的映射

image-20200728213220249

分布式缓存和本地缓存有什么区别

分布式

对于分布式缓存来说 常用的有 redis 与 memchched

  1. 对于 分布式缓存来说 用于在集群下面多个节点共同使用同一份缓存的情况,从而减少数据库的查询。
  2. 分布式缓存可能会受到网络 IO流的影响,因此吞吐率与缓存的数据的大小有很大的关系。
  3. 对于分布式缓存来说 网络 IO流消耗的时间对于 分布式缓存是一个不小的消耗

本地缓存

1.相比于分布式缓存来说 本地的缓存就会高效很多,因为网络 IO流的影响 一次分布式缓存存取数据所消耗的时间可能对于 本地缓存来说 能够存取几千上万次。

  1. 对于本地缓存来说 在集群的条件下可能会存在数据的不一致问题,所以在使用时候需要考虑到缓存的时效性,以及缓存的数据对数据不一致的敏感程度
    可以在使用的过程中使用本地缓存来做以及缓存,减少分布式缓存的访问量(网络IO带宽消耗及网络时间消耗)
    使用分布式缓存做二级的缓存,减少在集群的环境下访问数据库的次数。

reids 与 memcached有什么区别

  1. 对于 m来说,所有的数据都存储在内存汇总,断电以后会挂掉,数据不能超过内存的大小,局限性比较大,但是 reids有持久化的功能 可以将部分数据存储在硬盘上,保证了数据的安全性。
  2. 对于m来说 所有的值都是字符串类型,支持的数据也比较简单和少,但是redis支持的时刻类型比较多。
  3. reids的速度要比m快很多。
  4. 对于 redis来说 单个value的最大限制是1GB,但是对于m来说只能保存1MB的数据, 因此可以实现的功能也会比较多,简单的消息队列。
  5. Redis是单线程的,采用多路io复用的方式。m是多线程采用的是非阻塞io。

哨兵模式

参考:掘金

主从复制可能会出现的问题

  • 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。
  • 原生复制的弊端在早期的版本中也会比较突出,比如:redis复制中断后,从节点会发起psync。此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时,可能会造成毫秒或秒级的卡顿。

解决方案(哨兵模式)

首先了解什么是哨兵模式:

image-20200728203455736

如上图所示,主要功能有主节点存活检测、主从运行情况检测、自动故障转移、主从切换。Redis Sentinel最小配置是一主一从。主要有以下功能:

  • 监控:不断检查主服务器和从服务器是否正常运行。
  • 通知:当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他应用程序发出通知。
  • 自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就可以免了。
  • 配置提供者:在Redis Sentinel模式下,客户端应用在初始化时连接的是Sentinel节点集合,从中获取主节点的信息。

工作原理

每个 Sentinel 节点都需要 定期执行 以下任务:

  1. 每个 Sentinel每秒钟 一次的频率,向它所知的 主服务器从服务器 以及其他 Sentinel 实例 发送一个 PING 命令(如下图)。

image-20200728203940131

  1. 如果一个 实例instance)距离 最后一次 有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel 标记为 主观下线(如下图)。

    image-20200728204132928

  2. 如果一个 主服务器 被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次 的频率确认 主服务器 的确进入了 主观下线 状态(如下图)。

    image-20200728204248335

  3. 如果一个 主服务器 被标记为 主观下线,并且有 足够数量Sentinel(至少要达到 配置文件 指定的数量)在指定的 时间范围 内同意这一判断,那么这个 主服务器 被标记为 客观下线(如下图)。

    image-20200728204310029

  4. 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率,向它已知的所有 主服务器从服务器 发送 INFO 命令。当一个 主服务器Sentinel 标记为 客观下线 时,Sentinel下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从 10 秒一次改为 每秒一次(如下图)。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zNt8HIq-1595943711439)(upload\image-20200728204325355.png)]

  5. Sentinel和其他Sentinel协商 **主节点** 的状态,如果 **主节点** 处于SDOWN` 状态,则投票自动选出新的 主节点。将剩余的 从节点 指向 新的主节点 进行 数据复制(如下图)。

image-20200728204340176

  1. 当没有足够数量的 Sentinel 同意 主服务器 下线时, 主服务器客观下线状态 就会被移除。当 主服务器 重新向 SentinelPING 命令返回 有效回复 时,主服务器主观下线状态 就会被移除(如下图)。

image-20200728204405386

其他

Redis常见性能问题和解决方案?

  1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
  2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
    为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
  3. 尽量避免在压力较大的主库上增加从库
  4. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
  5. 一个字符串类型的值能存储最大容量是多少?
    512M

10亿个key 找出 10w个开头

答:keys 指令扫描
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis集群的最大节点数是多少?

16384个

什么是Redis哈希槽

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

如何保证缓存与数据库双写时的数据一致性?

只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

场景 描述 解决方案
先写缓存,再写数据库,缓存写成功,数据库写失败 缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读 这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存
先写数据库,再写缓存,数据库写成功,缓存写失败 写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据 缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现
需要缓存异步刷新 指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候 确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/107646029
今日推荐