目录
一、Redis性能管理
1.Info Memory——查看Redis内存使用
2.内存碎片率
3.内存使用率
4.内存回收key
二、Redis缓存雪崩、穿透、击穿、预热
1.缓存雪崩
1.1什么是缓存雪崩
1.2产生原因
1.3实际应用场景
1.4解决方案
1.4.1方案一设置redis的某些key永不过期
1.4.2搭建高可用redis集群+持久化
2.缓存穿透
2.1什么是缓存穿透
2.2产生原因
2.3解决方案
2.3.1方案一使用布隆过滤器
2.3.2方案二缓存空对象或者默认值
3.缓存击穿
3.1什么是缓存击穿
3.2发生场景
3.3解决方案
3.3.1方案一差异失效时间
3.3.2方案二采用互斥锁,双检加锁策略更新
4.缓存预热
4.1缓存预热实现方案
4.1.1方案一批量预加载数据
4.1.2异步预热
4.1.3定时任务预热
4.1.4使用SpringBoot InitializingBean接口实现
5.总结
一、Redis性能管理
1.Info Memory——查看Redis内存使用
[root@localhost ~]#redis-cli
127.0.0.1:6379> info memory
# Memory
used_memory:853304
used_memory_human:833.30K
used_memory_rss:10510336
used_memory_rss_human:10.02M
used_memory_peak:853304
used_memory_peak_human:833.30K
used_memory_peak_perc:100.01%
used_memory_overhead:841078
used_memory_startup:791384
used_memory_dataset:12226
used_memory_dataset_perc:19.74%
allocator_allocated:1661928
allocator_active:2068480
allocator_resident:9039872
total_system_memory:1911857152
total_system_memory_human:1.78G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.24
allocator_frag_bytes:406552
allocator_rss_ratio:4.37
allocator_rss_bytes:6971392
rss_overhead_ratio:1.16
rss_overhead_bytes:1470464
mem_fragmentation_ratio:12.94
mem_fragmentation_bytes:9698056
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:49694
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
2.内存碎片率
Redis内部有自己的内存管理器,为了提高内存使用的效率,来对内存的申请和释放进行管理。
Redis中的值删除的时候,并没有把内存直接释放,交还给操作系统,而是交给了Redis内部有内存管理器。
Redis中申请内存的时候,也是先看自己的内存管理器中是否有足够的内存可用。
Redis的这种机制,提高了内存的使用率,但是会使Redis中有部分自己没在用,却不释放的内存,导致了内存碎片的发生。
内存碎片是由操作系统低效的分配/回收物理内存导致的(不连续的物理内存分配)。
操作系统分配的内存值 used_memory_rss 除以 Redis 使用的内存总量值 used_memory 计算得出。内存值 used_memory_rss 表示该进程所占物理内存的大小,即为操作系统分配给 Redis 实例的内存大小。
除了用户定义的数据和内部开销以外,used_memory_rss 指标还包含了内存碎片的开销, 内存碎片是由操作系统低效的分配/回收物理内存导致的(不连续的物理内存分配)。
举例来说:Redis 需要分配连续内存块来存储 1G 的数据集。如果物理内存上没有超过 1G 的连续内存块, 那操作系统就不得不使用多个不连续的小内存块来分配并存储这 1G 数据,该操作就会导致内存碎片的产生。
跟踪内存碎片率对理解Redis实例的资源性能是非常重要的
- 内存碎片率稍大于1是合理的,这个值表示内存碎片率比较低,也说明 Redis 没有发生内存交换。
- 内存碎片率超过1.5,说明Redis消耗了实际需要物理内存的150%,其中50%是内存碎片率。需要在redis-cli工具上输入shutdown save 命令,让 Redis 数据库执行保存操作并关闭 Redis 服务,再重启服务器。
- 内存碎片率低于1的,说明Redis内存分配超出了物理内存,操作系统正在进行内存交换。需要增加可用物理内存或减少 Redis 内存占用。
3.内存使用率
redis实例的内存使用率超过可用最大内存,操作系统将开始进行内存与swap空间交换。
避免内存交换发生的方法
- 针对缓存数据大小选择安装 Redis 实例
- 尽可能的使用Hash数据结构存储
- 设置key的过期时间
4.内存回收key
内存清理策略,保证合理分配redis有限的内存资源。
当达到设置的最大阀值时,需选择一种key的回收策略,默认情况下回收策略是禁止删除。
配置文件中修改 maxmemory-policy 属性值:
vim /etc/redis/6379.conf
sed -n '598p' /etc/redis/6379.conf
maxmemory-policy noenviction
- volatile-lru:使用LRU算法从已设置过期时间的数据集合中淘汰数据(移除最近最少使用的key,针对设置了TTL的key)
- volatile-ttl:从已设置过期时间的数据集合中挑选即将过期的数据淘汰(移除最近过期的key)
- volatile-random:从已设置过期时间的数据集合中随机挑选数据淘汰(在设置了TTL的key里随机移除)
- allkeys-lru:使用LRU算法从所有数据集合中淘汰数据(移除最少使用的key,针对所有的key)
- allkeys-random:从数据集合中任意选择数据淘汰(随机移除key)
- noenviction:禁止淘汰数据(不删除直到写满时报错)
二、Redis缓存雪崩、穿透、击穿、预热
在实际工程中,Redis缓存问题常伴随高并发场景出现。例如,电商大促、活动报名、突发新闻时,由于缓存失效导致大量请求访问数据库,导致雪崩、击穿、穿透等问题。因此,新系统上线前需预热缓存,以应对高并发,减轻数据库压力。
1.缓存雪崩
1.1什么是缓存雪崩
指在一个高并发的系统中,由于大量的缓存数据在同一时间段内过期或失效,导致大量请求无法从缓存中获取数据,因此大量并发请求可能导致数据库服务器过载,甚至宕机,从而引发整个系统崩溃。
1.2产生原因
- 硬件故障。 比如:机房着火、停电等导致redis挂机。
- 软件设计。redis中有大量key同时过期或者大面积失效。导致缓存中查不到对应key。此时访问压力给到数据库。
1.3实际应用场景
- 金融系统股票价格、汇率等数据实时变动;
- 热点新闻缓存过期时,大量用户访问新闻详情页的请求;
- 电商平台在大促期间,如“618”、“双十一”等。
如果这些数据在缓存中的过期时间设置得过于集中,或者缓存服务器突然宕机,那么在数据过期或缓存失效的瞬间,大量请求会直接访问数据库,导致数据库负载剧增,可能引发雪崩。
1.4解决方案
1.4.1方案一设置redis的某些key永不过期
#简单地使用 SET 命令来添加这个key,不需要调用任何设置过期时间的命令
SET product:500100 "{\"name\":\"ProductName\", \"price\":99.99, \"status\":\"上架中\"}"
值得注意的是,可能因为Redis的内存限制或其他原因(如使用了Redis的LRU淘汰策略)而被删除。因此,在设计系统时,除了考虑key的过期时间外,还需要考虑Redis的内存管理和淘汰策略。
1.4.2搭建高可用redis集群+持久化
集群模式
- 主从模式(Master-Slave)
- 哨兵模式 (Sentinel)
- 分片模式 (Cluster)
2.缓存穿透
2.1什么是缓存穿透
查询一条根本没有的记录。它既不存在于redis中,也不存在数据库中。一般我们的查询顺序是先查redis、再查数据库。每次请求最终都会访问到数据库。造成数据库访问压力。这种现象叫缓存穿透。
2.2产生原因
- 查询记录在redis和数据库均不存在
- 恶意攻击
2.3解决方案
2.3.1方案一使用布隆过滤器
流程说明
- Web端发起请求到App服务应用
- 经过布隆过滤器判断key是否存在
- 若不存在,直接返回null
- 若key存在或者被误判(下文详述),查询redis
- 若redis中有结果,直接返回
- 未查到,则查询数据库返回结果
- 数据库查到结果,返回结果。并将对应key回写redis
- 未查到,则返回null
2.3.2方案二缓存空对象或者默认值
针对要查询的数据,需求层面沟通,可以在Redis里存一个缺省值(比如,零、负数、defaultNull等)。
流程说明:
- 先去redis查键user:xxxxxx没有,再去mysql查没有获得 ,这就发生了一次穿透现象;
- 第一次来查询user:xxxxxx,redis和mysql都没有,返回null给调用者;
- 将 user:xxxxxx,defaultNull 回写redis;
- 第二次查user:xxxxxx,此时redis就有值了。
可以直接从Redis中读取default缺省值返回给业务应用程序,避免了把大量请求发送给mysql处理,增加mysql压力。
但是,此方法只能解决key相同的情况,但是无法应对黑客恶意攻击。
3.缓存击穿
3.1什么是缓存击穿
热点key突然失效了,大量请求暴击数据库。关键词:大量请求、同一个热点key、正好失效。
3.2发生场景
- 某个key以到过期时间,但是还是被访问到
- key被删除,但是被访问到
3.3解决方案
3.3.1方案一差异失效时间
差异失效时间,对于访问频繁的热点key,不设置过期时间。
3.3.2方案二采用互斥锁,双检加锁策略更新
双检锁通常用于解决懒加载中的并发问题,即只有当数据在缓存中不存在时,才进行数据库查询,并且确保只有一个线程进行数据库查询。
- 当大量请求进来,多个线程同时去查询数据库获取同一条数据;
- 在第一个查询数据的请求上使用一个
互斥锁
来锁定; - 其他的线程走到这一步拿不到锁排队等着;
- 第一个线程查询到了数据,然后做缓存;
- 后面的线程进来发现已经有缓存了,就直接走缓存。
注意: 在真实的生产环境中,考虑到缓存一致性和数据一致性的问题,有些情况下可能还需要使用更复杂的并发控制机制。
4.缓存预热
缓存预热是一种缓存优化技术,其核心思想是在系统上线或服务重启之前,提前将相关的缓存数据加载到缓存系统中。这样做的目的是为了避免在实际请求到达时进行缓存项的加载,从而减少了响应时间,提升了系统的性能。
4.1缓存预热实现方案
4.1.1方案一批量预加载数据
缓存预热是一种缓存优化技术,其核心思想是在系统上线或服务重启之前,提前将相关的缓存数据加载到缓存系统中。这样做的目的是为了避免在实际请求到达时进行缓存项的加载,从而减少了响应时间,提升了系统的性能。
public void preloadCache() {
List<String> keys = ... // 获取需要预热的缓存键列表
for (String key : keys) {
Object value = dao.get(key); // 从数据库中获取数据
if (value != null) {
redis.set(key, serialize(value), cacheExpiration); // 将数据存入缓存
}
}
}
4.1.2异步预热
如果预热的数据量很大,或者预热过程比较耗时,可以考虑使用异步任务来执行预热操作。这样可以避免阻塞系统启动,并且可以利用系统的空闲时间来预热缓存。
@PostConstruct
public void startPreloadCacheAsync() {
CompletableFuture.runAsync(() -> preloadCache());
}
4.1.3定时任务预热
对于某些周期性变化的数据,可以用定时任务来定期预热缓存。例如,对于每天更新的数据,可以在每天的某个固定时间执行预热操作。
@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每天执行一次
public void schedulePreloadCache() {
preloadCache();
}
4.1.4使用SpringBoot InitializingBean接口实现
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class CachePreloader implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
// 获取需要预热的缓存键列表
List<String> keysToPreload = ...;
for (String key : keysToPreload) {
// 从数据源获取数据
Object value = dbService.get(key);
if (value != null) {
// 将数据放入缓存
cacheService.put(key, value);
}
}
}
}
5.总结
缓存问题 | 产生原因 | 解决方案 |
---|---|---|
缓存雪崩 | 大量缓存失效,导致数据库过载 | 1. 分散缓存失效时间 2. 多级缓存 3. 缓存高可用 4. 服务降级限流 |
缓存穿透 | 查询不存在的数据,导致数据库过载 | 1. 布隆过滤器 2. 空值缓存 |
缓存击穿 | 热点数据失效,导致数据库过载 | 1. 热点数据永不过期 2. 使用互斥锁 |