Redis缓存雪崩、击穿、穿透技术解析及解决方案

在使用 Redis 缓存时,经常会遇到一些异常问题。

概括来说有 4 个方面:

  • 缓存中的数据和数据库中的不一致;
  • 缓存雪崩;
  • 缓存击穿;
  • 缓存穿透。

关于第一个问题【缓存中的数据和数据库中的不一致】,在之前的文章中,已经深入分析过了,可以参考: 深入解析缓存模式下的数据一致性问题

今天重点看下后面三个问题。

缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

产生原因

产生缓存雪崩的原因有两种:

  • 缓存系统本身不可用,导致大量请求直接回源到数据库;
  • 应用设计层面大量的 Key 在同一时间过期,导致大量的数据回源。

针对第一个原因,可以通过主从节点的方式构建 Redis缓存高可用集群,如果主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

重点看下第二种原因,在缓存数据同时过期的情况下,对系统性能的影响。

假如我们有一个查询城市数据的需求。

  • 在系统初始化时,将 1000 条城市数据放入 Redis 缓存中,过期时间为 30s;
  • Redis 中数据过期后,从数据库中获取数据,然后写入缓存,每查询一次数据库,计数器加 1;
  • 程序启动的同时,启动一个定时任务线程每隔一秒输出计数器的值,并把计数器归零。
package com.redis.demo.controller;

@Slf4j
@RestController
public class RedisController {
   
    @Autowired
    private RedisUtil redisUtil;

    private AtomicInteger atomicInteger = new AtomicInteger(); // 全局 QPS 计数器

    @PostConstruct
    public void init() {
   
        //初始化1000个城市数据到Redis,所有缓存数据有效期30秒
        IntStream.rangeClosed(1, 1000).forEach(i -> redisUtil.set("city" + i, getCityFromDb(i), 30));

        //每秒一次,输出数据库访问的QPS,同时把计数器置0
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
   
            log.info("DB QPS : {}", atomicInteger.getAndSet(0));
        }, 0, 1, TimeUnit.SECONDS);
    }

    @GetMapping("/city")
    public String city() {
   
        //随机查询一个城市
        int id = ThreadLocalRandom.current().nextInt(1000) + 1;
        String key = "city" + id;
        String data = redisUtil.get(key);
        if (data == null) {
   
            //回源到数据库查询
            data = getCityFromDb(id);
            if (!StringUtils.isEmpty(data))
                //缓存30秒过期
                redisUtil.set(key, data, 30);
        }
        return data;
    }

    /**
     * 从数据库中查询数据
     * @param cityId 城市ID
     * @return
     */
    private String getCityFromDb(int cityId) {
   

        try {
   
            TimeUnit.MICROSECONDS.sleep(100); //模拟数据库查询操作
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }

        //模拟查询数据库,查一次计数器加一
        atomicInteger.incrementAndGet();
        return "citydata" + System.currentTimeMillis();
    }
}

使用 wrk 工具,设置 4 线程 4 连接压测 city 接口:

wrk -c4 -t4 -d100s  http://localhost:8080/city

缓存数据 30s 后过期,回源的数据库 QPS 最高达到了635.

2024-11-14 14:48:21.525  INFO 11546 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 307
2024-11-14 14:48:22.525  INFO 11546 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 635
2024-11-14 14:48:23.525  INFO 11546 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 74

解决方案

由于缓存 Key 同时大规模过期导致数据库压力激增的解决方式有两种:

随机化过期时间

随机化过期时间,不要让大量的 Key 在同一时间过期。

在系统初始化时,将过期时间设置为 30s + 10s 以内的随机值,这样的话,Key就会被分散到 30~40s 之间过期了。

@PostConstruct
    public void init() {
   
        //初始化1000个城市数据到Redis,所有缓存数据有效期30~40秒
        IntStream.rangeClosed(1, 1000).forEach(i -> redisUtil.set("city" + i, getCityFromDb(i), 30 + ThreadLocalRandom.current().nextInt(10)));
    }

缓存过期后,回源数据库不会集中在同一秒,数据库的 QPS 从 600 多降到了 100 左右。

2024-11-14 15:01:31.556  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 93
2024-11-14 15:01:32.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 95
2024-11-14 15:01:33.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 107
2024-11-14 15:01:34.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 91
2024-11-14 15:01:35.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 104
2024-11-14 15:01:36.557  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 120
2024-11-14 15:01:37.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 89
2024-11-14 15:01:38.558  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 114
2024-11-14 15:01:39.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 99
2024-11-14 15:01:40.558  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 54
2024-11-14 15:01:41.557  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 13

缓存永不过期

让缓存永不过期,启动一个后台线程 60 秒一次定时把所有数据更新到缓存,而且通过适当的休眠,控制从数据库更新数据的频率,降低数据库压力。

@PostConstruct
    public void rightInit2() throws InterruptedException {
   

        CountDownLatch countDownLatch = new CountDownLatch(1);

        //每隔60秒全量更新一次缓存
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
   

            log.info("定时去更新缓存");

            IntStream.rangeClosed(1, 1000).forEach(i -> {
   
                String data = getCityFromDb(i);

                // 通过适当的休眠,来控制访问数据库的频率
                try {
   
                    TimeUnit.MILLISECONDS.sleep(20);
                } catch (InterruptedException e) {
   }

                if (!StringUtils.isEmpty(data)) {
   
                    //缓存永不过期,被动更新
                    redisUtil.set("city" + i, data);
                }
            });

            log.info("定时更新缓存完成");

            //启动程序的时候需要等待首次更新缓存完成
            countDownLatch.countDown();
        }, 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值