“使用 Redis 查询的一般程序实现和击穿、雪崩、穿透”的版本间的差异

来自姬鸿昌的知识库
跳到导航 跳到搜索
第56行: 第56行:
  
 
==== 解决方案 ====
 
==== 解决方案 ====
预加载缓存和设置缓存永不过期
 
  
 +
===== 预加载缓存和设置缓存永不过期 =====
 
预加载缓存就是在数据被大量请求密集访问之前就提前设置到缓存中(比如节庆活动要访问的数据就在节庆活动开始前提前放进缓存),这样可以预防一开始 key 不存在时大量请求密集访问导致的缓存击穿;
 
预加载缓存就是在数据被大量请求密集访问之前就提前设置到缓存中(比如节庆活动要访问的数据就在节庆活动开始前提前放进缓存),这样可以预防一开始 key 不存在时大量请求密集访问导致的缓存击穿;
  
 
设置缓存永不过期就可以预防在 key 失效的一瞬间 大量请求密集访问导致的缓存击穿,但这样就要考虑要存储的数据量的大小和 redis 服务实例可以存储的大小,做好冷、热数据的区分处理,冷数据还是做定时失效,不然冷数据就会浪费缓存空间;
 
设置缓存永不过期就可以预防在 key 失效的一瞬间 大量请求密集访问导致的缓存击穿,但这样就要考虑要存储的数据量的大小和 redis 服务实例可以存储的大小,做好冷、热数据的区分处理,冷数据还是做定时失效,不然冷数据就会浪费缓存空间;
  
 +
===== key 访问计数动态调整失效时间 =====
 
实际可能会有这种情况:就是我们没办法提前知道哪一个 key 会是热点数据,针对这种情况,我们可以为这些数据增加一个算法实现——计数单个 key 在单位时间内的访问频率,当高到一个阈值之后,我们把这个 key 设置为永不过期。
 
实际可能会有这种情况:就是我们没办法提前知道哪一个 key 会是热点数据,针对这种情况,我们可以为这些数据增加一个算法实现——计数单个 key 在单位时间内的访问频率,当高到一个阈值之后,我们把这个 key 设置为永不过期。
 +
 +
===== 加锁排队 =====
 +
<syntaxhighlight lang="java">
 +
    public List<ProductCategory> findProductCategory() {
 +
        String key = "product:product-category";
 +
        Object obj = redisTemplate.opsForValue().get(key);
 +
 +
        if (obj == null) {
 +
            synchronized (this) {
 +
                //进入 synchronized 一定要先再查询一次 Redis,防止上一个抢到锁的线程已经更新了
 +
                obj = redisTemplate.opsForValue().get(key);
 +
                if (obj != null) {
 +
                    return (List<ProductCategory>)obj;
 +
                }
 +
                List<ProductCategory> productCategories = productCategoryMapper.selectProductCategory();
 +
                redisTemplate.opsForValue().set(key, productCategories, Duration.ofHours(2));
 +
                return productCategories;
 +
            }
 +
        } else {
 +
            return (List<ProductCategory>) obj;
 +
        }
 +
 +
    }
 +
</syntaxhighlight>

2023年2月13日 (一) 08:35的版本

https://www.bilibili.com/video/BV1fb4y147qw/

package io.github.jihch.service;

import io.github.jihch.bean.ExpressInfo;
import io.github.jihch.exception.ClientException;
import io.github.jihch.mapper.ExpressMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import java.awt.datatransfer.Clipboard;
import java.time.Duration;

public class ExpressInfoService implements IExpressInfoService {

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    ExpressMapper expressMapper;

    /**
     * 通过发货单查询物流信息
     * @param id
     * @return
     */
    @Override
    public ExpressInfo findByDeliveryOrderId(Long id) {
        String key = "xushu-express:express-info:";

        //从 Redis 查询物流信息
        Object obj = redisTemplate.opsForValue().get(key + id);

        if (obj != null) {
            return (ExpressInfo) obj;

        } else {
            ExpressInfo expressInfo = expressMapper.selectByDeliveryOrderId(id);
            if (expressInfo != null) {
                redisTemplate.opsForValue().set(key+id, expressInfo, Duration.ofHours(2));
                return expressInfo;
            } else {
                throw new ClientException("发货单:{} 的物流信息不存在", id);
            }
        }//end else

    }
}
查询增加缓存实现的一般流程.png

但这种一般实现,不足以应对高并发的场景,可能会出现缓存击穿、缓存穿透、雪崩的问题

缓存击穿

假设这样的场景:高并发情况下,像上面的这种一般实现,一个 key 一开始是不在缓存里的,或者它设置了失效时间在某一个时间点失效,

但它又是一个访问频率非常高的 key,那么当大量请求密集访问这个接口的时候,就会出现因为缓存中没有这个 key,然后都去查询数据库,就会给数据库带来非常大的压力导致性能下降甚至崩溃。

解决方案

预加载缓存和设置缓存永不过期

预加载缓存就是在数据被大量请求密集访问之前就提前设置到缓存中(比如节庆活动要访问的数据就在节庆活动开始前提前放进缓存),这样可以预防一开始 key 不存在时大量请求密集访问导致的缓存击穿;

设置缓存永不过期就可以预防在 key 失效的一瞬间 大量请求密集访问导致的缓存击穿,但这样就要考虑要存储的数据量的大小和 redis 服务实例可以存储的大小,做好冷、热数据的区分处理,冷数据还是做定时失效,不然冷数据就会浪费缓存空间;

key 访问计数动态调整失效时间

实际可能会有这种情况:就是我们没办法提前知道哪一个 key 会是热点数据,针对这种情况,我们可以为这些数据增加一个算法实现——计数单个 key 在单位时间内的访问频率,当高到一个阈值之后,我们把这个 key 设置为永不过期。

加锁排队
    public List<ProductCategory> findProductCategory() {
        String key = "product:product-category";
        Object obj = redisTemplate.opsForValue().get(key);

        if (obj == null) {
            synchronized (this) {
                //进入 synchronized 一定要先再查询一次 Redis,防止上一个抢到锁的线程已经更新了
                obj = redisTemplate.opsForValue().get(key);
                if (obj != null) {
                    return (List<ProductCategory>)obj;
                }
                List<ProductCategory> productCategories = productCategoryMapper.selectProductCategory();
                redisTemplate.opsForValue().set(key, productCategories, Duration.ofHours(2));
                return productCategories;
            }
        } else {
            return (List<ProductCategory>) obj;
        }

    }