redis单机数据库部分有很多常见问题:
- 键的过期时间
- redis的过期键删除策略
- redis的数据淘汰策略
- redis的RDB快照持久化和AOF文件持久化
- redis事件
- redis和数据库双写一致性怎么保证
- 如何应对缓存穿透和缓存雪崩
- redis的并发竞争问题
redis服务器中的数据库
redis服务器将所有的数据库都保存在服务器状态redis.h/redisServer结构的db数组中。每个redisDb结构代表一个数据库。
默认情况下,dbnum为16,即redis服务器默认会创建16个数据库
redis数据库底层就是依照字典(哈希表)来实现的,包括对数据库的增删查改也是基于哈希表之上的。
每个客户端都拥有自己的数据库,默认Redis客户端的目标数据库为0号数据库,用户可以通过SELECT命令切换数据库。
键的过期时间
- 设置键的生存时间可以通过EXPIRE或者PEXPIRE命令。
- 设置键的过期时间可以通过EXPIREAT或者PEXPIREAT命令。
过期键的删除策略
过期键的删除主要有3种策略:
- 定时删除(对内存友好,对CPU不友好)
在设置键的过期时间的同时创建一个定时器,让定时器每次在到达键的过期时间是立即执行对键的删除操作。 - 惰性删除(对CPU友好,对内存不友好)
每次从键空间中获取键的时候,都检查取得的键是否过期,如果过期了,就删除该键;如果没有过期,就返回该键。 - 定期删除(折中)
每隔一段时间程序就对数据库进行一次检查,删除里面的过期键
redis实际是配合使用惰性删除和定期删除两种策略:为了服务器可以更合理的使用CPU时间同时尽量避免浪费内存空间
内存淘汰机制
redis可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
具体有6种数据淘汰策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止驱逐数据 |
一般场景下:
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
redis持久化
redis为什么需要持久化?
这个问题比较好理解,我们知道redis的数据是基于内存的,一旦redis重启退出或者发生了什么故障,内存里的数据会丢失。数据丢失这可不是我们希望看到的结果。所以我们需要持久化技术。
redis提供两种持久化技术将数据存储到硬盘里:
- RDB快照持久化:将某个时间点的数据库状态保存到一个RDB文件中
- AOF文件追加:当redis服务器执行写命令的时候,将写命令保存到AOF文件中
RDB持久化
redis有两个命令可以用于生成RDB文件:
- SAVE:SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止。
- BGSAVE:BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,父进程继续处理命令请求
RDB文件的载入不需要特殊命令,而是服务器在启动时检测到RDB文件存在就自动载入RDB文件。(服务器在载入RDB文件时会处于阻塞状态,直到载入完成)
虽然AOF和RDB持久化可以同时使用,但是由于AOF文件的更新频率比RDB文件高,所以如果服务器开启了AOF持久化功能,则会优先载入AOF文件来持久化;如果AOF持久化功能处于关闭状态,服务器才会载入RDB文件来持久化。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,保存快照的时间会很长。
AOF持久化
AOF持久化主要分为命令追加、文件写入、文件同步三个步骤:
- 命令追加:命令写入aof_buf缓冲区,注意一下,redis不是直接把命令写到硬盘里去的,在redis和硬盘之间还有一个Linux OS 缓存区,要先把命令写入缓存区,然后再写入硬盘
- 文件写入:调用flushAppendOnlyFile函数,考虑是否要将aof_buf缓冲区写入AOF文件中
- 文件同步:考虑是否将内存缓冲区的数据真正写入到硬盘
AOF重写
为什么redis提供了AOF重写功能?
为了解决AOF文件体积膨胀的问题,因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器的运行,AOF文件中内容越来越多,文件体积会越来越大。一旦AOF文件体积过大,不仅会对宿主计算机造成影响,对数据还原所需时间也会较长。这里注意下,虽然redis里面的数据也是在不断增长,但是redis本身实现了相应的内存淘汰策略,比如LRU等,以免数据过于庞大,但是对于AOF文件,之前redis淘汰掉的数据也还是会写到AOF里的,所以需要一个重写的机制。
AOF重写就是指redis可以创建一个新的AOF文件来替代现有的AOF文件新旧两个AOF文件保存的数据库状态相同,但是由于新的AOF文件不包含浪费空间的冗余命令,所以新的AOF文件相对来说体积要小很多。
AOF重写由Redis自行触发(参数配置),也可以用BGREWRITEAOF命令手动触发重写操作,AOF重写不需要对现有的AOF文件进行任何的读取、分析。AOF重写是通过读取服务器当前数据库的数据来实现的。也就是说AOF重写是基于redis当前现有的数据。
AOF后台重写是不会阻塞主进程接收请求的,新的写命令请求可能会导致当前数据库和重写后的AOF文件的数据不一致!
为了解决数据不一致的问题,Redis服务器设置了一个AOF重写缓冲区,这个缓存区会在服务器创建出子进程之后使用。
所以AOF后台重写其实主要是为了解决AOF文件和数据库中的数据不一致的问题。
RDB和AOF的各自优缺点
- RDB优点
- RDB会生成多个数据文件,每个文件代表某一时刻的完整的数据快照,适合做冷备份
- RDB对redis的读写服务影响较小,因为redis主进程会fork出一个子进程,让子进程进行磁盘IO操作进行持久化
- 相比于AOF,直接基于RDB数据文件来重启和恢复redis进程,速度更快
- RDB缺点
- 相比于AOF,如果redis服务器出现故障,那么RDB数据损失的情况会更严重
- RDB每次在fork子进程执行数据文件生成的时候,如果数据文件特别大,可能会导致客户端提供服务出现延时,所以一般执行生成RDB文件的间隔时间不要太长。
- AOF优点
- AOF文件以append-only模式写入命令,不会有磁盘寻址的开销,写入性能高
- 保证尽量少的数据丢失
- AOF缺点
- AOF文件容易过大
使用持久化
一般需要结合RDB和AOF两种持久化机制来进行redis持久化,通过AOF来保证尽量少的数据丢失,通过RDB来支持不同程度(不同时间间隔)的冷备份。
RDB和AOF对过期键的处理策略
RDB对过期键的处理:
- 执行SAVE或者BGSAVE命令创建出的RDB文件,程序会对数据库中的过期键检查,已过期的键不会保存在RDB文件中。
- 载入RDB文件时,程序同样会对RDB文件中的键进行检查,过期的键会被忽略。
AOF对过期键的处理:
- 如果数据库的键已过期,但还没被惰性/定期删除,AOF文件不会因为这个过期键产生任何影响,当过期的键被删除了以后,会追加一条DEL命令来显示记录该键被删除了
- 重写AOF文件时,程序会对RDB文件中的键进行检查,过期的键会被忽略。
redis和数据库双写一致性问题
数据库和缓存双写,则必然会出现数据不一致问题,我们可以采取适当的策略,尽可能降低不一致发生的概率。
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
之所以是删除缓存,是因为在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值
如果缓存删除失败,可以提供一个补偿措施:利用消息队列
什么是缓存穿透和缓存雪崩
缓存穿透
缓存穿透指刻意请求缓存中不存在的数据,导致所有的请求都到了数据库,从而引起数据库连接异常
解决思路:
- 考虑对这些请求进行过滤
- 对这些不存在的数据缓存空数据
缓存雪崩
缓存雪崩指某一时间缓存大面积失效,这是又来了一大波请求,此时请求到达数据库层面,引发数据库连接异常
解决思路:
- 如果是由于同一时期大面积的数据过期导致缓存雪崩,可以考虑观察用户行为,合理设置缓存的过期时间
- 如果是由于服务器宕机导致缓存雪崩,可以考虑使用分布式缓存,确保某一节点的服务器宕机时,其余节点的缓存能够正常访问
- 为了防止系统刚启动大量数据还未进行缓存导致缓存雪崩,可以考虑进行缓存预热处理。
redis怎么保证原子性的
redis能够保证原子性很显然,因为redis是单线程的。
对redis命令的原子性来说,一个操作不可以再分,要么执行,要么执行。而单个线程的任务就是一个一个执行的。
redis并发竞争问题
多客户端同时并发写一个 key,导致数据出错
思路:
- 某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。
- 要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
- 考虑消息队列,在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。
把Redis.set操作放在队列中使其串行化,必须的一个一个执行。