Redis适用于哪些场景?
- 缓存
- 分布式锁
- 降级限流
- 消息队列
- 延迟消息队
说一说缓存穿透
缓存穿透的概念
用户频繁的发起恶意请求查询缓存中和数据库中都不存在的数据,查询积累到一定量级导致数据库压力过大甚至宕机。
缓存穿透的原因
比如正常情况下用户发起一个请求根据某个主键ID获取数据库信息,服务器接收到请求后会先查询缓存,如果缓存命中则返回,未命中就去查询数据库,数据库中存在则返回并存入Redis,不存在提示或者报错。
但是假如有人使用一定不存在的ID,比如负数或者很大的数值进行恶意频繁的请求,就会导致数据库的压力增大甚至宕机。
缓存穿透的解决方案:
- 在缓存中给不存在的ID设置null值。
对于那些缓存中和数据库中都查不到的数据,同样把它保存到缓存中并将value值设置为null,这样下次同样的id再来请求就直接从缓存中获取null值并返回。
优点:实现简单。
缺点:浪费内存,并且如果攻击者每次使用的是不同的脏数据进行攻击,这种方式是处理不了的。
- 使用布隆过滤器
使用布隆过滤器服务器的逻辑就会变成这样:
什么是布隆过滤器呢?
先来看下面这张图:
布隆过滤器的底层是一个bitmap,也就是一个数组,每个下标只存储0或1,默认初始化时全部都为0。可以在其中定义多个不同的哈希函数对要存储的数据进行计算,不同的哈希函数计算出的结果作为数组的下标将其对应的值改为1。这样不管是存数据还是查询数据都可以通过哈希计算得到对应下标然后根据是否为1判断是否存在,如果有一个值不为1则就是不存在。
但是,这种机制也是会出现一定的误判率的,具体看下面这张图:
首先将id1和id2存入布隆过滤器中,得到了对应的下标并设置为1,这个时候id3也要存进来,然后通过哈希计算得到了3、9、12的下标,这个时候虽然id3并不存在,但是布隆过滤器也会判定为存在!这个就是误判。
说一说缓存击穿
缓存击穿的概念
在Redis缓存中某条热点数据过期了,然后在这一时间有大量的并发请求到服务器导致缓存中查不到,就都请求到了数据库,数据库压力瞬间增大甚至宕机。
缓存击穿的原因
缓存穿透的概念基本就是它的出现的原因,理论上当在数据库查到数据时会将数据放到缓存,后续请求就可以从缓存中获取到,但是缓存穿透发生的节点就是在数据库查到数据之后和将数据放回缓存之前这一时间段。
缓存击穿的解决办法
- 使用逻辑过期时间
逻辑过期时间类似于Mybatis的逻辑删除,可以对缓存中的数据增加一个expire属性,对应的值就是过期时间。这样当请求过来之后,从缓存中获取到数据判断是否逻辑过期了,如果逻辑过期了就创建一个线程进行缓存重建,注意,这里需要加上互斥锁进行,新开线程是为了不阻塞主线程,而加互斥锁是防止后续的线程也进行缓存重建流程,在缓存重建完成之前主线程都就会直接返回这条过期数据给用户。
**缓存重建:**查询数据库,并重新放入缓存并设置新的逻辑过期时间。
优点:可以保证高可用性。
缺点:在过期重建期间,返回的数据是已经过期的,不能保证数据的完全一致性。
- 互斥锁
当缓存中查询不到该数据时,先获取互斥锁(可以利用redis分布式锁),然后再去查询数据库并将数据重新放入缓存和设置过期时间,最后释放锁。
优点:保证数据的完全一致性。
缺点:性能差,加锁后其它线程只能等待。
下面是两种方式的图解对比:
说一说缓存雪崩
缓存雪崩的概念
大量的热点数据在同一时间段缓存过期或Redis宕机了,导致大量请求到达数据库,带来巨大压力。
缓存雪崩的原因
缓存雪崩的主要原因有两种,一种是大量的key都设置了同一过期时间,如果在数据过期是出现大量的请求就会导致数据库压力过大;另一种是Redis直接宕机了,那么所有的请求到会直接到达数据;
缓存雪崩的解决办法
- 设置随机值
在给不同的key设置过期时间的时候,可以设置一个随机值比如1-5分钟时间,让过期时间加上这个随机值作为key的过期时间,这样就避免了在同一时间大量的key同时过期。
- 利用Redis集群提高可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
Redis 如何实现双写一致
什么是双写一致?按照正常逻辑读数据时会先查询缓存,如果缓存未命中才会去查数据库;写数据时则需要数据库的数据和缓存中的数据保持一致。正常情况下我们先删除缓存,再更新数据库并重新放回缓存是没有任何问题的。但是在分布式场景下或者并发场景下,这种方式就会导致在写数据时的数据不一致问题。
比如下面的两种情况都会导致数据不一致性问题:
而保证写数据时数据库和缓存中的数据保持完全一致,也就是所说的双写一致。
缓存双删
既然先删缓存再写数据库和先写数据库再删缓存都不行,那我们就删两次,在写数据库之前删一次缓存,写完之后再删一次;这种方式基本上可以解决问题,但是写完之后我们不能立马删除缓存,而是需要延迟一会儿再删除,为什么呢?因为大部分公司数据库都采用了主从的模式,那么刚更新数据库后需要同步给从数据库,这个时间点如果立马删除缓存数据可能还是会导致数据不一致性(一般主数据库接收写操作,从数据库接收读操作),所以要延迟一会儿再进行删除。
但是因为这个延迟时间并不好控制,所以延迟双删在存在数据库主从模式的架构下是不建议使用的,它不能保证数据库的完全一致性。
读写锁
首先先来看下具体的实现思路:
可以利用读写锁的方式,在写数据的时候我们加上排他锁,也就是写数据时其他线程是不可以进行读和写操作的。而在读数据时是加上共享锁,使得其他线程可以并发读取数据。
读写锁的方式既保证了数据的强一致性,又保证了效率。那么具体实现方式可以使用Redisson来实现。
读锁代码:
写锁代码:
异步通知
具体实现思路如下图:
首先需要加一个中间层,也就是MQ;在进行数据库写的操作时需要将这条消息发送到MQ中,然后再写一个监听这条消息的方法,去进行缓存的更新操作。那么这样就可以保证更新完数据以后缓存也会实时的进行更新,而可靠性也就相当于交给了MQ了。
优点:保证了数据最终一致性,并且不影响效率。
缺点:在同步MQ和MQ进行更新操作期间,用户读到的数据不是最新的。
Redis 如何实现持久化?
Redis实现持久化的方式有两种,一种是RDB的形式,一种是AOF的形式,一般公司项目中都会使用两种方式结合的形式保证数据的持久化。
什么是持久化?持久化其实说白了就是将数据写到磁盘中,因为Redis是基于内存的,所以数据都是放在内存中,如果Redis崩溃了,那么就会导致所有的数据丢失,这是一个严重的问题!
为了解决这种问题,就可以将内存中的数据写入到磁盘中,一旦Redis崩溃了,那我们还可以在重启Redis时将磁盘中保存好的数据进行回复,而这种操作就被称为持久化。
那么接下来我们需要思考下面几个问题:
- 在什么节点去进行持久化的操作?
- 怎么保证持久化的数据和Redis崩溃前的数据保持完全一致?
RDB
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中,默认会创建一个名为 dump.rdb 的文件保存数据,当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
在Redis配置文件中可以通过 dbfilename 属性查看RDB的文件名:
Redis的RDB持久化操作可以在以下两种情况下触发:
- 手动触发:
- save 命令:这是手动触发RDB持久化的机制。当执行 save 命令时,Redis会阻塞主线程,创建一个子进程,并将数据快照保存到磁盘上的RDB文件中。在持久化完成之前,Redis的主线程将无法处理其他请求。因此,如果Redis中的数据量较大,使用 save 命令可能会造成长时间的阻塞,因此在线上环境中通常不推荐使用此命令。
- bgsave 命令:这是异步触发RDB持久化的机制。当执行 bgsave 命令时,Redis会fork一个子进程来负责持久化操作,而主进程可以继续处理其他请求。因此,使用 bgsave 命令可以避免阻塞主线程,提高Redis的性能。
- 自动触发:
- 自动触发RDB持久化的机制,这可以通过在配置文件中设置 save 指令来实现。例如,设置 save 900 1 表示在900秒(15分钟)内,如果至少有1个键发生变化,则自动触发RDB持久化操作。这样可以根据实际需求来灵活地控制RDB持久化的频率。
需要注意的是,在自动触发RDB持久化时,Redis也会选择 bgsave 而不是 save 来进行持久化操作,以避免阻塞主线程。
打开本地的Redis配置文件,可以看到Redis有下面默认配置:
这里有三个save参数:
- 如果在900秒内,至少有1个键发生变化,就执行一次RDB持久化操作。
- 如果在300秒内,至少有10个键发生变化,也执行一次RDB持久化操作。
- 如果在60秒内,至少有10000个键发生变化,还会执行一次RDB持久化操作。
当设置了多个save参数时,Redis会按照从上到下的顺序依次检查每个save参数的条件是否满足。如果某个save参数的条件被满足(即时间间隔内数据的变化量达到了指定的数量),Redis就会执行一次RDB持久化操作。这意味着,只要有一个save参数的条件被满足,Redis就会进行持久化操作。
- 优点:
- 以二进制压缩文件的形式存储,占用内存更小,恢复时更快。
- 缺点:
- 周期性数据丢失,如果在Redis宕机之前的最后一次进行RDB持久化后,仍有别的写操作执行,那么这部分数据会导致丢失。
AOF
- AOP的概念与原理
AOP,即Append Only File,是Redis的一种持久化方式。与RDB(Redis Database)快照模式不同,AOP模式是通过记录每一条修改数据的命令,并将这些命令追加到AOF文件中来实现数据的持久化。当Redis服务器重启时,它会通过重新执行AOF文件中的命令来恢复数据。
AOP模式的核心原理是“先执行命令,后写日志”。每当Redis执行一个修改数据的命令时,它都会将该命令写入AOF文件。这种方式的优点是可以确保数据的实时性和准确性,因为即使在系统崩溃的情况下,也不会丢失任何已经写入AOF文件的命令。
- **AOP的配置与使用
**在Redis中,AOP模式默认是关闭的,需要通过修改配置文件来启用。在Redis的配置文件(redis.conf)中,可以通过设置以下参数来启用AOP模式:- appendonly yes:启用AOP模式。
- appendfilename “appendonly.aof”:设置AOF文件的名称,默认为"appendonly.aof"。
- appendfsync always|everysec|no:设置AOF文件的写入策略。其中,"always"表示每次写入操作都同步到AOF文件;"everysec"表示每秒同步一次;"no"表示由操作系统控制同步周期。
- **AOP的优点与不足
**AOP模式具有以下优点:- 数据实时性和准确性高:由于AOP模式记录了每一条修改数据的命令,因此可以确保数据的实时性和准确性。
- 可读性强:AOF文件以文本形式存储,可读性强,便于进行故障排查和数据恢复。
- 支持增量备份:AOP模式支持增量备份,只需要备份自上次备份以来的AOF文件即可。
然而,AOP模式也存在一些不足之处:
- 文件体积大:随着命令的不断写入,AOF文件的体积会越来越大,占用更多的磁盘空间。
- 恢复速度慢:当系统崩溃时,Redis需要重新执行AOF文件中的每一条命令来恢复数据,这可能会花费较长的时间。
- AOP重写机制
为了解决AOF文件体积过大的问题,Redis引入了AOP重写机制。AOP重写是将Redis进程内的数据转化为写命令同步到新AOF文件的过程。在重写过程中,Redis会遍历当前内存中的所有数据,并生成相应的写命令,然后将这些命令写入新的AOF文件中。重写后的AOF文件体积会大大减小,但需要注意的是,在重写过程中,Redis的写操作可能会受到一定的影响。
混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。 将 rdb 文 件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自 持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
Redis 的过期策略
惰性过期
- 描述:当客户端尝试访问一个key时,Redis会检查这个key是否设置了过期时间,并且是否已过期。如果key已经过期,Redis会立即删除这个key,并且不会返回任何数据给客户端。
- 优点:节省CPU资源,因为过期检查只在key被访问时执行。
- **缺点:**如果过期key长时间不被访问,它们会持续占用内存空间。
定时过期
- 描述: Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。
- **定时扫描策略(不能手动设置):**Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是 采用了一种简单的贪心策略。
1、从过期字典中随机 20 个 key;
2、删除这 20 个 key 中已经过期的 key;
3、如果过期的 key 比率超过 1/4,那就重复步骤 1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认上述操作不会超过 25ms。
- **优点:**确保过期key得到及时删除,避免内存空间的浪费,并减少数据不一致的风险。
- **缺点:**需要消耗一定的CPU资源来执行过期检查,并且可能存在延迟删除的情况,即key的过期时间和实际删除时间之间存在一定的时间差。
Redis实际使用的过期策略
- Redis结合了惰性过期和定时过期两种策略。当客户端访问key时,Redis会执行惰性过期检查;同时,后台任务也会周期性地执行定时过期检查,以确保过期key得到及时删除。
- 这种组合策略既能够节省CPU资源,又能够保持内存空间的清洁和数据的一致性。
Redis 的淘汰策略
Redis的淘汰策略(Eviction Policy)定义了当Redis内存使用达到其最大限制(由maxmemory配置指令设置)时,Redis应该如何处理新的写请求(如SET、LPUSH等)。以下是Redis的八种淘汰策略的详细说明:
- noeviction(默认策略):当内存不足以容纳新写入数据时,它不会淘汰任何数据,而是直接返回错误给写请求的客户端。
- allkeys-random:当内存不足以容纳新写入数据时,它会从所有key中随机选择并删除一些key。
- volatile-random:当内存不足以容纳新写入数据时,它只会从设置了过期时间的key中随机选择并删除一些key。
- **volatile-ttl:**当内存不足以容纳新写入数据时,它会在设置了过期时间的key中,挑选那些快要过期的key进行删除。
- allkeys-lru(Least Recently Used):当内存不足以容纳新写入数据时,它会从所有key中选择最久未使用的key进行删除。
- **volatile-lru:**与allkeys-lru类似,但是只会从设置了过期时间的key中选择最久未使用的key进行删除。
- allkeys-lfu(Least Frequently Used):当内存不足以容纳新写入数据时,它会从所有key中选择最不经常使用的key进行删除。LFU是一个基于访问频率的淘汰策略。
- **volatile-lfu:**与allkeys-lfu类似,但是只会从设置了过期时间的key中选择最不经常使用的key进行删除。
volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时 不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。
LRU 算法原理
Redis 为实现近似 LRU 算法,它给每个 key 增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。
上面提到处理 key 过期方式分为定时集中处理和懒惰处理,LRU 淘汰不一样,它的处理 方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。
如何采样就是看 maxmemory-policy 的配置,如果是 allkeys 就是从所有的 key 字典中 随机,如果是 volatile 就从带过期时间的 key 字典中随机。每次采样多少个 key 看的是 maxmemory-samples 的配置,默认为 5。
同时 Redis3.0 在算法中增加了淘汰池,进一步提升了近似 LRU 算法的效果。 淘汰池是一个数组,它的大小是 maxmemory_samples,在每一次淘汰循环中,新随机出 来的 key 列表会和淘汰池中的 key 列表进行融合,淘汰掉最旧的一个 key 之后,保留剩余较旧的 key 列表放入淘汰池中留待下一个循环 。
lru 和 lfu 的区别:
- lru 算法:lru主要基于最近最少访问进行删除,比如key1总访问次数是9,key2的总访问次数是4,那么key2将会被删除。
- lfu 算法:lfu主要基于访问频率进行删除,比如key1总访问次数是9,但是十分钟内key1的访问次数是2,key2的总访问次数是4,但是十分钟内key2的访问次数是4,那么key1将会被删除。