黑马点评技术汇总(四)缓存雪崩 && 缓存击穿

缓存雪崩: 是指在同一时段大量的缓存key同时失效(TTL同时到期) 或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:
1. 给不同的Key的TTL添加随机值**(因为在进行批量导入的时候TTL可能都一样)**
2. 利用Redis集群提高服务的可用性
3. 给缓存业务添加降级限流策略**(限流保护服务器)**
**4.**给业务添加多级缓存

缓存击穿: 一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

eg:多个线程同时访问一个商品页面,由于查询数据库速度较慢,多个线程同时访问就会导致多线程同时查询数据库,很多重复操作,增加数据库压力。

解决方案:

1.互斥锁

第一个线程在查询到未命中是先获取互斥锁,在开始查询数据库。
优点: 保证数据的一致性
**缺点:**会导致多个线程进行等待,降低性能

案例:

java 复制代码
   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询商铺信息
     */
    @Override
    public Result queryById(Long id) {
        //缓存穿透
        //Shop shop = queryWithPassThrough(id);

        //互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if(shop==null){
            return Result.fail("商铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 缓存穿透
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id){
        //从redis查缓存
        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:"+id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //判断是否获取成功
            if(!isLock){
                //失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //根据id查询数据库
            shop = getById(id);
            //模拟重建延时
            Thread.sleep(200);
            if(shop == null){
                //不存在,将控制写入redis
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //存在,写入redis
            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.逻辑过期

就是过期后手动处理过期,而不是立刻删除。在第一个线程获取锁后,将查询数据库的任务交给另一个线程,自己返回过期数据。此时其他线程来来访问,发现获取锁失败也返回过期数据。

优点: 提高线程利用率
**缺点:**数据不一致

案例:

缓存命中后判断是否过期,如果过期尝试获取互斥锁,获取失败就返回过期信息,如果获取到锁,就开启一个独立线程进行数据重建,并返回过期信息。

java 复制代码
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期,解决缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id){
        //从redis查缓存
        String key = CACHE_SHOP_KEY+ id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if(StrUtil.isBlank(shopJson)){
            //存在,直接返回
            return null;
        }
        //命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, 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){
            //成功,开启独立线程实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    saveShop2Redis(id,20L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });
        }
        //返回过期商铺信息
        return shop;
    }
java 复制代码
/**
     * 锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    /**
     * 解锁
     * @param key
     * @return
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
java 复制代码
    public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
        //查询店铺数据
        Shop shop = getById(id);
        Thread.sleep(200);
        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        redisData.setData(shop);
        //写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

RedisData

逻辑过期本质是永不过期,所以我们不能直接存在redis中,而是定义一个类。当程序读取到数据后 ,会比较当前时间和数据中的 expireTime。如果当前时间更大,说明数据"逻辑上"已经失效了。

java 复制代码
import lombok.Data;

import java.time.LocalDateTime;

@Data
public class RedisData {
    //过期时间
    private LocalDateTime expireTime;
    private Object data;
}
相关推荐
lzhdim2 小时前
SQL 入门 7:SQL 聚合与分组:函数、GROUP BY 与 ROLLUP
java·服务器·数据库·sql·mysql
lifewange2 小时前
INSERT INTO ... SELECT ...
数据库·sql
Uso_Magic2 小时前
SQLSERVER__EXPLAIN 常用分析案例。
服务器·数据库·sql
IAtlantiscsdn2 小时前
Redis面试题总结
数据库·redis·缓存
2501_924952693 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
诗酒当趁年华3 小时前
langchain核心组件1-智能体
数据库·langchain
流星白龙3 小时前
【MySQL】9.MySQL内置函数
android·数据库·mysql
原来是猿3 小时前
MySQL 在 Centos 7环境安装
数据库·mysql·centos
路小雨~4 小时前
Milvus 向量数据库的官方文档笔记
数据库·学习·milvus