Redis持久化机制及缓存失效解决方案

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis持久化机制及缓存失效解决方案相关的知识,希望对你有一定的参考价值。


Redis集群故障监测及哨兵机制原理解析
Redis海量数据存储方案Redis Cluster

前言

在之前的文章写过redis的实用功能,包括数据结构,主从复制结构,以及应对高并发海量数据场景下的分片redis cluster 集群;本篇文章继续学习redis框架应对缓存失效,以及持久化机制及内存管理出现的问题,以及提供的解决方案及思想。

概述

在redis中缓存失效的原因主要是重启导致数据失效, 解决方案 RDB、AOF持久化机制。以及aof中为什么能保证数据在断电或重启不失效的原因,提供不同的fsync策略:完全没有fsync,每秒fsync,每个查询fsync。使用默认策略fsync时,每秒的写入性能仍然很好(fsync是使用后台线程执行的,并且在没有进行fsync的情况下,主线程将尽力执行写入操作。)保证数据不失效;缓存中常见的内存淘汰与过期管理机制,保证数据更新;以及缓存雪崩分析及解决方案,在redis中利用ehcache 缓存降级,或者Redis备份和快速预热 等都可以避免 缓存出现缓存雪崩问题。

Redis的持久化机制

Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开
启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。用来做数据的持久存储,保证数据不丢失。

 持久化的方式

redis中既有RDB持久化,也有AOF持久化,两者是可以并存的。对于数据要求非常高的情况下,官方是推荐使用AOF,在配置文件中使用 redis.conf中对应的开启方式

​ 

在磁盘中对应的文件名为appendonly.aof 

对应rdb与aof的持久化配置策略

RDB 持久化

RDB 持久化方式能够在指定的时间间隔对你的数据进行快照存储

Redis客户端直接通过命令BGSAVE或者SAVE来创建一个内存快照

  • BGSAVE 调用fork来创建一个子进程,子进程负责将快照写入磁盘,而父进程仍然继续处理命令。
  • SAVE 执行SAVE命令过程中,不再响应其他命令。
在redis.conf中调整save配置选项,当在规定的时间内,Redis发生了写操作的个数满足条件会触发发生
BGSAVE命令
# 900秒之内至少一次写操作
save 900 1 
# 300秒之内至少发生10次写操作
save 300 10
# 60秒之内发生至少10000次
save 60 10000

优点

对性能影响最小 RDB文件进行数据恢复比使用AOF要快很多
缺点
同步时丢失数据 
如果数据集非常大且CPU不够强(比如单核 CPU),Redis在fork子进程时可能会消耗相
对较长的时间,影响Redis对外提供服务的能力。

AOF(append only file)持久化

AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新
执行这些命令来恢复原始的数据
记录每次服务受到的写
BGREWRITEAOF命令可以触发日志重写或自动重写,废除对同一个Key历史的无用命令,重建当前数据集所需的最短命令序列。
意外中断,如果最后的命令只写了一部分,恢复时则会跳过它,执行后面完整的命令。
开启AOF持久化
appendonly yes
AOF策略调整
#每次有数据修改发生时都会写入AOF文件,非常安全非常慢
appendfsync always
#每秒钟同步一次,该策略为AOF的缺省策略,够快可能会丢失1秒的数据
appendfsync everysec
#不主动fsync,由操作系统决定,更快,更不安全的方法
appendfsync no

优点

最安全   容灾  易读,可修改
缺点
文件体积大   性能消耗比RDB高 数据恢复速度比RDB慢

Redis丢失数据的可能性

持久化丢失的可能
RDB方式
快照产生的策略,天生就不保证数据安全
AOF持久化策略
默认每秒同步一次磁盘,可能会有1秒的数据丢失
每次修改都同步,数据安全可保证,但Redis高性能的特性全无
主从复制丢失的可能
异步复制,存在一定的时间窗口数据丢失
网络、服务器问题,存在一定数据的丢失
都会导致数据可能会丢失

Redis中淘汰策略

Redis在内存空间不足的时候,为了保证命中率,就会选择一定的数据淘汰策略,这个和我们操作系统中的页面置换算法类似。

内存分配

不同数据类型的大小限制
  • Strings类型:一个String类型的value最大可以存储512M。
  • Lists类型:list的元素个数最多为2^32-1个,也就是4294967295个。
  •  Sets类型:元素个数最多为2^32-1个,也就是4294967295个。
  •  Hashes类型:键值对个数最多为2^32-1个,也就是4294967295个
# 最大内存控制
maxmemory 最大内存阈值
maxmemory-policy 到达阈值的执行策略

 单位是字节  ,利于精确控制

内存压缩

当内存达到配置量时,会做一个内存压缩 ,这些配置都是压缩优化内存手段。

#配置字段最多512个
hash-max-zipmap-entries 512 
#配置value最大为64字节
hash-max-zipmap-value 64
#配置元素个数最多512个
list-max-ziplist-entries 512
#配置value最大为64字节
list-max-ziplist-value 64

#配置元素个数最多512个
set-max-intset-entries 512
#配置元素个数最多128个
zset-max-ziplist-entries 128 
#配置value最大为64字节
zset-max-ziplist-value 64

 大小超出压缩范围,溢出后Redis将自动将其转换为正常大小,减少cpu的消耗

过期数据的处理策略

Reids的种淘汰策略:

主动处理( redis 主动触发检测key是否过期)每秒执行10次。过程如下:

1. 从具有相关过期的密钥集中测试20个随机密钥 
2. 删除找到的所有密钥已过期
3. 如果超过25%的密钥已过期,请从步骤1重新开始
被动处理:
1. 每次访问key的时候,发现超时后被动过期,清理掉

数据恢复阶段过期数据的处理策略
RDB方式
过期的key不会被持久化到文件中。
载入时过期的key,会通过redis的主动和被动方式清理掉。
    AOF方式
当 redis 使用 AOF 方式持久化时,每次遇到过期的 key redis 会追
加一条 DEL 命令 到 AOF 文件,
也就是说只要我们顺序载入执行 AOF 命令文件就会删除过期的键。

过期数据的计算和计算机本身的时间是有直接联系的。

LRU算法

LRU(Least recently used,最近最少使用):根据数据的历史访问记录来进行淘汰数据
  • 核心思想:如果数据最近被访问过,那么将来被访问的几率也更高。
  • 注意:Redis的LRU算法并非完整的实现,完整的LRU实现是因为这需要太多的内存。
  • 方法:通过对少量keys进行取样(50%),然后回收其中一个最好的key。
 配置方式: maxmemory-samples 5

结构是通过链表+map来进行实现的,当淘汰也是淘汰链表尾的数据

 产生的代价就是 访问、删除都需要遍历链表

LFU算法

LFU(Least Frequently Used)根据数据的历史访问频率来淘汰数据
核心思想:如果数据过去被访问多次,那么将来被访问的频率也更高。
  •  Redis实现的是近似的实现,每次对key进行访问时,用基于概率的对数计数器来记录 访问次数,同时这个计数器会随着时间推移而减小。
  •  Morris counter算法依据: https://en.wikipedia.org/wiki/Approximate_counting_algorithm
  • 启用LFU算法后,可以使用热点数据分析功能。( redis-cli --hotkeys )

 Redis内存回收策略

配置文件中设置:maxmemory-policy noeviction
动态调整:config set maxmemory-policy noeviction
回收的策略
noeviction 客户端尝试执行会让更多内存被使用的命令直接报错
allkeys-lru 在所有key里执行LRU算法
volatile-lru 在所有已经过期的key里执行LRU算法
volatile-lfu 使用过期集在密钥中使用近似LFU进行驱逐
allkeys-lfu 使用近似LFU逐出任何键
allkeys-random 在所有key里随机回收
volatile-random 在已经过期的key里随机回收
volatile-ttl 回收已经过期的key,并且优先回收存活时间(TTL)较短的键

适合缓存的数据

三个维度评判数据是否合适缓存

缓存穿透、缓存雪崩的解决方案 

缓存穿透

缓存失效的两种情况:
  • 高峰期大面积缓存Key失效。(所有请求全部访问后端数据库)

类似12306网站,因为用户频繁的查询车次信息,假设所有车次信息都建立对应的缓存,那么如果所有车次建立缓存的时间一样,失效时间也一样,那么在缓存失效的这一刻,也就意味着所有车次的缓存都失效。通常当缓存失效的时候我们需要重构缓存,这时所有的车次都将面临重构缓存,即出现问题1的场景,此时数据库就将面临大规模的访问。

  • 局部高峰期,热点缓存Key失效。(导致海量的请求直击数据库) 缓存数据有效期到来的那一瞬间

春节马上快到了,抢票回家的时刻也快来临了。通常我们会事先选择好一个车次然后疯狂更新车次信息,假设此时这般车的缓存刚好失效,可以想象会有多大的请求会直怼数据库。

 这会造成数据库的压力是非常大的,有可能导致数据库连接占满,有可能会影响其他功能,大量占用数据库连接,导致其他应用访问该DB数据库时,都会等待着,查询慢的情况。这就是缓存雪崩,缓存失效。

突发重要热点事件   春节发红包 电商降价、抢购、促销活动

缓存雪崩风险

缓存雪崩:因为缓存服务挂掉或者热点缓存失效,从而导致海量请求去查询数据库,
导致数据库连接不够用或者数据库处理不过来,从而导致整个系统不可用。

 解决方案

  • 在redis中设置过期时间,设置不一样的过期时间
  • 不需要大量的请求来恢复缓存,采用互斥锁;把数据库不存在的数据,也缓存起来,短期过滤,过滤一些不存在的key.

  •  拿到锁的线程负责更新缓存其他请求读取备份缓存数据或者执行降级策略;
    备份缓存通常是不设置过期时间的,异步更新的缓存。
  • 限流限次限频。
  • 服务降级  对应用前端请求降级

使用锁的机制对商品进行 处理的形式

public class GoodsService2 {

    private final Logger logger = LoggerFactory.getLogger(GoodsService2.class);

    @Resource(name = "mainRedisTemplate")
    StringRedisTemplate mainRedisTemplate;

    @Autowired
    DatabaseService databaseService;

    Lock lock = new ReentrantLock();

    /**
     * 查询商品库存数
     *
     * @param goodsId 商品ID
     * @return 商品库存数
     */
    // @Cacheable 不管用什么样的方式,核心步骤 1,2,3
    public Object queryStock(final String goodsId) throws InterruptedException {
        // 1. 先从redis缓存中获取余票信息
    	String cacheKey = "goodsStock-"+goodsId;
        String value = mainRedisTemplate.opsForValue().get(cacheKey);
        if (value != null) {
            logger.warn(Thread.currentThread().getName() + "缓存中取得数据==============>" + value);
            return value;
        }

        // 2000 请求
        // 同步 一个个来
        lock.lock(); // 2000 线程 1个线程拿到,1999 等待排队
        try {
            // 再次获取缓存
            value = mainRedisTemplate.opsForValue().get(cacheKey);
            if (value != null) {
                logger.warn(Thread.currentThread().getName() + "缓存中取得数据==============>" + value);
                return value;
            }

            // 拿到锁 重建缓存
            // 2. 缓存中没有,则取数据库
            value = databaseService.queryFromDatabase(goodsId);
            System.out.println(Thread.currentThread().getName() + "从数据库中取得数据==============>" + value);

            // 3. 塞到缓存,120秒过期时间
            final String v = value;
            mainRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
                return connection.setEx(cacheKey.getBytes(), 120, v.getBytes());
            });
        } finally {
            lock.unlock();
        }

        return value;
    }
}

以上是关于Redis持久化机制及缓存失效解决方案的主要内容,如果未能解决你的问题,请参考以下文章

redis常见问题

Redis 缓存失效机制

缓存击穿、穿透、雪崩及Redis分布式锁

细说Redis持久化机制

Redis篇:持久化淘汰策略,缓存失效策略

Redis篇:持久化淘汰策略,缓存失效策略