Redis集群故障监测及哨兵机制原理解析
Redis海量数据存储方案Redis Cluster
前言
在之前的文章写过redis的实用功能,包括数据结构,主从复制结构,以及应对高并发海量数据场景下的分片redis cluster 集群;本篇文章继续学习redis框架应对缓存失效,以及持久化机制及内存管理出现的问题,以及提供的解决方案及思想。
概述
在redis中缓存失效的原因主要是重启导致数据失效, 解决方案 RDB、AOF持久化机制。以及aof中为什么能保证数据在断电或重启不失效的原因,提供不同的fsync策略:完全没有fsync,每秒fsync,每个查询fsync。使用默认策略fsync时,每秒的写入性能仍然很好(fsync是使用后台线程执行的,并且在没有进行fsync的情况下,主线程将尽力执行写入操作。)保证数据不失效;缓存中常见的内存淘汰与过期管理机制,保证数据更新;以及缓存雪崩分析及解决方案,在redis中利用ehcache 缓存降级,或者Redis备份和快速预热 等都可以避免 缓存出现缓存雪崩问题。
Redis的持久化机制
![](https://i-blog.csdnimg.cn/blog_migrate/d0032b9372da09b7498482b2c2c07936.png)
持久化的方式
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
优点
AOF(append only file)持久化
![](https://i-blog.csdnimg.cn/blog_migrate/f29dd93d6bd7667506f56378dbfdf1d0.png)
appendonly yes
#每次有数据修改发生时都会写入AOF文件,非常安全非常慢
appendfsync always
#每秒钟同步一次,该策略为AOF的缺省策略,够快可能会丢失1秒的数据
appendfsync everysec
#不主动fsync,由操作系统决定,更快,更不安全的方法
appendfsync no
优点
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次。过程如下:
过期数据的计算和计算机本身的时间是有直接联系的。
LRU算法
- 核心思想:如果数据最近被访问过,那么将来被访问的几率也更高。
- 注意:Redis的LRU算法并非完整的实现,完整的LRU实现是因为这需要太多的内存。
- 方法:通过对少量keys进行取样(50%),然后回收其中一个最好的key。
配置方式: maxmemory-samples 5
结构是通过链表+map来进行实现的,当淘汰也是淘汰链表尾的数据
产生的代价就是 访问、删除都需要遍历链表
LFU算法
- Redis实现的是近似的实现,每次对key进行访问时,用基于概率的对数计数器来记录 访问次数,同时这个计数器会随着时间推移而减小。
- Morris counter算法依据: https://en.wikipedia.org/wiki/Approximate_counting_algorithm
- 启用LFU算法后,可以使用热点数据分析功能。( redis-cli --hotkeys )
Redis内存回收策略
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数据库时,都会等待着,查询慢的情况。这就是缓存雪崩,缓存失效。
缓存雪崩风险
![](https://i-blog.csdnimg.cn/blog_migrate/93b9220107d787634567dff523e6a794.png)
解决方案
- 在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;
}
}