摘要:
缓存优化是提升系统性能的关键策略,其核心在于减少数据库访问、降低延迟、减轻数据库负载并支持高并发。通过将数据存储在内存中,缓存能够提供更快的访问速度,从而显著提升应用程序性能。
此外,缓存策略的优化,如热点操作逻辑的修改和缓存与数据库数据的同步,也是确保数据一致性和系统稳定性的重要手段。针对缓存穿透、缓存雪崩和缓存击穿等问题,文章提出了多种解决方案,包括缓存空值、布隆过滤器、互斥锁和逻辑过期等,以应对不同的缓存挑战,确保系统的高效运行。
一,缓存优化
1,缓存优化的好处:
-
提高性能:缓存可以显著减少数据库的访问次数,因为频繁的数据库访问会导致性能瓶颈。通过将数据存储在内存中,缓存可以提供更快的访问速度,从而提高应用程序的整体性能。
-
减少延迟:缓存数据通常存储在更接近用户的位置(如内存或本地磁盘),这比从远程数据库服务器获取数据要快得多。因此,缓存可以减少网络延迟,提高用户体验。
-
减轻数据库负载:缓存可以减少数据库的读写压力,因为许多请求可以直接从缓存中获取数据,而不需要访问数据库。这有助于防止数据库过载,并延长数据库的寿命。
-
支持高并发:缓存可以处理大量并发请求,因为它将数据存储在内存中,内存的访问速度远高于磁盘。因此,缓存可以支持高并发场景,而不会导致系统崩溃。
2,修改热点操作逻辑,如:查询店铺具体信息操作
逻辑:redis查询,命中返回;未命中,查询数据库,然后更新数据到缓存
public Result queryShopInfoById(Long id) {
//1,查询缓存
String key = CACHE_SHOP_KEY+id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2,判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3,存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4,缓存不存在,查数据库
Shop shop = getById(id);
if (shop == null) {
//5,不存在,返回错误
return Result.fail("店铺不存在!");
}
//6,存在,写入缓存,并返回
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
二,缓存策略
为了解决缓存与数据库数据不一致的问题,我们需要对数据库和缓存中的数据进行同步,在执行更新或者修改操作时要修改数据库和缓存。

总结:
首先开启事务(@Transactional),保证数据库操作和redis操作的原子性,先操作数据库再删除缓存。这种方式在即使没有锁的情况下也只有极低概率发送不一致的问题,因为发生这种情况要满足条件:(1)两个线程并行执行(2)线程1查询缓存刚好失效(3)就在线程1写入缓存时另一个线程先更新了数据库
众所周知,redis的读写操作是非常快的,所以出现(3)的概率极低,且还需要满足3个条件,因此一般采用操作数据库再删除缓存模式。
三,缓存穿透
缓存穿透是指大量请求访问缓存中不存在的数据,导致这些请求直接访问数据库,从而给数据库带来巨大压力甚至崩溃。
常见解决方案:
(1)缓存空值
优点:简单便捷,维护成本低
缺点:占用额外的内存空间,短暂不一致性(设置合适有效期可解决)
(2)布隆过滤器
优点:占用内存少,无多余的key
缺点:实现复杂,可能误判
这两种解决方案都是比较被动的解决方法,一般来说,对于恶意访问,我们更多通过增强id的复杂度,做好数据基础格式校验,加强用户权限校验等等。
实现非常简单,当缓存未命中且数据库中也没有查询到就将该id对应空值存入redis并设置有效期(比较短),然后在查询数据库之前添加判断非null的逻辑,阻止进入数据库
Shop shop = getById(id);
if (shop == null) {
//5,数据库不存在,向redis中存入空值
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回错误
return Result.fail("店铺不存在!");
}
五,缓存雪崩
定义:缓存层(如 Redis)突然大面积失效或不可用,导致大量请求直接穿透到数据库,引发数据库负载激增、响应变慢甚至宕机,最终造成系统整体不可用的连锁反应。
成因:
-
缓存集中过期
大量缓存项在同一时间段内过期,导致同一时刻大量请求发现缓存失效,纷纷转向数据库查询。 -
流量峰值叠加缓存失效
突发流量(如秒杀、大促)与缓存失效同时发生,数据库无法承受瞬时高并发请求。
常用解决方案:随机过期时间、集群高可用、流量控制等。
四,缓存击穿
热点 key在过期瞬间被大量请求同时访问,导致请求直达数据库(如秒杀活动),引发数据库负载激增。
这里介绍两种解决方法:
1,互斥锁
当缓存失效时,仅允许一个线程执行数据库查询并重建缓存,其他线程等待缓存重建完成后再读取新缓存。通过 “互斥” 机制避免大量请求同时访问数据库。
(1)检查缓存是否存在,如果存在直接返回
(2)尝试获取锁,如果获取不成功说明有其他线程正在重建缓存,所以休眠一下,然后再尝试获取缓存。
(3)获取锁成功后还需要再次检测缓存,如果还是无缓存则执行缓存重建逻辑,查询数据库然后存入缓存。
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
//存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的缓存是否是空值
if (shopJson != null) {
//如果是说明数据库中也没有这个值,返回空
return null;
}
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
//获取锁
boolean isLock = tryLock(lockKey);
//是否拿到锁
if (!isLock) {
//没有拿到说明有线程正在进行缓存重建
Thread.sleep(50);
//所以休眠一会然后重新查询缓存
queryWithMutex(id);
}
//拿到锁检查一下缓存是否重建完毕
String reShopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(reShopJson)) {
Shop reshop = JSONUtil.toBean(reShopJson, Shop.class);
return reshop;
}
if (reShopJson != null) {
return null;
}
//缓存还是不存在,查数据库
shop = getById(id);
//模拟延迟
Thread.sleep(200);
if (shop == null) {
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//返回错误
return null;
}
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lockKey);
}
return shop;
}
2,逻辑过期(实际不过期)
不设置物理过期时间,而是在缓存中存储一个 “逻辑过期时间” 字段。当请求发现逻辑过期时,异步触发缓存更新,其他请求直接返回旧数据,避免阻塞。
(1)检查缓存是否存在,如果存在直接返回。
(2)解析逻辑过期时间,反序列化 JSON,获取expireTime,这里得到的对象是JSONObject需要手动转化为shop对象。
(3)判断是否过期,对比当前时间与expireTime,未过期,直接返回业务数据;已过期,触发异步更新缓存(这里采用开启新线程方式完成),同时返回旧数据(允许短暂不一致)。
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(shopJson)) {
return null;
}
//判断是否过期 (逻辑过期)
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期,直接返回店铺信息
return shop;
}
//已过期,需要缓存重建
String lockKey = LOCK_SHOP_KEY + id;
//获取锁
boolean isLock = tryLock(lockKey);
if (isLock) {
String reShop = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(reShop)) {
return null;
}
//判断是否过期 (逻辑过期)
RedisData redisDataCheck = JSONUtil.toBean(shopJson, RedisData.class);
Shop shopCheck = JSONUtil.toBean((JSONObject) redisDataCheck.getData(), Shop.class);
LocalDateTime expireTimeCheck = redisDataCheck.getExpireTime();
if (expireTimeCheck.isAfter(LocalDateTime.now())) {
//未过期,直接返回店铺信息
return shopCheck;
}
//获取锁成功,开启独立线程,重建缓存
CACHE_REBUILD_EXECUTOR.submit(()->{
//重建缓存
try {
this.saveShopToRedis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
});
}
return shop;
}
1554

被折叠的 条评论
为什么被折叠?



