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命令过程中,不再响应其他命令。
# 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 yesAOF策略调整
#每次有数据修改发生时都会写入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 )
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();
// @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) connection -> {
return connection.setEx(cacheKey.getBytes(), 120, v.getBytes());
});
} finally {
lock.unlock();
}
return value;
}
}



