5、Redis实战场景
1、Redis数据丢失的问题
我们知道redis是ap模型,会优先保证高可用,但是在一定场景下可能会存在数据丢失的问题
1.1、持久化
redis提供了两种持久化策略:aof和rdb,组合起来就是四种:
- RDB
- AOF
- No persistence
- RDB + AOF
1.1.1、RDB
RDB是redis默认的持久化方案,是一种基于Redis数据快照的方式,当满足一定的条件的时候,会把当前内存中的数据持久化到磁盘,生成一个快照文件dump.rdb,文件名字和文件路径可在redis.conf文件中配置:
dbfilename dump.rdb
dir ./
1.1.1.1、持久化触发方式
1.1.1.1.1、自动触发
- ① 配置触发
redis自动触发条件同样也是可以在redis.conf文件中进行配置的:
save 900 1 //900s检查一次,至少有1个key被修改就触发
save 300 10 //300s检查一次,至少有10个key被修改就触发
save 60 10000 //60s检查一次,至少有10000个key被修改
- redis关闭的时候触发
这个很好理解,基本上所有的组件在关闭的时候都会做一些清理的善后工作
- fluashall指令触发
这个指令就是所谓的”删库跑路“,它会触发RDB,生成一个空的rdb文件。如果没有开启aof持久化,这个指令的执行将是灾难性的。
1.1.1.1.2、手动触发
- save
- bgsave
这两个指令都是触发备份,不同点在于,save指令是主线程来进行备份,备份期间不在接收其它指令,其它客户端指令阻塞等待,慎重!
bgsave是触发后由子线程去进行备份,不会阻塞其它客户端的指令执行
1.1.1.2、RDB的备份原理
在触发RDB备份的时候,redis会fork一个子进程,子进程将内存中的数据持久化到一个临时文件,当临时文件持久化完毕之后,替换旧的rdb文件
1.1.1.3、RDB的优势
根据官网的描述,RDB持久化方式的优势主要有以下几点:
- RDB在做数据备份非常方便。我们可以每天备份一次,并且将不同版本的备份的数据保存起来
- RDB在做灾难恢复非常方便,因为它是一个独立的压缩文件,我们可以非常方便地将这个文件放到一些可靠的远程存储上,比如s3
- RDB在做备份的时候是起一个新的子进程来做的,主进程并不会参与到磁盘I/O,大大提升了备份效率,而且还不会耽误主进程处理本身的业务
- 在大数据量的时候,相较于aof,rdb的重启速度更快
1.1.1.4、RDB的不足
但是任何事情都有两面性,RDB也并不是完美的。它也有一些不足,比如:
- RDB需要根据配置的时间间隔来检查是否需要持久化,如果这个过程中刚好有数据接入,而此时redis又挂了,那么就会出现数据丢失的问题
- 因为redis的RDB持久化模式在做备份的时候是需要fork一个子进程来做的,需要消耗一定的cpu资源,所以对CPU并不友好
1.1.2、AOF
1.1.2.1、AOF的配置
AOF的出现主要是为了弥补RDB的数据可靠性不足,它的核心思想是将redis的所有操作指令都记录下来,方便在需要的时候做数据恢复。在redis中,aof默认是关闭的,可以在redis.conf文件中开启:
appendonly no //默认关闭,可以进行开启,改为yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof" //AOF文件名
话虽如此,如果真的在执行每个命令的时候都跟磁盘交互一次,那跟关系型数据库比如MySQL又没有太大的区别了,所以redis提供了一些aof策略给用户选择,在redis.conf文件中配置:
# appendfsync always //表示每次写入都执行fsync(刷新)函数 性能会非常非常慢 但是非常安全
appendfsync everysec //默认值,每秒执行一次fsync函数 可能丢失1s的数据
# appendfsync no //作系统保证数据同步到磁盘,速度最快 你的数据只需要交给操作系统就行
AOF想要重写,还需要满足一些条件,也是在redis.conf配置文件中可以配置的:
auto-aof-rewrite-percentage 100 #下一次重写的时候必须是上一次重写的2倍,即超过100%
auto-aof-rewrite-min-size 64mb # 重写的时候文件必须达到64mb
1.1.2.2、AOF重写
我们知道aof是记录所有的操作指令,那么随着时间的推移,这个文件会越来越大,所以越到后面可能加载就越慢,redis肯定需要解决这个问题,解决方案就是进行aof重写
比如下面有一个场景,对一个key设置了100遍,set k 100, set k 1001 ... set k 100,aof记录这100个set的意义对于这样一个key来说貌似没什么太大的意义,还不如直接保存最后一个set,因为最终的数据就是被最后一个set k 100设置的。
所以,redis的aof重写就是把当前内存中的数据重写下来,然后将之前的文件删除。在redis4.0之后,redis会通过RDB的方式来重写AOF,因为核心目的是要记录当前redis内存中的数据,如果数据量特别大指令特别多的时候写那么多aof指令会非常慢,总之目的达到就行,殊途同归。后续追加的指令还是以AOF的形式写入。但是这需要在redis.conf文件中开启
aof-use-rdb-preamble yes //是否开启RDB与AOF混合模式
aof重写流程(redis7.0之前):
- redis服务器fork一个子进程,在一个临时文件中将当前内存中的数据写入新的aof
- 在写新的aof文件的时候,主进程接收到的新的指令会保存在一个缓存区,并且也会追加到旧的aof文件一份,这样一来,及时新写的失败了,旧的也是ok的数据不会丢
- 如果子进程写入完成,主进程会将内存缓存区中的数据追加到新的aof文件中
- 替换旧的aof,再接收到的新的指令就会只写入到新的aof
aof重写流程(redis7.0之后):
新增了临时清单的概念
- redis服务器fork一个子进程来将内存中的数据写入一个基础aof文件
- 主进程开启一个新的增量aof文件,来记录redis后续的增量写入更新。如果子进程写基础aof失败,还可以根据旧的aof+增量aof文件来进行恢复,保证数据安全
- 子进程重写基础aof成功之后,主进程根据子进程重写的基础aof文件和新开启的增量aof文件来构建临时清单,并将其持久化
- redis对清单文件进行原子替换,使重写的结果生效。同时还会清理旧的基础文件和未使用的增量文件
1.1.2.3、AOF的优势
前面介绍了redis的AOF持久化机制,结合redis官网的介绍,总结一下redis有以下优势:
- 数据安全性更好。开启了AOF后,在默认的配置情况下,最多只有1s的数据丢失。而且提供了多种策略,用户可以根据场景灵活配置
- 更好的容错性。如果因为断电或者其它原因导致aof文件写入失败,redis提供了方便的aof修复工具,如
redis-check-aof - 文件可读性更高。因为aof文件里边写入的是redis的指令。即使是失误执行了
flushall清库命令,也可以利用aof文件快速恢复数据 - 自动重写。当文件变得越来越大的时候,redis会在后台自动重写aof,而且这个过程是安全的
1.1.2.4、AOF的不足
虽然aof有诸多的优势,但是不得不承认,redis的aof持久化机制也并不是完美的。比如:
- 同样数据量的情况下,AOF的文件往往比RDB的文件更大
- 异常情况下,持久化和数据恢复速度没有RDB快
- 在7.0版本之前,在重写AOF的期间,新的数据写入会先保存在内存的一个缓存区里,后续才追加到新的aof文件中,这样需要利用一部分的内存资源
- 重写期间,需要跟磁盘进行两次I/O,一次是写如老的AOF文件,一次是写入新的AOF文件。
1.2、脑裂
比如在哨兵模式下,如果这个时候出现脑裂,就会出现两个master,两个master同时接收到写入指令,如果后续因为网络恢复,其中一个变成了slave,要从另外一个master同步数据。这个时候因为是以master的数据为准,这个slave会先清空自己的数据,然后进行同步。这就出现原先写入到这个节点的数据被丢弃了
1.3、淘汰策略
前面提到当redis进行写入操作的时候,如果出现内存不足,就会触发redis的淘汰机制。如果不是采用默认的noeviction淘汰策略,redis就会从数据库里按照配置的淘汰策略选择一批数据,并根据对应的算法计算出一个淘汰值,淘汰值最高的就会被淘汰,即使这个key仍然是有效的。
1.4、主从切换
因为主从同步是异步的,如果主的数据写入成功,则会返回成功(虽然新版本可以配置同步到多少个slave才算成功)。如果这个时候异步的数据同步任务还没来得及执行,因为某种原因进行了主从切换,原来的主变成了从,它需要反向从新的master上同步数据,并将自己的数据情空,这个时候因为写入的数据尚未同步到原先的slave,也就是现在新的master,所以这个新写入的数据就会丢失
总结:redis的数据并不是完全可靠的,不建议用来做业务逻辑,一般用作缓存
2、Redis跟数据库的数据一致性问题
redis经常用来作为缓存,想象一个场景,在高并发场景下,如果一个线程正在读取某个key,大致流程是:先去读redis缓存,如果缓存不存在,则会去查数据库,如果查到了对应的数据,写入redis缓存,并返回结果
2.1、缓存一致性问题
但是如果它查到结果之后,写入缓存,然后有另外一个线程又修改了这个数据,也就是说数据库里边的数据已经更新了,但是现在缓存里边的是之前的旧数据。正常来讲数据库的数据更新了接下来要及时更新缓存,但是在高并发场景下,如果在这个节骨眼上,另有一个线程又来访问这个数据了,它还是先从缓存里边拿,那它拿到的就是旧数据,而不是数据库中的新数据。
这就出现了缓存和数据库数据不一致的问题
2.2.、缓存一致性问题可能想到的方案(X)
在实际的业务开发中,一般如何处理这种缓存一致性问题呢?一般来说可能想到的有几种方案:
2.2.1、DB更新后删除缓存
这个方案是在更新完数据库之后,立即删除redis缓存。这样其它的线程再去访问这个数据的时候,发现缓存中不存在,就会去读取数据库,从而拿到最新的数据。
但是这种方案指标不治本,因为操作数据库和删除缓存这两个操作并不是原子性的
2.2.2、双删
所谓双删就是在更新数据之前先删除redis缓存,然后更新数据库,数据库更新成功之后再次删除redis缓存。但是假设有下面场景:
- 线程A从redis没有发现对应的数据,然后查了数据库,并且拿到了结果数据
- 这个时候线程B准备开始修改这个数据,它首先会删除redis,然后更新,然后再次删除redis里的缓存
- 接下来线程A继续执行,将它查到的数据写入了redis,也就是说redis缓存还是被线程A用旧数据写入了
这就是问题,它并没有解决缓存一致性问题
2.2.3、延时双删
上面提到双删的方案,经过分析它并没有解决缓存一致性问题。可能有的人会想到让线程B等线程A写完redis之后再去删除redis,也就是加了个延时。但关键问题是线程B需要延时多久呢?所以还是有问题
2.2.4、锁
redis本身就是为了追求极致的性能,如果加了同步的锁,就违背了设计的初衷,性能肯定会大大下降
2.3、redis缓存最终一致性方案(√)
上面分析了可能想到的几种处理方案,但是发现最终仍然还是有各种问题。于是着眼redis的根本目标,那就是解决性能问题,要求性能,追求更高的可用性,而不是盲目要求严格的强一致性,更何况,redis本身的数据也并不是完全可靠的,一般我们也只是把它用来做缓存,其实在一定时间段内的缓存不一致也是可以容忍的。
所以就有了一些最终一致性方案
2.3.1、ttl过期时间
方案是给每个key设置一个过期时间,这样的key在一定时间之后就会自动过期,这样后续的线程再来访问这个key,就会因为缓存里没有而从数据库中查询到最新的值
2.3.1、canal最终一致性方案
方案的原理就是canal会监听数据库的binlog,如果数据有变更,可以通知到我们的业务方,业务方再基于canal的数据变更事件来做缓存处理。
2.3.2、基于可靠消息的最终一致性方案
这个方案就是当业务方在更新完数据库之后,向MQ的broker发送一条可靠消息,这样,业务方通过监听这个消息就知道有数据变更了,从而进行相关缓存的处理。这有个前提就是消息的可靠性
3、缓存雪崩问题
缓存雪崩的问题是指当redis中大量的key同时过期,或者redistribution挂了,导致大量的请求打到数据库。
3.1、方案
- redis的高可用,防止redis挂掉,比如用redis的cluster模式
- 设置随机过期时间
4、缓存穿透问题
缓存穿透的问题是指客户端请求一个数据库中不存在的数据,这样先查redis的时候也就不会有,然后对应的请求就打到了数据库,同样也查不到数据。在高并发的场景下,大量的请求这种不存在的数据就会导致数据库的压力非常大
4.1、缓存穿透问题解决方案
- 布隆过滤器
- 设置null值
- 针对恶意攻击,动态封IP
5、慢查询问题
类似MySQL的慢查询,redis也定义了慢查询(不包括网络时间),具体耗时多久算慢查询,可以在edis.conf中通过slowlog-log-slower-than参数指定:
0-记录每个命令
负数-关闭慢查询日志
最大值1000000
10000-默认值,10ms
另外一个参数slowlog-max-len表示最多记录多少个慢查。如果想要查看慢查日志,可以通过下面命令
#查看n条慢查日志
slowlog get n
#慢查日志清理
slowlog reset
慢查询的原因有很多,比如:
- 指令本身问题,如:
keys *、hgetall key - 大对象
将大对象拆分成小对象,建议每个对象不要超过10k
可以通过客户端命令查看大对象
ubuntu@ubuntu-001:~/tools/redis/redis-8.2.3$ ./src/redis-cli --bigkeys # Scanning the entire keyspace to find biggest keys as well as # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec # per 100 SCAN commands (not usually needed). 100.00% |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| Keys sampled: 16 -------- summary ------- Total key length in bytes is 90 (avg len 5.62) Biggest list found "students" has 4 items Biggest hash found "person" has 4 fields Biggest string found "bitmap" has 194807115 bytes Biggest set found "hobby" has 3 members Biggest zset found "friends" has 4 members 1 lists with 4 items (06.25% of keys, avg size 4.00) 1 hashs with 4 fields (06.25% of keys, avg size 4.00) 0 streams with 0 entries (00.00% of keys, avg size 0.00) 10 strings with 194807274 bytes (62.50% of keys, avg size 19480727.40) 2 sets with 6 members (12.50% of keys, avg size 3.00) 2 zsets with 7 members (12.50% of keys, avg size 3.50)可以看到最大的对象在
string的key:bitmap,占用了194807115bytes
阻塞
- 网络阻塞
- cpu阻塞
- redis的aof持久化子进程的时候
数据结构不合理

