持久化
- AOF 文件的内容是操作命令;
- RDB 文件的内容是二进制数据
AOF
Redis 中 AOF 持久化功能默认是不开启的,需要我们修改
redis.confRedis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里
这保存写操作命令到日志的持久化方式,是 Redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的
Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的
- 避免额外的检查开销
- 不会阻塞当前写操作命令的执行,因为当写操作命令执行成功后,才会将命令记录到 AOF 日志
风险:
- 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险
- 写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给「下一个」命令带来阻塞风险
执行过程:
- Redis 执行完写操作命令后,会将命令追加到
server.aof_buf缓冲区; - 通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
- 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定
- 写回硬盘策略:
- Always,「总是」,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
- Everysec,「每秒」,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,每隔一秒将缓冲区里的内容写回到硬盘;
- No,不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘
- 写回硬盘策略:
- Redis 执行完写操作命令后,会将命令追加到
AOF重写机制
- AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大 Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制
- 机制是在重写时,读取当前数据库中的所有键值对,将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件
- 妙处在于:尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对
AOF后台重写
- 写入 AOF 日志的操作是在主进程完成的,因为写入的内容不多,不太影响命令操作
- Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的
- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,避免阻塞主进程
- 子进程带有主进程的数据副本
- 重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」
- AOF 缓冲区:用于临时存储写命令,定期或根据策略写入 AOF 文件。
- AOF 重写缓冲区:用于在 AOF 重写过程中临时存储新的写命令,确保重写期间的数据不丢失
RDB
Redis 提供两个命令生成 RDB 文件,分别是
save和bgsave,区别是否在「主线程」里执行:- 执行了 save 命令,会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,可以避免主线程的阻塞
Redis 通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令
save 900 1 save 300 10 save 60 10000满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是:
- 900 秒之内,对数据库进行了至少 1 次修改;
- 300 秒之内,对数据库进行了至少 10 次修改;
- 60 秒之内,对数据库进行了至少 10000 次修改
Redis 快照是全量快照,每次执行快照,是把内存中的「所有数据」都记录到磁盘中
RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多
执行快照,数据能被修改吗
- 执行 bgsave 过程,Redis 依然可以继续处理操作命令的,也就是数据是能被修改
- 主线程(父进程)要修改共享数据里的某一块数据(比如键值对
A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对A'),然后主线程在这个数据副本(键值对A')进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对A)写入到 RDB 文件 - bgsave 快照过程中,主线程修改了共享数据,发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照
RDB和AOF合体
RDB 恢复速度快的优点和有 AOF 丢失数据少的优点
开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:
aof-use-rdb-preamble yesAOF 重写日志时,
fork出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
Redis大key
- 对AOF持久化影响
- 当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的
- 对AOF重写和RDB影响
- 都会分别通过
fork()函数创建子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程)- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程
- 都会分别通过
- 其他影响
- 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,很久很久都没有响应。
- 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
- 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多
- 避免:
- 设计阶段,就把大 key 拆分成一个一个小 key
- 对AOF持久化影响
删除和淘汰策略
过期删除策略
Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除
设置过期时间
expire <key> <n>:设置 key 在 n 秒后过期设置 key 在 100 秒后过期;
expire key 100pexpire <key> <n>:设置 key 在 n 毫秒后过期设置 key2 在 100000 毫秒(100 秒)后过期。
pexpire key2 100000expireat <key> <n>:设置 key 在某个时间戳(精确到秒)之后过期key3 在时间戳 1655654400 后过期(精确到秒);
expireat key3 1655654400pexpireat <key> <n>:设置 key 在某个时间戳(精确到毫秒)之后过期key4 在时间戳 1655654400000 后过期(精确到毫秒)
pexpireat key4 1655654400000在设置字符串时,同时对 key 设置过期时间
set <key> <value> ex <n>:设置键值对的时候,同时指定过期时间(精确到秒);set <key> <value> px <n>:设置键值对的时候,同时指定过期时间(精确到毫秒);setex <key> <n> <valule>:设置键值对的时候,同时指定过期时间(精确到秒)
查看某个 key 剩余的存活时间,可以使用
TTL <key>命令# 设置键值对的时候,同时指定过期时间位 60 秒 > setex key1 60 value1 OK # 查看 key1 过期时间还剩多少 > ttl key1 (integer) 56 > ttl key1 (integer) 52取消 key 的过期时间,使用
PERSIST <key>命令。# 取消 key1 的过期时间 > persist key1 (integer) 1 # 使用完 persist 命令之后, # 查下 key1 的存活时间结果是 -1,表明 key1 永不过期 > ttl key1 (integer) -1
常见三种过期删除策略,并不是redis会用
- 定时删除:
- 在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作
- 优点:
- 保证过期 key 会被尽快删除,内存可以被尽快地释放
- 缺点:
- 过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间
- 惰性删除:
- 不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key
- 优点:
- 每次访问时,才会检查 key 是否过期,只会使用很少的系统资源
- 缺点:
- 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费
- 定期删除:
- 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key
- 优点:
- 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用
- 缺点:
- 内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少
- 定时删除:
Redis过期删除
Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡
Redis 实现惰性删除
int expireIfNeeded(redisDb *db, robj *key) { // 判断 key 是否过期 if (!keyIsExpired(db,key)) return 0; .... /* 删除过期键 */ .... // 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除; return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); }Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:
- 如果过期,则删除该 key,至于选择异步删除,还是选择同步删除,根据
lazyfree_lazy_expire参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 客户端; - 如果没有过期,不做任何处理,然后返回正常的键值对给客户端
- 如果过期,则删除该 key,至于选择异步删除,还是选择同步删除,根据
Redis实现定期删除
do { //已过期的数量 expired = 0; //随机抽取的数量 num = 20; while (num--) { //1. 从过期字典中随机抽取 1 个 key //2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++ } // 超过时间限制则退出 if (timelimit_exit) return; /* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */ } while (expired > 20/4);每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key
- 在 Redis 中,默认每秒进行 10 次过期检查一次数据库
- 可通过 Redis 的配置文件 redis.conf 进行配置
- 数据库每轮抽查时,会随机选择 20 个 key 判断是否过期
- 在 Redis 中,默认每秒进行 10 次过期检查一次数据库
内存淘汰策略
配置文件 redis.conf 中,可以通过参数
maxmemory <bytes>来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存,才会触发内存淘汰策略不进行数据淘汰的策略
- noeviction(Redis3.0之后,默认的内存淘汰策略) :当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入
进行数据淘汰的策略
- 在设置了过期时间的数据中进行淘汰:
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
- 在所有数据范围内进行淘汰:
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值
- 在设置了过期时间的数据中进行淘汰:
查看redis使用的内存淘汰策略
使用
config get maxmemory-policy,查看当前 Redis 的内存淘汰策略 :127.0.0.1:6379> config get maxmemory-policy 1) "maxmemory-policy" 2) "noeviction"
LRU算法和LFU算法
- LRU
- Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据
- 传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素
- LFU
- Least Frequently Used 翻译为最近最不常用,LFU 算法是根据数据访问次数来淘汰数据的,核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”
- LRU
缓存异常
Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能
在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情
缓存雪崩
- 为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存
- 大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,就是缓存雪崩
- 原因及解决方案:
- 大量数据同时过期
- 均匀设置过期时间
- 给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间,对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数
- 互斥锁:
- 当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存
- 实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象
- 后台更新缓存:
- 让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新 缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”
- 均匀设置过期时间
- Redis故障宕机:
- 服务熔断或请求限流机制
- 启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误
- 为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制
- 构建 Redis 缓存高可靠集群
- 主从节点的方式构建 Redis 缓存高可靠集群
- 服务熔断或请求限流机制
- 大量数据同时过期
缓存击穿
- 业务通常会有几个数据被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据
- 缓存中的某个热点数据过期,大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,缓存击穿
- 解决方案:
- 互斥锁方案:
- 保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
- 不给热点数据设置过期时间:
- 后台异步更新缓存
- 在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
- 互斥锁方案:
缓存穿透
当用户访问的数据,既不在缓存中,也不在数据库中,就是缓存穿透的问题
发送原因
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务
解决方案:
- 非法请求的限制
- API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误
- 缓存空值或者默认值
- 可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:
- 非法请求的限制
布隆过滤器工作过程:
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器通过 3 个操作完成标记:
使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
将每个哈希值在位图数组的对应位置的值设置为 1;