Redis

采用单线程为什么还这么快

  • Redis绝大部分操作都在内存中完成,且底层用了高效的数据结构,这就是保证了快的前提。又由于Redis执行的命令都是短平快的,不涉及计算密集型的任务,所以CPU就不是性能瓶颈,所以仅用单线程就可以。

  • Redis采用单线程避免了多线程之间的竞争,省去了多线程切换在时间和性能上带来的开销。

  • Redis服务器采用多路复用的模型,所以单线程就可以监控和管理多个IO流,效率更高。

RDB优缺点

  1. RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要进行fork创建子进程,属于重量级操作,频繁执行成本过高。

  2. Redis加载RDB恢复数据远远快于AOF方式。

  3. RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个不同RDB版本,兼容性方面可能有问题。

Redis持久化的方式

RDB(Redis DataBase)

  • RDB 是一种快照方式的持久化方法,它会在指定的时间间隔内将内存中的数据保存到磁盘上的一个文件中。

  • 这个文件是一个经过压缩的二进制文件,包含了某个时间点上的 Redis 数据集的所有数据。

  • RDB 持久化适合用于备份数据、进行灾难恢复等场景。

  • RDB 持久化是通过 SAVEBGSAVE 命令来实现的。SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕,而 BGSAVE 命令会派生一个子进程来执行持久化操作,不会阻塞服务器进程。

  • RDB 的缺点是在发生故障时可能会丢失最后一次持久化后的数据。

RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。

SAVE

SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。

BGSAVE

BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。

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

AOF(Append Only File)

  • AOF 是一种以日志形式记录 Redis 服务器所执行的写命令的方式,每个写命令都会被追加到文件末尾。

  • AOF 文件内容是以 Redis 协议格式记录的写命令,当 Redis 重启时,会重新执行 AOF 文件中的命令来恢复数据。

  • AOF 采用的是追加写的方式,因此即使 Redis 服务器在执行 AOF 重写时发生了故障,也不会影响 AOF 文件的完整性。

  • AOF 通过 BGREWRITEAOF 命令或自动配置的条件下触发 AOF 重写,将 AOF 文件中的写命令以更紧凑的格式重新写入一个新的文件中,可以减小 AOF 文件的大小。类似BGSAVE

AOF是否影响Redis性能

如图来说就是AOF的工作流程,可以看见AOF机制增加了于磁盘的交互频率。但是实际影响不大,因为AOF缓冲的存在就是让其中的数据积累到一定程度然后再同一写入磁盘,所以降低与磁盘的交互频次。而且磁盘上读写数据也并不都是慢的,顺序读写肯定比随机读写的效率来得高,而AOF是每次把新的操作写入到原文件末尾,属于顺序写入

AOF写回硬盘策略

AOF提供了三种写回硬盘的策略:alwayseverysecno

这三种策略都无法完全解决【主进程阻塞】和 【减少数据丢失】的问题,这两个问题是对立的,偏向其中一方,必然损失另一方。

  • always策略能最大程度保证数据不丢失,但是也是最损失性能的。因为每执行一条写操作命令就同步将AOF内容写回硬盘。

  • everysec策略相对折中,则是每秒将AOF内容写到磁盘。

  • no策略则是完全将这种写回的策略交由操作系统来负责,这种时机是不确定的,也不由程序员维护。

至于实现三种写回策略的底层原理也就是控制fsync的作用时期。

AOF重写机制

随着执行写命令的操作越来越多,AOF文件大小会越来越大,那么redis重新加载AOF文件的时候所需的时间也会越来越多。所以redis为了避免AOF文件越来越大,采用重写机制来解决,主要分为手动触发自动触发

  • 自动触发:当文件大小达到一个阈值的时候会启动AOF重写机制来压缩AOF文件。

由两个字段控制:

auto-aof-rewrite-min-size: 表示触发重写时AOF最小的文件大小,默认为64M
auto-aof-rewrite-percentage: 表示当前AOF占用大小相比上次重写时增加的比例

重写机制具体如何做的?

AOF重写机制也就是在重写时,读取当前数据库中所有键值对,然后每个键值对用一命令记录到新的AOF文件中,等到全部记录完后用这个新的AOF文件替换旧的AOF文件

AOF重写为什么需要新文件而不是直接对原文件操作?

因为如果直接对原文件进行操作,那么在重写过程中如果出现意外,那么就会污染原文件。所以为了避免这种情况,需要使用新文件,这样就能保证原子性,即要么成功使用新文件,要么失败使用旧文件,不用担心中间状态

  • 手动触发:手动执行bgrewriteaof

不论是自动触发和手动触发,都是执行上图逻辑来进行后台重写

由图中可以看出,fork之后分为三路:

  1. 子进程读取内存中的所有键值对,然后每个键值用一条命令记录到新AOF文件中,但是注意子进程此时看到的内存数据相当于是凝固的,即使主进程有数据写入子进程也无法看见。

  2. 主进程将新到来的数据同时写入到aof_buf, aof_rewrite_buf。写入到aof_buf的目的是为了之后同步到旧AOF文件,写入到aof_rewrite_buf是为了让子进程能够”看到”新来的数据从而保证新AOF文件中数据的最新状态。

如果需要执行bgrewriteaof的时候,当前redis已经正在进行aof重写了,那么就会立即返回不会再次执行重写;如果在执行bgrewriteaof的时候,当前redis正在生成rdb快照,那么aof重写操作就会等待直至rdb快照生成后再执行。

AOF和RDB的区别

AOF与RDB最大的区别:

  • rdb对于fork之后产生的新数据就置之不理了,这注定了rdb没有办法和最新数据保持一致。但是这也符合rdb的设计理念,即”定期备份”。

  • aof对于fork之后产生的新数据会保存在aof_rewrite_buf中然后会同步到新的aof文件中,这就保证了aof的”实时备份”。

“实时备份”也不一定就比”定期备份”来得好,具体还是要看实际场景。

混合持久化

将AOF和RDB混合使用就不仅可以具有RDB恢复数据快的优点,且拥有AOF丢失数据少的优点,简称混合持久化。

//redis.conf中配置如下
aof-use-rbd-preamble yes

混合持久化的工作时机:AOF日志重写过程

在AOF日志重写的时候,fork出来的子进程不再使用AOF方式将内存数据写入到新文件,而是使用RDB方式。主进程处理的增量命令依然会保存在aof_rewrite_buf中,该缓冲区中的数据最终会以AOF的方式追加到新文件的末尾。全部写完之后通知主进程将新的含有RDB格式和AOF格式的新AOF文件替换旧的AOF文件。

简单地说:AOF文件前半部分是RDB格式的全量内存数据,后半部分是AOF格式的增量命令数据。

采用混合持久化的好处

重启Redis加载内存数据的时候,由于前半部分是RDB内容,所以加载速度会很快。然后加载AOF内容,由于只是少量的增量数据,加载也很快。综合下来,效率大幅度提升了,且数据更少的丢失。

Redis淘汰策略

最常用的(LRU - Least Recently Used

  1. volatile-lru:从已设置过期时间的键中挑选最长没有访问过的的键进行淘汰。这种策略仅对设置了 TTL(Time To Live)的键进行淘汰。

  2. allkeys-lru:从所有的键中挑选最长时间没有使用的键进行淘汰,不论键是否有过期时间。

其他

  1. noeviction:不进行任何淘汰,当内存不足时,新写入操作会报错。

  2. volatile-ttl:从设置了过期时间的键中选择剩余时间最短的键进行淘汰。

  3. volatile-random:随机淘汰一些设置了过期时间的键。

  4. allkeys-random:从所有键中随机淘汰一些键。

  5. volatile-lfu(Least Frequently Used):从设置了过期时间的键中挑选使用频率最低的键进行淘汰。

  6. allkeys-lfu:从所有键中挑选使用频率最低的键进行淘汰。

Redis事务

事务功能关键点

  1. 命令队列:当你开始一个事务时,你的命令不会立即执行,而是被放入一个队列中。只有当你发出执行命令时,这些被队列的命令才会被执行。

  2. 使用MULTI命令开始事务:这个命令会标记一个事务的开始。紧接着的命令都会被添加到队列中。

  3. 命令入队:在MULTI之后,你可以继续输入多个命令。这些命令会被放入队列,但不会被执行。

  4. 使用EXEC命令执行事务:当你输入EXEC命令时,所有在MULTI之后队列中的命令将一次性执行。如果在执行EXEC之前连接断开,那么事务中的所有命令都不会被执行。

  5. 原子性:Redis 事务的原子性意味着事务中的命令要么全部执行,要么全部不执行。然而,如果事务中的某个命令执行失败,其余的命令仍会继续执行。这点与其他数据库系统的事务回滚不同。

  6. 使用DISCARD命令取消事务:如果在执行EXEC之前决定取消事务,可以使用DISCARD命令。这将清空事务队列。

  7. 无隔离级别:在 Redis 中,由于其单线程的特性,所有命令都是顺序执行的,因此不提供传统意义上的隔离级别。

  8. 错误处理:如果事务中的一个命令语法错误,Redis 会在EXEC时拒绝执行所有命令,并返回错误。如果命令在逻辑上无法执行(例如对一个字符串进行递增),则其他命令会继续执行,该命令会报错

Redis事务相较其他数据库而言的四大特性

  • redis事务的原子性

我们对原始的原子性的定义是:把多个操作打包到一起,要么都不执行,要么都执行,但是执行不一定确保都会成功。mysql对于这种事务的原子性走的更远,如果某个命令执行失败了,那么回滚到事务没开启前的状态。而Redis虽然没有回退功能,但是能将事务整体打包执行其实已经能算作具有原子性。但是具体这个原子性怎么定义还是看具体场景(Mysql这个标杆,提高了”原子性”的门槛,这就使人们谈到的原子性第一时间想到的是具有回滚功能的mysql事务)。

  • redis事务的的隔离性

redis不涉及隔离性,因为仅由主线程来执行任务,所有的请求/事务都是串行执行的。

  • redis事务的持久性

redis不具备持久性,因为redis本身就是内存数据库,并不与磁盘直接打交道。而RDB和AOF机制只是为了恢复内存数据而设计的,与mysql的事务持久性不是一个概念。0–0

  • redis事务不具备一致性

因为redis不具备回滚,执行事务的时候某个环节出错就会导致数据不一致。

从ACID各个维度看,redis的事务就像是一个”半成品”,那么redis的事务有什么用?

Redis的事务,主要的意义就是为了”打包”,避免其他客户端的命令,插队插到中间。

multi //开启事务
exec //提交事务
discard //放弃事务

watch key //监控某个key,如果在某个事务中这个key在修改和exec的过程中被其他客户端修改,那么本次事务修改失败

watch的实现原理

watch key的时候,redis给这个key分配版本号并且记录这个版本号,只要是这个key被修改了那么就会引起版本号变大。那么当事务提交(exec)的时候,如果事务中也有进行key修改的操作那么就会进行版本号检测,如果与watch记录的版本号不同则修改失败返回nil

主从重同步如何实现

答案:完整重同步和部分重同步

为了解决旧版复制功能在处理断线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。 PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:

完整重同步

确定了主从服务器之间的关系之后,主从服务器就需要进行第一次同步。第一次同步过程如下 :

  1. 从节点发送psync指令表明想要同步

  2. 主节点根据指令分析出要进行全量复制,回复+FULLRESYNC响应

  3. 从节点接受主节点的运行信息并保存

  4. 主节点生成RDB文件

  5. 主节点将RDB文件发送给从节点

  6. 主节点补发增量数据

  7. 从节点清空旧有原数据

  8. 从节点加载RDB文件得到与主节点一样的数据

  • psync表明从服务器想要与主服务器进行同步,psync具体形式如下:

psync replid offset
/*
replid和offset为各自默认值?和-1的时候,表明从服务器想要进行全量复制
offset为正整数n的时候:表明从偏移量为n的位置来进行获取数据
*/

部分重同步

部分重同步功能由以下三个部分构成:

  • 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。

  • 主服务器的复制积压缓冲区(replication backlog)。

  • 服务器的运行ID(run ID)。

什么时候进行全量复制,什么时候进行部分复制?

首次和主节点进行数据同步的时候或主节点不方便进行部分复制的时候采用全量复制;从节点之前已经从主节点复制过数据了,但是因为网络抖动或者从节点重启了,这时候从节点需要从主节点同步数据并且不需要全部复制。

无硬盘模式

主节点生成的RDB的二进制数据不是直接保存到文件中了,而是直接通过网络传输,省去了写磁盘的过程。

从节点之前收到增量数据是先写到rdb文件中,然后加载rdb文件。现在也省略这个过程,直接将数据加载。

即便如此,整个操作流程仍然是比较重的,比较耗时的。

  • 实时复制

第一次同步完成双方维护一个Tcp长连接,后续主节点可以通过该连接将写操作命令传播给从节点,然后从节点读取到该命令之后执行,使得主从节点数据一致。

网络断开重连后如何保证主从数据一致

Redis2.8之后,网络断开又恢复后,主从节点通过增量复制的方式实现数据一致性。

如图所示:从服务器恢复网络的时候会向主节点发送psync命令;主节点收到该命令后,然后使用+CONTINUE来响应从节点(告诉从节点接下来要使用增量复制的方式来同步数据);然后主节点将从节点断线期间所执行的写命令发送给从节点,然后从节点执行这些命令。

主节点如何知道要将哪些增量数据发送给从节点?

127.0.0.1:6379> info replication
role:master


repl_backlog_active:1 // 开启复制缓冲区
repl_backlog_size:1048576 // 缓冲区最⼤⻓度
repl_backlog_first_byte_offset:7479 // 起始偏移量,计算当前缓冲区可⽤范围
repl_backlog_histlen:1048576 // 已保存数据的有效⻓度

如上代码块中有关backlog就是主节点中用来维护增量数据的字段,具体名称叫做复制积压缓冲区,本质就是一个环形队列。

更详细地,主从节点在进行命令传播(实时复制)的时候,主节点同时将写命令写入到复制积压缓冲区中(repl_backlog_buffer)。所以无论网络是否正常,复制积压缓冲区始终记录着最近的写命令。

网络恢复时从节点发送的psync命令中的offset填写的是slave_repl_offset,主节点拿着这个offset来与自己的master_repl_offset计算差距,然后来确定对从节点使用哪种(+FULLRESYNC/+CONTINUE)响应:

  1. 如果判断出从节点要读取的数据还在复制积压缓冲区中,那么采用增量同步的方式。

  2. 如果不在,则采用全量同步的方式。

主从断开连接时机与晋升主节点

  • 从节点主动和主节点断开连接

slaveof no one

这个时候从节点可以晋升为主节点,且需要程序员要主动修改redis的组成结构。

  • 主节点挂了

这个时候,从节点不会晋升为主节点,而是需要人工干预来恢复主节点。

主节点故障与主客观下线

哨兵每隔一秒给所有主从节点发送PING命令,当主从节点收到PING命令的时候会进行响应

主观下线

当主从节点没有在规定的时间内对PING命令作出响应,那么发出该PING命令的哨兵将节点标记为主观下线。

这个规定的时间是可以进行配置的:

//修改配置项
down-after-milliseconds

客观下线

客观下线用于标记主节点的情况

考虑到主节点没有在规定时间响应PING命令可能是网络拥塞或者系统压力大,实际上并没有故障。为了防止这种误判,哨兵在部署的时候不会只有一个节点,而是由多个节点部署成哨兵集群(至少3个节点)。通过多个哨兵节点进行判断就可以避免由于单个哨兵自身网络状况不好而导致的误判

具体地,当一个哨兵将一个主节点标记为主观下线的时候会给其他哨兵节点发起命令,其他哨兵节点收到命令后开始投票。投票规则如下:

当投的赞成票数达到哨兵配置文件中的quorum配置项设定的值后,即被标记为客观下线

PS:quorum的值一般为哨兵个数的1/2+ 1

由哪个哨兵进行主节点故障转移

首先需要在哨兵集群中选出一个leader,让leader来执行主从切换。选出leader之前得有个候选者。

谁来做候选者?

哪个节点判断主节点为客观下线,那么该节点就是候选者。

确定候选者之后,还需要满足一些条件才能成为leader:

  • 拿到半数以上赞成票

  • 拿到的票数同时还要大于等于哨兵配置文件中的quorum

哨兵节点为什么至少需要3个?

由于需要满足半数以上的赞成票,假如只有2个节点,那么半数以上就是2张票。如果此时有任意一个哨兵节点出现问题,那么无法满足条件,就选不出leader,导致无法进行主节点故障转移。所以通常会配置至少3个哨兵节点。这时候3个节点中出现一个挂点还是能正常选出leader的,极端一点如果这时候3个节点挂掉2个,就需要人工干预了,或者多增加一些哨兵节点。

练习

  1. Redis服务器一主四从,有5个哨兵,quorum = 3。如果哨兵节点挂掉2个,当主节点宕机时,哨兵节点能否判断主节点客观下线?能够进行主节点故障转移?

判断能够客观下线只需赞成票满足quorum即可,所以该场景下是有可能满足,所以可以判断客观下线。

判断能否主从切换只需判断赞成票数能否过半,3张赞成票已经过半,所以可以进行主从切换。

  1. 条件相同的情况下,如果哨兵节点挂掉3个,quorum = 2,那么结果如何?

由于quorum = 2的条件可以满足,那么是可以判断客观下线的;但是能投赞成票的哨兵节点个数超不过一般,所以不能主从切换。

如果quorum = 3,那么既不能判断客观下线,也不能主从切换。

quorum = 2能判断客观下线而不能自动主从切换,那么功能就是鸡肋的,还不如设置quorum = 3,这样两个都不会工作。

综合以上结果:哨兵节点个数为5的时候,quorum的值设置为3比较好,所以结论就是quorum的值应该是哨兵节点个数的1/2 + 1,且哨兵节点个数应该为奇数个。

主从切换的具体流程是怎样的

选出leader之后就可以进入主从切换的流程了。

First step:选出新主节点

在已下线的主节点属下的所有从节点中,挑选出一个网络状态良好,数据完整的从节点,然后向从节点发出slaveof no one命令,将从节点转化为主节点。

如何选出好的从节点,需要经历三轮考察

  • 第一轮:优先级最高的从节点胜出

Redis中有一个slave-priority配置项,可以给从节点配置优先级。每一台从节点的的服务器配置不一定相同,所以可以根据服务器性能来配置节点优先级。

  • 第二轮:复制进度最靠前的节点胜出

如果在第一轮中发现优先级高且相同的从节点有好几个,那么就会进行第二轮考察

常见的数据分片算法

三种主流分片方式:

哈希求余

对key使用哈希算法计算出hash值(比如md5),再把这个hash值余上分片个数,就可以得到一个下标,此时就可以将数据放入对应下标的分片中了。

hash(key) % N = 0 //将数据存放到0号下标

分片的主要目的就是为了提高数据存储能力,分片越多,可以存储的数据也就越多,那么维护的成本也就越高。一般一开始都是先搞几个分片(3个),随着业务逐渐发展,数据量也就越大,3片已经保存不下了,这时候就需要扩容。

这种分片方式虽然简单,但是存在明显缺陷:一旦服务器集群需要扩容,这个成本就很高了。根据上面的表达式,当N变化的时候,最后的结果也就变化,原来分片中的数据在集群扩容之后就不应该待在当前分片中了,就需要重新分配到别的分片中(数据搬运)

由图可知,只有少部分数据在扩容后还在原来的分片中,大部分数据都被搬运走了。

上述级别的扩容开销极大,往往是不能在生产环境上操作的,只能通过”替换”的方式来实现扩容。具体点就是假如需要4个分片,那么就准备4台主机,然后原来3台主机的数据按照新分片的方式写入这4台主机,最后用这4台主机替换原来的3台,至此实现扩容。

一致性哈希

哈希取余的缺点是扩容的代价很大,造成这种代价大的根本原因就是:key属于哪个分片经过取余后是不断交替的。而一致性哈希就是将这种交替改成了连续,从而降低数据搬运成本。

这种方式如何降低搬运成本?

如图所示的例子中,只需要将0号分片中一部分连续数据搬运给3号分片即可。搬运成本相比哈希取余来说低了不少。

但是,虽然搬运成本降低了,但是这几个分片上的数据量可能就不均匀了,我们将这种现象称之为数据倾斜***。*

均匀的增加分片数目可以缓解数据倾斜的问题,但是一次性增加好几个分片,每个分片下面又有从节点,这就导致需要主机的数量很大,也不是一种很好的方式。所以接下来提出哈希分槽算法。

哈希分槽

哈希分槽算法是当前Redis正在采用的算法,给出的公式如下:

hash_slot = crc16(key) % 16384
//crc16也是一种计算哈希值的算法,与md5类似
//16364 = 16 * 1024 = 16k
//hash_slot表示哈希槽,总共16384个

将16384个哈希槽位分配给不同的分片,例如:

假设当前有三个分⽚, ⼀种可能的分配方式:

• 0 号分⽚: [0, 5461], 共 5462 个槽位

• 1 号分⽚: [5462, 10923], 共 5462 个槽位

• 2 号分⽚: [10924, 16383], 共 5460 个槽位

虽然不是严格意义上的均匀,但是实际相差非常小,我们就认为三个分片上的数据比较均匀。假如此时需要查询某个key,则先计算出对应的hash_slot,然后根据hash_slot来找出所在的分片,每个分片的槽位号可以是连续的也可以是不连续的

简单的说:哈希分槽 = 哈希求余 + 一致性哈希

每个分片如何得知自己拥有哪些槽?

每个分片维护位图数据结构来表示自己是否拥有对应槽,总共16384个槽,那么就需要16384个比特位(16384 / 8 = 2048 bytes, 约等于2kb),花费的空间不大。

Redis集群最多有16384个槽位吗?

这是理论上的,但是实际上并不合理。因为总共有16384个槽位,也就意味着key映射成槽位的时候会有重复,重复的key都会存储在同个分片中。如果真的分配了16384个分片,那么一定有某些分片上的key是多的,有些是少的,这种情况下数据是不均匀的。所以分片数目不宜过多,这样即使某个槽有多个key,但是由于给分片分配的槽数很多,那么这种重复就可以忽略,可以认为槽位的数量就是key的数量。

Redis作者建议集群分片数目不应该超过1000

为什么选用16384而不是32768或者65536个槽位?

从内存角度来说,心跳包的传输是秒级的,而其中就有字段来表示该节点所拥有的槽数,16384是2kb,32768是4kb,65536是8kb,相比来说会消耗更大,因为心跳包传输的频率很高。16384经过大量实践被认为是性能与内存消耗的平衡点(刚好够用,且不会占据太多内存资源)。或许可能在若干年后网络带宽成倍增长,或者是服务器集群数目成倍增长,那么到了那个时候肯定也会去寻找新的平衡点。

过期删除策略与内存淘汰策略

首先Redis可以对key设置过期时间,数据结构如下:

typedef struct redisDb {
dict dict; / 数据库键空间,存放着所有的键值对 */
dict expires; / 键的过期时间 */

} redisDb;

由图中可以得知,被设置的过期时间的key都会以键值对(键:key,值:过期时间)的形式存储在expire指向的字典中。所以查询key的时候都会将过期时间与系统时间进行比对从而判断是否过期。

Redis的过期删除策略是什么?

Redis采用惰性删除 + 定期删除两种方法配合使用

  • 惰性删除的做法:

不主动将过期键删除,每次从数据库访问key的时候都判断key是否过期,如果过期则将该key删除。

由于检查key是否过期只需要O(1)的时间复杂度即可,所以只会消耗很小的CPU资源;但是key如果过期了没有被及时删除则会消耗额外的内存空间。

  • 定期删除的做法:

每隔一段时间随机从数据库中选出一定量的key来进行检查,如果过期了则删除。

消耗的CPU资源肯定比惰性删除多,但同时也减少了过期key对内存空间的无效占用。

  • 定时删除的做法:

采用时间轮定时器或者优先级队列来进行精准的key删除。

这样做的好处就是可以及时释放内存资源,但是如果存在大量的过期key,那么删除这些key所消耗的CPU资源就很高了,线程可能就会被阻塞,且由于单线程的原因,客户端就暂时无法进行响应。所以在这里如果Redis采用这样的设计是不太合理的。

Redis怎么实现定期删除的?

检查的频率是每秒抽取10次key,每次抽取20个进行检查并删除过期key。如果20个中有25%是过期的,那么继续抽取20个key…,反之则停止结束本次删除,等待下一轮再检查。

do {
//已过期的数量
expired = 0;
//随机抽取的数量
num = 20;
while (num–) {
//1. 从过期字典中随机抽取 1 个 key
//2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++
}
// 超过时间限制则退出
if (timelimit_exit) return;、
/* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */
} while (expired > 20/4);

且为了不会在while(expired > 20/4)中循环过度,redis设置了一个时间上限为25ms,超过则退出。

Redis内存淘汰策略是什么?

127.0.0.1:6379> config get maxmemory-policy

  1. “maxmemory-policy”
  2. “noeviction”

_“noeviction”策略是Redis3.0_之后采用的默认内存淘汰策略,表示当运行内存超过最大设置的内存时,不淘汰任何数据。这时候如果还进行写入,则禁止写入并报错。如果只是查询或者删除操作,那么依然可以正常工作。

除了【不进行数据淘汰】的内存淘汰策略之外还有【进行数据淘汰】的内存淘汰策略,划分如下:

  • 仅针对有设置过期时间的key

volatile-random: (Random) 随机淘汰设置了过期时间的任意键值。

volatile-ttl: (FIFO: First In First Out) 优先淘汰更早过期的键值。

volatile-lru: 【Redis3.0之前默认的策略】优先淘汰所有设置了过期时间的键值中,最久没有使用的。

volatile-lfu: 【Redis4.0之后新增的策略】优先淘汰所有设置了过期时间的键值中,最少使用的键值。

  • 针对所有的key

allkeys-random:随机淘汰任意键值;

allkeys-lru:淘汰整个键值中最久未使用的键值;

allkeys-lfu:【Redis 4.0 后新增的内存淘汰策略】淘汰整个键值中最少使用的键值。

Redis用作缓存时的缓存更新策略

定期生成

把访问的数据以日志的形式记录下来,可以每天更新,也可以每个月更新。

然后配合一套离线流程(往往使用shell, python写脚本代码),并通过定时任务来触发。这里的流程要执行如下逻辑:

  1. 完成统计热词的过程

  2. 根据热词,找到对应结果的数据

  3. 把得到的缓存数据同步到缓存服务器上

  4. 控制这些缓存服务器自动重启

【定期生成】的优点:实现比较简单,过程也可控(缓存中有什么内容都是比较固定的),也方便排查问题;缺点:缺乏实时性,无法面对一些突发情况,例如有一些不是热词的内容突然变成了热词(比如”春节晚会”),新的热词由于没有及时写入Redis,可能给后面的数据库带来较大压力。

实时生成

如果在Redis中查到了则直接返回,没有查到则去数据库中查找结果并写入到Redis中。

这样有一个问题就是:不断的向Redis中写入会导致内存占用越来越多,逐渐达到内存上限(可以设置配置项maxmemory),这时候就需要”内存淘汰策略“机制的介入。

缓存注意事项

缓存预热(Cache preheating

我们知道缓存数据的更新策略有实时更新与定期更新,其中定期更新不涉及缓存预热问题,而实时更新才有缓存预热问题。

具体地,由于客户端先查询Redis,如果没查到就走Mysql,查到了之后数据会顺带被写入Redis。此时所有的请求都会打给Mysql,随着时间的推移Redis上的数据越来越多,Mysql承担的压力逐渐减小。我们担心的是一开始给Mysql造成的压力太大从而导致崩溃。

缓存预热就是用来解决上述问题,做法就是先采用离线的方式(定期更新)将一部分热点数据写入Redis。导入的这部分数据就能帮Mysql承担很多的压力。随着时间推移逐渐就用新的热点数据淘汰旧的热点数据(实时更新)。

缓存穿透(Cache penetration)

当查询的某个key即不在Redis中,也不在Mysql中,那么自然也不会更新到Redis中。如果像这样的数据很多,那么一样会给Mysql带来很大的压力。

发生这种事件的场景:

  • 业务设计不合理,比如缺少必要的参数校验环节,导致非法的key也被查询了(典型场景)。

  • 开发/运维的误操作,导致Mysql上的数据被删除了(没那么典型,但是造成的结果也是缓存穿透,且不一定能及时发现)。

  • 黑客恶意攻击。

如何解决?

  1. 通过改进业务,加强监控报警…(不是很靠谱,亡羊补牢)。

  2. 如果发现这个key在Redis和Mysql上都没有,仍然写入Redis中,但是给定一个非法value(比如空串””)。

  3. 还可以引入布隆过滤器,每次查询前先通过布隆过滤器查看key是否存在。

缓存雪崩(Cache avalanche)

由于短时间内,Redis上大量的key失效,导致缓存的命中率陡然下降,Mysql的压力迅速上升,甚至直接宕机。

发生这种情况的原因:Redis宕机/Redis集群中大量节点挂了或者大量的key同时过期。

大量数据同时过期如何解决?

  1. 当初给key设置过期时间的时候添加上一个随机因子X,例如本来是5s后过期 ==> (5 + X)s后过期,这样就保证了不会有大量的key过期时间相同。

  2. 使用互斥锁,如果发现当前访问的key不在Redis中,就加上一把互斥锁,保证在同一时间只能有一个请求来构建缓存,其他客户端的请求要么因为没有争抢到锁而直接返回默认值或空值,要么等待锁释放后重新读取缓存。

注意:设置互斥锁的时候,最好也要设置一个超时时间,避免客户端因为意外导致一直占有锁而不释放。

缓存击穿(Cache breakdown)

相当于缓存雪崩的特殊情况,针对热点key突然过期了,导致大量的请求直接打到了Mysql上,直接引起Mysql宕机。

解决方案

基于统计的方式发现热点key,然后将热点key设置为永不过期;进行必要的服务降级,访问数据库的时候使用分布式锁,限制同时请求数据库的并发数。

无论哪种情况的解决方案无非就是要想方设法的让Redis能在各种场景下都能替Mysql负重前行。