Redis篇
Redis有哪些使用场景?
一、缓存穿透
例如查询根据文章,通常情况下,应该是先查redis,若redis中有,则直接返回。若redis中没有,那么就需要去数据库中查,从数据库中查到数据后,将数据保存到redis中,然后再返回给客户端。
这时候就会出一个一个问题,如果查询一个不存在的数据,数据库中查不到数据也不会直接写入到缓存,就会导致每次请求都需要查询数据库,就是这缓存穿透。
解决方案
-
缓存空数据。查询放回的数据为空,任给这个空结果进行缓存。
优点:简单
缺点:消耗内存。可能会导致不一致的问题。
-
布隆过滤器。在查询redis之前,先查布隆过滤器。在缓存预热的时候,需要给数据添加到布隆过滤器中。
优点:内存占用少,没有多余的key。
缺点:实现起来复杂,存在误判。
缓存穿透是指查询一个不存在的数据,如果从数据库中找不到这个数据则不会写入缓存,这样就导致这个不存在的数据每次请求都需要到数据库中查询,可能会导致数据库挂掉。这种情况大概率是受到了攻击。
通常使用布隆过滤器来解决。
什么是布隆过滤器呢?
布隆过滤器主要是检索一个数据是否存在一个集合中。当时是使用redission实现的布隆过滤器。它的底层主要是初始化一个较大的数据,里面存放0或者1,默认是0。当一个key来了后经过3次hash计算,模数组长度找到数据的下标,将数组中的0改为1,这样话,三个数组的位置可以标明一个key是否存在。查找的过程也是一样的。
当然,这也是有缺点的:
- 布隆过滤器可能会产生一定的误判,一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不然就得增加数组的长度。其实已经很划算了,5%以内的误判一般项目也能够接受,不至于高并发下压倒数据库。
- 删除元素会影响其他的key。可以定期异步重建布隆过滤器。
怎么减少布隆过滤器的误判?
- 增加二进制位数。
- 增加Hash的次数。
二、缓存击穿
给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间给数据库压垮。查询数据库的时候,不会是重建缓存吗?在重建缓存的时候,消耗的时间过长依然可能会给数据库压垮。
解决方案
-
互斥锁:在重建缓存的时候,添加一个互斥锁。

优点:数据的强一致性。
缺点:重建的缓存的时候,性能较低。
-
逻辑过期:缓存数据的时候,不设置过期时间,但是会额外添加一个字段,这个字段存储的是过期时间。接收到请求后,判断是否过期,无论是否过期都会直接放回数据。但是当过期的时候,会添加一个互斥锁,获取锁成功后,新建一个线程去重建缓存,获取锁失败则什么都不做。

优点:高可用,性能好。
缺点:数据可能会不一致。
缓存击穿就是对于设置了过期时间的key,当key过期的同时,恰好有大量对这个key的并发请求过来,这些请求发现缓存过期一般都会从数据库查询并重建缓存,这个时候大并发请求可能瞬间吧数据库压垮。
解决方案有两种。
第一种可以使用互斥锁,当缓存失效时,不立即去查询数据库,而是先使用Redis的setnx去设置一个互斥锁,当操作成功返回时再进行查询数据库并重建缓存,否则重试获取缓存的方法。
第二种是设置当前key逻辑过期。大概思路如下:
- 在设置key的时候,设置一个过期时间字段一块存入缓存汇总,不给key设置过期时间。
- 当查询的时候,从redis取出数据后判断时间是否过期。
- 如果过期则开通另一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新的。
三、缓存雪崩
大量的key同时过期或者redis服务器宕机,导致大量的请求到达数据库,带来巨大压力。
大量的key同时过期
在写缓存的时候,一般情况下,会根据业务的访问特点,给每种业务数据预制一个过期时间,在写缓存的时候把这个过期时间带上,让缓存数据在这个固定的过期时间后淘汰。因为缓存数据是逐步写入的,所以也是逐步过期的,不会出现雪崩的问题。
但在特殊场景,一大批数据从DB批量加载到缓存,过期时间一到,这批数据就会一起过期,针对这批数据的所有请求都不会命中缓存,穿透到DB,DB效率远远低于缓存,压力增大,甚至宕机。
比如新业务上线的时候,需要缓存预热,这时候就会从DB加载大批数据到缓存中。
解决方案
- 给不同的key的过期时间随机加上一段时间。 => 过期时间 = 基准过期时间 + 随机时间
- 利用Redis集群提高服务的可用性。
- 给缓存业务添加降级限流策略。
- 给业务添加多级缓存。
注:降级限流可以做为系统的保底策略,适用于穿透、击穿、雪崩。
如何确定基准过期时间?
- 缓存数据的更新频率。热点新闻类的时间可以设置稍微长一点。
- 缓存数据的大小和内存容量。数据大,容量小,时间可以短一些,短才可以及时释放内存空间。反过来时间可以稍长,这样可以提高缓存的命中率。
服务器宕机
-
缓存不支持rehash导致
由于较多的缓存节点不可用,请求穿透导致DB也过载不可用,最终导致整个系统雪崩不可用。
缓存节点不支持rehash,较多缓存节点不可用时,大量cache访问失败,这些请求会进一步访问DB,而DB可承载的访问量远远小于Cache,请求量过大,就很容易造成DB过载,大量慢查询,最终阻塞甚至崩溃,从而导致服务异常。
-
缓存支持rehash导致
大多更流量洪峰有关,流量洪峰到达,引发部分缓存节点过载,然后因rehash扩散到其他缓存节点,最终导致整个缓存体系异常。
在缓存分布设计时,会选择一致性Hash分布方式,同时在部分节点异常时,采用rehash策略,即把异常节点请求平均分散到其他缓存节点。在一般情况下,一致性Hash分布+rehash策略可以很好得运行,但在较大的流量洪峰到临之时,如果大流量key比较集中,正好在1~2个节点,容易将这些缓存节点的内存、网卡过载,缓存节点异常崩溃,然后这些异常节点下线,这种大流量key请求又被rehash到其他缓存系欸但,进而导致其他缓存节点也被过载崩溃,缓存异常持续扩散,最终导致整个缓存体系异常,无法对外提供服务。
四、数据不一致
1. 原因分析
- DB和缓存不一致大多根缓存更新异常或者更新策略有关。比如更新DB后,更新缓存的时候失败了,从而导致缓存中的是老数据。
- 集群模式下,如果系统采用一致性Hash分布,同时采用rehash自动漂移策略,在节点多次上下线后,也会产生脏数据。缓存有多个副本时,更新某个副本失败,也会导致这个副本的数据是老数据。
2. 业务场景
在缓存机器的带宽被打满,或者机房出现网络波动的时,缓存更新失败,新的数据没有写入缓存,就会导致缓存和DB的数据不一致。
缓存rehash时,某个缓存机器反复异常,多次上下线。这样,一份数据存在多个节点,且每次rehash只更新某个节点,导致一些缓存节点产生脏数据。
3. 解决方案
3.1 延迟双删
更新DB后,马上删除缓存里的数据,然后通过某种策略(使用MQ),再删除一次。
如果不用延迟双删,有下面几个问题。
-
是先删除缓存还是先修改数据库呢?
无论是先删除缓存还是先修改数据库,都可能出现读到脏数据的情况。


-
为什么要删除两次缓存?
无论先删除缓存还是先修改数据库,都会出现脏数据的情况。
-
为什么要延迟删除?
数据库一般是主从模式,需要等一会让数据库之间同步。但时间不太好控制,在同步的时候还是有脏数据的风险。
一致性要求高
- 互斥锁,在写或者读数据的时候,添加一个分布式锁。
- 使用redission的读写锁(共享锁和排他锁),是对互斥锁稍微优化一下。
允许延迟一致
- 异步通知保证数据的最终一致性。需要保证MQ的可靠性。修改数据库后,通知缓存删除。
- Canal的异步通知。不需要修改业务代码,伪装成MySQL的一个从节点监听binlog,canal通过读取binlog数据更新缓存。
3.2 闪电缓存
将缓存失效的时间设置非常短,比如3-5秒。
3.3 不使用rehash
不使用rehash,而采用缓存分层策略,尽量避免脏数据的产生。
五、Hot Key
并发请求非常大,访问数据完全相同的请求称为热点查询。
六、Big Key
大Key,是指缓存访问时,部分key的value过大,读写、加载易超时的现象,影响到Redis效率和可用性。
持久化
Redis提供两种数据持久化的方式:RDB和AOF。
RDB
Redis的数据库快照,简单来说就是给内存中的数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
使用redis-cli的save或者bgsave命令。save由主进程来执行RDB,会阻塞所有命令。bgsave开启子进程执行RDB,避免主进程受影响。
Redis中内部可以自动地触发RDB,可以在redis.conf文件中找到,格式如下
# 900秒内,如果至少有一个1key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000
RDB执行原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据(内部复制了主进程的页表)。完成fork后读取数据并写入到RDB文件。
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,共享主进程内存。
- 当主进程执行写操作时,会拷贝一份数据,执行写操作。
AOF
Append Only File(追加文件)。Redis在处理的每一个命令都会记录到AOF文件中去,可以看作是命令日志文件。
AOF默认是关闭的,需要修改redis.conf配置文件。
AOF有三种记录的命令的策略。
| 配置项 | 刷盘时机 | 优点 | 缺点 |
|---|---|---|---|
| always | 同步刷盘 | 可靠性高,几乎不会丢数据 | 性能影响大 |
| everysec | 每秒刷盘 | 性能适中 | 最多丢失一秒的数据 |
| no | 操作系统控制 | 性能最好 | 可靠性差,可能丢失大量数据 |
因为AOF是记录的命令,对同一个key的多次写操作只有最后一次有效。通过执行bgrewirteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果。
Redis也会在触发阈值时自动重写AOF文件。可以在redis.conf中配置。
auto-aof-rewirte-precentage 100
auto-aof-rewirte-min-size 64mb
| RDB | AOF | |
|---|---|---|
| 持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
| 数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘速度 |
| 文件大小 | 会有压缩,小 | 记录命令,大 |
| 宕机恢复速度 | 快 | 慢 |
| 数据恢复优先级 | 低,数据完整性不然AOF | 高,数据完整性更高 |
| 系统资源占用 | 高,大量占用CPU和内存资源 | 低,主要是磁盘IO资源但AOF重写时会占用大量CPU和内存资源 |
| 使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高 |
两种各有优缺点,通常结合使用。
数据过期
-
惰性删除
使用该key的时候,会检查是否过期,过期了就删除,否则就返回value。
优点:对CPU友好,只会在使用该key的时候才进行过期检查,对于许多用不到的key不会消耗过多的CPU的资源去检查是否过期。
缺点:对内存不友好,如果一个key已经过期,但是长期用不到,该key就会一起存在内存中。
-
定期删除
每隔一段时间,检查部分(随机)key,删除里面已经过期的key。
有两种模式:
SLOW模式:默认的执行频率是10hz(1秒内执行10次),每次不超过25ms,可以通过配置文件redis.conf的hz选项来调整次数。
FAST模式:执行频率不固定,但是两次间隔不低于2ms,每次耗时不超过1ms。
两种模式的目的是尽量少地影响主进程。
优点:可以限制删除操作的执行时长和频率来减少删除操作对CPU的影响。定期删除也能有效的释放过期的key占用的内存。
缺点:难以确定删除操作执行的时长和频率。
Redis的默认策略是两种配合使用。
淘汰策略
当Redis内存不够用时,此时再向Redis添加新的key,那么Redis就会按照某一种策略和规则将内存中的数据删掉,这种数据删除规则称之为淘汰策略。
有八种不同的策略,
- noeviction:不淘汰任何key,但是当内存不够用时不允许写入新的数据。默认
- volatile-ttl:对进行设置了TTL的key,比较key的剩余TTL值,越小越先被淘汰。
- allkeys-random:全体key,随机淘汰。
- volatile-random:对设置了TTL的key,随机淘汰。
- allkeys-lru:全体key,基于LRU算法进行淘汰。
- volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-LFU:全体key,基于LFU算法进行淘汰
- volatile-LFU:对设置了TTL的key,基于LFU算法进行淘汰
LRU(Least Recently Used):最近最少使用。当前时间减去最后一次访问时间,这个值越大淘汰优先级越高
LFU(Least Frequency Used):最少频率使用。统计每个key的访问频率,值越小越淘汰优先级越高。
使用建议
- 如果业务中有明显的冷热数据区分,建议使用allkeys-lru。充分利用LRU算法的优势,把最近常访问的数据留在缓存中。通常使用该策略
- 如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用allkeys-random。
- 如果业务中有置顶需求,可以使用volatile-lru,同时置顶数据不设置过期时间。
- 如果业务中有短时高频访问数据,可以使用allkeys-lfu或者volatile-lfu。
2. 分布式锁
主要是利用Redis的setnx的命令实现的。setnx是set if not exists的简写。
redssion实现的分布式锁是可重入的,在存储的时候,使用Hash结构来存储线程信息和重入次数。
通过红锁来确保主从一致。
红锁:不能只在一个Redis实例上创建锁,应该在多个redis实例上创建锁(n / 2 + 1),避免在一个redis实例上枷锁。
红锁实现复杂,性能差,运维繁琐。官方不建议直接使用。
Redis是AP思想,即高可用优先,最终数据一致性。要想保证强一致性,建议使用CP思想的zookeeper。
其他
集群
主从复制
单节点Redis的并发能力有效,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。一帮是一个主节点,多个从节点,主节点负责写数据,从节点负责读数据。
全量同步
是指从节点与主节点第一次建立连接的过程,过程如下:
- 从节点请求主节点同步数据。其中从节点会携带直接的replication id和offset。
- 主节点判断是否是第一次请求,依据就是replication id是否一致。是第一次就与从节点同步版本信息(replication id 和 offset)。若replication id一样,则表示同一数据集。故从节点的replication id和主节点的一定是一样的。offset就是偏移量了。
- 主节点执行bgsave,生成rdb文件后,发送给从节点去执行。从节点先给自己的数据情况,然后执行主节点发送过来的rdb文件。
- 在执行rdb期间,主节点会以命令的方式记录到缓冲区(一个日志文件)。
- 把生成好的日志文件发送给从节点进行同步。
增量同步
当从节点重启后,数据就不一致了,这时候过程如下:
- 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset的值。
- 主节点从日志中获取offset值后的数据,发送给从节点同步。
哨兵模式
Redis提供了哨兵机制来实现主从集群的自动故障恢复。解决高可用的问题。
一般会部署三台哨兵节点。
哨兵的三个特点
- 监控:哨兵会不会检查主从节点,看是否按预期工作。
- 自动故障恢复:如果主节点故障,哨兵节点会将一个从节点提升为主节点。当故障实例恢复后,也以新的主节点为主。
- 通知:哨兵节点充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端。
哨兵基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某个哨兵节点发现某实例未在规定的时间内响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的哨兵节点都认为该实例主观下线,则该实例客观下线。quorum值最好超过哨兵实例数量的一半。
哨兵选主规则:
- 首先判断主节点与从节点断开时间长短,如超过指定值就排除该从节点
- 判断从节点的slave-priority值,越小优先级越高。
- 如果slave-priority一样,则判断slave节点的offset值,越大优先级越高。
- 判断slave节点的运行id大小,越小优先级越高。
哨兵脑裂问题
是由于主节点、从节点和哨兵节点处于不同的网络分区,导致哨兵节点感受不到主节点的心跳,所以通过选举的方式提升了一个从节点为主节点,这样就存在了两个主节点,就像大脑分裂了一样,这样会导致客户端还在向老的主节点写数据,新节点无法同步数据,当网络恢复后,哨兵会将老的主节点变成从节点,这时在从新的主节点同步数据,就会导致数据丢失。
解决:可以修改redis的配置文件,设置最小的从节点数量(例如最少需要一个从节点才能同步数据)以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
分片集群
解决海量数据存储问题和高并发写的问题。
分片集群特点:
集群中有多个主节点,每个主节点保存不同数据
每个主节点都可以有多个从节点。
主节点直接通过ping监测彼此健康状态。
客户端请求可以访问集群任意节点,最终都会被转发到正确节点。(自动路由)
Redis分片集群引入了hash槽的概念,Redis集群有16384个hash槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点都负责一部分hash槽。读写数据:根据key的有效部分计算hash值,对16384取余(有些部分,如果key前面有大括号,大括号的内容就是有效部分,没有则key本身就是有效部分)余数做插槽,寻找插槽所在实例。
Redis为什么快
- Redis是纯内存操作,执行速度非常快。
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题。
- 使用I/O多路复用模型,非阻塞IO。
IO多路复用模型
Redis是纯内存操作的,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求。
TODO …