Java --- redis7之缓存预热+雪崩+穿透+击穿

目录

一、缓存预热

二、缓存雪崩 

三、缓存穿透 

3.1、解决方案 

 3.1.1、空对象缓存或者缺省值

3.1.2、Goolge布隆过滤器Guava解决缓存穿透 

 四、缓存击穿 

4.1、危害 

 4.2、解决方案

 4.3、模拟百亿补贴活动案例

一、缓存预热

场景:MySQL有N条新记录,redis没有

1、MySQL做数据新增 ,redis利用回写机制,让它逐步实现100条新增记录的同步,部署发布版本的时候,自己人提前做一次redis同步。

2、通过中间件或者程序自行完成。

3、使用白名单

二、缓存雪崩 

场景:

1、redis主机挂掉,redis全盘崩溃,偏硬件运维。

2、redis中有大量key同时过期大面积失效,偏软件开发。 

预防和解决:

1、redis中key设置为永不过期或者过期时间错开

2、redis缓存集群实现高可用:①、主从+哨兵。②、redis集群。③、开启redis持久化机制aof/rdb,尽快恢复缓存集群。

3、多缓存结合预防雪崩:ehcache本地缓存+redis缓存。

4、服务降级:Hystrix或者阿里sentinel限流和降级

5、用钱购买阿里云---》云数据库redis。

三、缓存穿透 

查询一条或N条数据,redis中没有,mysql中也没有,但请求每次都会去查询数据库,导致后台数据库压力暴增,就是缓存穿透。 

3.1、解决方案 

 3.1.1、空对象缓存或者缺省值

 黑客或恶意攻击:

 黑客会对系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库查询。可能会导致数据库压力大而宕掉。

key相同攻击系统:

第一次访问到MySQL,空对象缓存后第二次就返回defaultNull缺省值,避免MySQL被攻击,不用到数据库中去走一圈了。

key不同攻击系统:

由于存在空对象缓存和缓存回写,redis中的垃圾key会越写越多(设置key过期时间)。

3.1.2、Goolge布隆过滤器Guava解决缓存穿透 

 白名单过滤器:

1、误判问题,概率小可以接受,不能从布隆过滤器中删除

2、全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据就是返回null

 代码实现:

改pom

 <!--google开源guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

测试案例:

  /**
     * 创建Guava布隆过滤器测试
     */
    @Test
    public void testGuavaAndBloomFilter(){
        //创建Guava布隆过滤器
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);
        //判断指定的元素是否存在
        System.out.println(bloomFilter.mightContain(1));
        System.out.println(bloomFilter.mightContain(2));
        System.out.println("加入后");
        //将元素添加进布隆过滤器
        bloomFilter.put(1);
        bloomFilter.put(2);
        System.out.println(bloomFilter.mightContain(1));
        System.out.println(bloomFilter.mightContain(2));
    }

 使用百万数据测试:

@Service
@Slf4j
public class GuavaBloomFilterServiceImpl implements GuavaBloomFilterService {
    public static final Integer  _1W = 10000;
    //定义guava布隆过滤器,初始容量
    public static final Integer  SIZE = 100 * _1W;
    //误判率,误判率越小误判个数越少
    public static double fpp = 0.03;
    //创建guava布隆过滤器
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE,fpp);
    @Override
    public void guavaBloomFilter() {
        //在guava布隆过滤器加入一百万白名单数据
        for (int i = 0; i < SIZE; i++) {
           bloomFilter.put(i);
        }
        //取十万个不在合法范围的数据测试
        ArrayList<Integer> list = new ArrayList<>(10 * _1W);
        //测试
        for (int i = SIZE + 1; i < SIZE+(10 * _1W) ; i++) {
            //判断在guava布隆过滤器是否存在
            if (bloomFilter.mightContain(i)){
                log.info("被误判:{}",i);
                list.add(i);
            }
        }
        log.info("误判总数为:{}",list.size());
    }
}
@RestController
@Api(tags = "Google的guavaBloomFilter")
@Slf4j
public class GuavaBloomFilterController {
    @Autowired
    private GuavaBloomFilterService guavaBloomFilterService;
    @RequestMapping(value = "/guavaBloomFilter",method = RequestMethod.GET)
    @ApiOperation("guava布隆过滤器插入100万样本数据和10万测试数据")
    public void guavaBloomFilter(){
        guavaBloomFilterService.guavaBloomFilter();
    }
}

结论 :100000 / 3033 = 0.03033

源码分析:

 四、缓存击穿 

大量的请求同时查询一个key时,而这个热点key正好失效,就会导致大量的请求都打到数据库上面去。 

4.1、危害 

会造成某一刻数据库请求量过大,压力剧增。 

 4.2、解决方案

 热点key失效的原因:

1、时间到了自然被清除但还是被访问到

2、删除的key,刚好被访问。

方案1:

差异失效时间,对于访问频繁的热点key,不设置过期时间。

方案2:

互斥更新,采用双检加锁策略。

 4.3、模拟百亿补贴活动案例

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "百亿补贴活动模拟")
public class Product {
    //商品id
    private Long id;
    //商品名
    private String name;
    //商品价格
    private Integer price;
    //商品描述
    private String detail;
}
@Service
@Slf4j
public class BYBTTaskServiceImpl implements BYBTTaskService {
    public static final String BYBT_KEY = "bybt";
    public static final String BYBT_KEY_A = "bybt:a";
    public static final String BYBT_KEY_B = "bybt:b";
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 模拟MySQL添加商品
     * @return
     */
    public List<Product> getProductFromById(){
        List<Product> list = new ArrayList<>();
        for (int i = 1; i <= 20; i++) {
            Random random = new Random();
            int id = random.nextInt(1000);
            Product product = new Product((long) id, "product" + i, i, "鸽子蛋" + i);
            list.add(product);
        }
        return list;
    }
//    @PostConstruct
    public void initBYBT(){
        log.info("启动定时器模拟百亿补贴活动开始。。。");
        //使用线程模拟定时任务,后台任务定时将MySQL里的活动商品刷新到redis中
        new Thread(()->{
            while (true){
                //从MySQL中查询数据,写入redis
                List<Product> list = this.getProductFromById();
                //删除过期key
                redisTemplate.delete(BYBT_KEY);
                //使用redis的list数据结果存储最新数据
                redisTemplate.opsForList().leftPushAll(BYBT_KEY,list);
                //暂停2分钟模拟,模拟百亿补贴参加活动商品
                try {
                    TimeUnit.MINUTES.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        },"a1").start();
    }
    @PostConstruct
    public void initBYBTAB(){
        log.info("启动AB定时器模拟百亿补贴活动开始。。。" + DateUtil.now());
        //使用线程模拟定时任务,后台任务定时将MySQL里的活动商品刷新到redis中
        new Thread(()->{
            while (true){
                //从MySQL中查询数据,写入redis
                List<Product> list = this.getProductFromById();
                //先更新缓存B,让缓存B过期时间超过缓存A,缓存A突然失效,还有缓存B,以防止缓存击穿
                redisTemplate.delete(BYBT_KEY_B);
                redisTemplate.opsForList().leftPushAll(BYBT_KEY_B,list);
                //设置过期时间
                redisTemplate.expire(BYBT_KEY_B,86410L,TimeUnit.SECONDS);
                //更新缓存A
                redisTemplate.delete(BYBT_KEY_A);
                redisTemplate.opsForList().leftPushAll(BYBT_KEY_A,list);
                //设置过期时间
                redisTemplate.expire(BYBT_KEY_A,86400L,TimeUnit.SECONDS);
                //暂停2分钟模拟,模拟百亿补贴参加活动商品
                try {
                    TimeUnit.MINUTES.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"a1").start();
    }
}
@RestController
@Slf4j
@Api(tags = "百亿补贴活动模拟")
public class BYBTTaskController {
    public static final String BYBT_KEY = "bybt";
    public static final String BYBT_KEY_A = "bybt:a";
    public static final String BYBT_KEY_B = "bybt:b";
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 分页查询,查询redis
     * @return
     */
    @RequestMapping(value = "/findPage",method = RequestMethod.GET)
    @ApiOperation("查询商品,每次1页每页5条显示")
    public List<Product> findPage(int page,int size){
        List<Product> list =null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            //使用redis的list里的lrange查询分页
            list = redisTemplate.opsForList().range(BYBT_KEY,start,end);
            if (CollectionUtils.isEmpty(list)){
                //查询为空就去mysql中查询
                return null;
            }
            log.info("参加活动商品:{}",list);
        } catch (Exception e) {
            //出现异常,一般redis出现事故
            log.error("bybt异常:{}",e);
        }
        return list;
    }
    @RequestMapping(value = "/findPageAB",method = RequestMethod.GET)
    @ApiOperation("双缓存查询商品,每次1页每页5条显示")
    public List<Product> findPageAB(int page,int size){
        List<Product> list =null;
        long start = (page - 1) * size;
        long end = start + size - 1;
        try {
            //先去缓存A中查找
            list = redisTemplate.opsForList().range(BYBT_KEY_A,start,end);
            if (CollectionUtils.isEmpty(list)){
                log.info("缓存A已失效,暂时采用缓存B");
                //若缓存A没有就去缓存B中查找
                list = redisTemplate.opsForList().range(BYBT_KEY_B,start,end);
                if (CollectionUtils.isEmpty(list)){
                    //TODO 去MySQL查找
                    return null;
                }
            }
            log.info("参加活动商品:{}",list);
        } catch (Exception e) {
            //出现异常,一般redis出现事故
            log.error("bybt异常:{}",e);
            e.printStackTrace();
        }
        return list;
    }
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
缓存穿透缓存击穿缓存雪崩是常见的缓存问题,下面是关于Redis缓存穿透缓存击穿缓存雪崩的介绍: 1. 缓存穿透缓存穿透是指当一个请求查询一个不存在于缓存中的数据时,由于缓存无法命中,请求会直接访问数据库。这种情况下,如果有大量的请求查询不存在的数据,会导致数据库压力过大,影响系统性能。 2. 缓存击穿缓存击穿是指当一个热点数据的缓存过期或失效时,大量的请求同时访问该数据,导致缓存无法命中,请求会直接访问数据库。这种情况下,数据库会承受巨大的压力,可能导致数据库崩溃。 3. 缓存雪崩缓存雪崩是指当缓存中的大量数据同时过期或失效时,大量的请求会直接访问数据库,导致数据库压力剧增,性能下降甚至系统崩溃。缓存雪崩通常是由于缓存服务器故障、缓存设置不合理或者缓存数据过期时间设置不当等原因引起的。 为了避免缓存穿透缓存击穿缓存雪崩问题,可以采取以下措施: - 缓存穿透:可以在应用层对查询的数据进行校验,如果数据不存在,则不进行缓存操作,避免大量无效的请求访问数据库。 - 缓存击穿:可以互斥锁或分布式锁来保护热点数据的问,当缓存失效时,只允许一个请求访问数据库并更新缓存,其他请求等待缓存更新完成后再从缓存中获取数据。 - 缓存雪崩:可以采用多级缓存缓存预热、设置合理的缓存过期时间等策略来避免大量缓存同时失效,保证系统的稳定性和性能。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鸭鸭老板

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值