快取問題(四) 快取穿透、快取雪崩、快取併發 解決案例

Crzayliyang-架構Young發表於2020-11-10

視訊地址: https://www.bilibili.com/video/BV1Ha411c7hB

程式碼地址: https://gitee.com/crazyliyang/video-teaching.git

 

1. 快取穿透 解決案例

 

使用布隆過濾器

aaaaa.png

 

核心程式碼, 使用 Redisson 庫的布隆過濾器:    org.redisson.api.RBloomFilter

 

<!-- Redis 客戶端工具 redisson 實現對 Redisson 的自動化配置-->
<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson-spring-boot-starter</artifactId>
     <version>3.13.4</version>
</dependency>

配置類, 例項化布隆過濾器  RBloomFilter

@Configuration
public class RedisConfiguration {
    /**
     * 預計要插入多少資料
     */
    private static int size = 100000; // 十萬

    /**
     * 期望的誤判率
     */
    private static double fpp = 0.001;

    @Value("${spring.redis.host}")
    private String redistHost;

    @Value("${spring.redis.port}")
    private Integer redistPort;


    @Autowired
    private ProductService productService;  // 商品服務


    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        // 建立 RedisTemplate 物件
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        // 設定開啟事務支援
        template.setEnableTransactionSupport(true);

        // 設定 RedisConnection 工廠。 它就是實現多種 Java Redis 客戶端接入的祕密工廠
        template.setConnectionFactory(factory);

        // 使用 String 序列化方式,序列化 KEY 。
        template.setKeySerializer(RedisSerializer.string());

        // 使用 JSON 序列化方式(庫是 Jackson ),序列化 VALUE 。
        template.setValueSerializer(RedisSerializer.json());

        return template;
    }

    /**
     * 例項化布隆過濾器
     *
     */
    @Bean
    public RBloomFilter redisBloomFilter(){

        String redisAddress = "redis://"+redistHost+ ":"+redistPort;

        Config config = new Config();
        config.useSingleServer().setAddress(redisAddress);
        RedissonClient redisson = Redisson.create(config);

        // 建立RedisBloomFilter
        RBloomFilter<String> redisBloomFilter = redisson.getBloomFilter("productBloomFilter03");

        //初始化布隆過濾器:預計元素為10000L ( 十萬), 誤差率為 0.001
        redisBloomFilter.tryInit(size, fpp);

        return redisBloomFilter;
    }

}

使用隆過濾器  RBloomFilter

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {

    //redisKey字首
    private static final String KEY_PREFIX = "product:";

    //  格式化一下
    private static String buildKey(String id) {
        return KEY_PREFIX + id;  // product:商品ID
    }

    @Resource(name = "redisTemplate")
    private ValueOperations<Serializable, String> redisOperations;

    @Autowired
    private RBloomFilter<String> redisBloomFilter;

    /**
     * 過載 getById(id)  做第一點增強
     *
     */
    @Override
    public ProductEntity getById(Serializable id) {
        String redisKey = buildKey(String.valueOf(id)); // 構建rediskey
        
        if(!redisBloomFilter.contains(redisKey)){  // 布隆過濾器判斷 redisKey 不存在
            return null;
        }
        String redisValue = redisOperations.get(redisKey);  // 從快取中去查

        if (redisValue != null) {  // 查到了
            return JSONUtil.parseObject(redisValue, ProductEntity.class); //直接返回;

        } else { // 沒查到
            ProductEntity product = getBaseMapper().selectById(id); // 從資料庫中查, 然後放到快取中;
            if(product !=null){
                String jsonString = JSONUtil.toJSONString(product);
                redisOperations.set(redisKey, jsonString);    // 放到快取中
                return product;
            }
            return null;
        }
    }
}

 

 

2. 快取雪崩 解決案例

 

 

bbbbb.png

 

核心程式碼: 使用一個過期時間的隨機數,  讓 key 的過期時間分散化,    防止 key  大批集中過期;

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {
    private static final String KEY_PREFIX = "product:";
    private static String buildKey(String id) {
        return KEY_PREFIX + id;  // product:商品ID
    }
    @Resource(name = "redisTemplate")
    private ValueOperations<Serializable, String> redisOperations;
   
    @Autowired
    private RBloomFilter<String> redisBloomFilter;

    /**
     * 過載 getById(id)  做第一點增強
     */
    @Override
    public ProductEntity getById(Serializable id) {
        String redisKey = buildKey(String.valueOf(id)); // 構建rediskey
        if(!redisBloomFilter.contains(redisKey)){  // 布隆過濾器判斷 redisKey 不存在
            return null;
        }
        String redisValue = redisOperations.get(redisKey);  // 從快取中去查
        if (redisValue != null) {  // 查到了
            return JSONUtil.parseObject(redisValue, ProductEntity.class); //直接返回;

        } else { // 沒查到
            ProductEntity product = getBaseMapper().selectById(id); // 從資料庫中查, 然後放到快取中;
            if(product !=null){
                String jsonString = JSONUtil.toJSONString(product);
                long radomLong = radomLong();  // 隨機產生一個數 ( 24 ~ 72 )
                redisOperations.set(redisKey, jsonString, radomLong, TimeUnit.HOURS);    // 放到快取中, 時間取隨機, 時間單位小時
                return product;
            }
            return null;
        }
    }
    public static long radomLong(){
        long min = 24;
        long max = 72;
        long rangeLong = min + (((long) (new Random().nextDouble() * (max - min))));
        return rangeLong;
    }

}

 

 

3. 快取併發 解決案例

 

cccc.png

 

使用Redis 分散式鎖, 核心程式碼:

 

封裝 RedisLock

@Component
public class RedisLock {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 加鎖
     * @param lockKey
     * @return
     */
    public  RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    /**
     * 釋放鎖
     * @param lockKey
     */
    public  void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    /**
     * 釋放鎖
     * @param lock
     */
    public  void unlock(RLock lock) {
        lock.unlock();
    }

    /**
     * 帶超時的鎖
     * @param lockKey
     * @param timeout 超時時間   單位:秒
     */
    public  RLock lock(String lockKey, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, TimeUnit.SECONDS);
        return lock;
    }

    /**
     * 帶超時的鎖
     * @param lockKey
     * @param unit 時間單位
     * @param timeout 超時時間
     */
    public  RLock lock(String lockKey, TimeUnit unit , int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    /**
     * 嘗試獲取鎖
     * @param lockKey
     * @param waitTime 最多等待時間
     * @param leaseTime 上鎖後自動釋放鎖時間
     * @return
     */
    public  boolean tryLock(String lockKey, int waitTime, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 嘗試獲取鎖
     * @param lockKey
     * @param unit 時間單位
     * @param waitTime 最多等待時間
     * @param leaseTime 上鎖後自動釋放鎖時間
     * @return
     */
    public  boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

}

配置類 例項化 註冊 RedissonClient

@Configuration
public class RedisConfiguration {
    
    @Value("${spring.redis.host}")
    private String redistHost;

    @Value("${spring.redis.port}")
    private Integer redistPort;


    @Autowired
    private ProductService productService;  // 商品服務

    /**
     * RedissonClient 註冊
     */
    @Bean
    public RedissonClient redissonClient(){
        String redisAddress = "redis://"+redistHost+ ":"+redistPort;

        Config config = new Config();
        config.useSingleServer().setAddress(redisAddress);
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
    
}

 

使用Redis 分散式鎖:

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {

    //字首
    private static final String KEY_PREFIX = "product:";

    private static String buildKey(String id) {
        return KEY_PREFIX + id;  // product:商品ID
    }

    @Resource(name = "redisTemplate")
    private ValueOperations<Serializable, String> redisOperations;

    @Autowired
    private RBloomFilter<String> redisBloomFilter;

    @Autowired
    private RedisLock redisLock;   // 自己封裝的 redisLock

    /**
     * 過載 getById(id)  做第一點增強
     */
    @Override
    public ProductEntity getById(Serializable id) {
        String redisKey = buildKey(String.valueOf(id)); // 構建rediskey

        if (!redisBloomFilter.contains(redisKey)) {  // 布隆過濾器判斷 redisKey 不存在
            return null;
        }
        String redisValue = redisOperations.get(redisKey);  // 從快取中去查

        if (redisValue != null) {  // 查到了
            return JSONUtil.parseObject(redisValue, ProductEntity.class); //直接返回;
        }

        // 在查詢 DB 之前加 分散式鎖
        redisLock.lock(String.valueOf(id));
        try{
            // 再一次查詢快取, 是為了分散式併發情況下, 其他併發的請求, 能在鎖解開之後, 再次查快取
            String redisValue2 = redisOperations.get(redisKey);
            if (redisValue2 != null) {
                return JSONUtil.parseObject(redisValue, ProductEntity.class);
            }
            ProductEntity product = getBaseMapper().selectById(id); // 從資料庫中查
            if (product != null) {
                String jsonString = JSONUtil.toJSONString(product);
                redisOperations.set(redisKey, jsonString);    // 放到快取中
                return product;
            }
            return null;
        }finally {
            redisLock.unlock(String.valueOf(id));   // 釋放鎖
        }
    }
}

 

 

 

相關文章