day03-商家查詢快取02

2023-04-21 06:01:03

功能02-商鋪查詢快取02

知識補充

(1)快取穿透

https://blog.csdn.net/qq_45637260/article/details/125866738

快取穿透(cache penetration)是指使用者存取的資料既不在快取當中,也不在資料庫中。出於容錯的考慮,如果從底層資料庫查詢不到資料,則不寫入快取。這就導致每次請求都會到底層資料庫進行查詢,快取也失去了意義。當高並行或有人利用不存在的Key頻繁攻擊時,資料庫的壓力驟增,甚至崩潰,這就是快取穿透問題。

簡單地說,快取穿透是指使用者請求的資料在快取和資料庫中都不存在,則每次請求都會打到資料庫中,給資料庫帶來巨大壓力。

之後再存取該資料,只要redis的空值對沒有過期,就不會存取到資料庫,從而起到保護資料庫的作用。

3.5查詢商鋪id的快取擊穿問題

當查詢店鋪id時,可能會出現該店鋪id對應的快取失效,從而大量請求傳送到資料庫的情況,這裡使用兩種方案分別解決該問題。

3.5.1基於互斥鎖方案解決

3.5.1.1需求分析

修改根據id查詢商鋪的業務,基於互斥鎖方式來解決快取擊穿問題。

如下,當出現快取擊穿問題,首先需要判斷當前的執行緒是否能夠獲取鎖:

  1. 若可以,則進行快取重建(將資料庫資料重新寫入快取中),然後釋放鎖。
  2. 如果不能,則執行緒等待一段時間,然後再判斷快取是否能命中。
    • 如果未命中,則重複獲取鎖的流程,直到快取命中,或者獲得鎖,重建快取。
image-20230420184120133

根據redis的setnx命令,當setnx設定某個key之後,如果該key存在,則其他執行緒無法設定該key。

我們可以根據這個特性,作為一個lock的邏輯標誌,當一個執行緒setnx某個key後,代表獲取了「鎖」。當刪除這個key時,代表釋放「鎖」,這樣其他執行緒就可以重新獲取「鎖」。此外,可以對該key設定一個有效期,防止刪除key失敗,產生「死鎖」。

3.5.1.2程式碼實現

(1)修改 ShopServiceImpl.java

package com.hmdp.service.impl;

import ...

/**
 * 服務實現類
 *
 * @author 李
 * @version 1.0
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
        implements IShopService {
    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店鋪不存在!");
        }
        return Result.ok(shop);
    }

    //快取穿透(儲存空物件)+快取擊穿解決(互斥鎖解決)
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //從redis中查詢商鋪快取
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //判斷快取是否命中
        if (StrUtil.isNotBlank(shopJson)) {
            //命中,直接返回商鋪資訊
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判斷命中的是否是redis的空值(快取擊穿解決)
        if (shopJson != null) {
            return null;
        }
        //未命中,嘗試獲取互斥鎖
        String lockKey = "lock:shop:" + id;
        boolean isLock = false;
        Shop shop = null;
        try {
            //獲取互斥鎖
            isLock = tryLock(lockKey);
            //判斷是否獲取成功
            if (!isLock) {//失敗
                //等待並重試
                Thread.sleep(50);
                //直到快取命中,或者獲取到鎖
                return queryWithMutex(id);
            }
            //獲取鎖成功,開始重建快取
            //根據id查詢資料庫,判斷商鋪是否存在資料庫中
            shop = getById(id);
            //模擬重建快取的延遲-----------
            Thread.sleep(200);
            if (shop == null) {
                //不存在,防止快取穿透,將空值存入redis,TTL設定為2min
                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;
    }

    //快取穿透方案
//    public Shop queryWithPassThrough(Long id) {
//        String key = CACHE_SHOP_KEY + id;
//        //1.從redis中查詢商鋪快取
//        String shopJson = stringRedisTemplate.opsForValue().get(key);
//        //2.判斷快取是否命中
//        if (StrUtil.isNotBlank(shopJson)) {
//            //2.1若命中,直接返回商鋪資訊
//            return JSONUtil.toBean(shopJson, Shop.class);
//        }
//        //判斷命中的是否是redis的空值
//        if (shopJson != null) {
//            return null;
//        }
//        //2.2未命中,根據id查詢資料庫,判斷商鋪是否存在資料庫中
//        Shop shop = getById(id);
//        if (shop == null) {
//            //2.2.1不存在,防止快取穿透,將空值存入redis,TTL設定為2min
//            stringRedisTemplate.opsForValue().set(key, "",
//                    CACHE_NULL_TTL, TimeUnit.MINUTES);
//            //返回錯誤資訊
//            return null;
//        }
//        //2.2.2存在,則將商鋪資料寫入redis中
//        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
//                CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        return shop;
//    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店鋪id不能為空");
        }
        //1.更新資料庫
        updateById(shop);
        //2.刪除redis快取
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }
}

(2)使用jemeter模擬高並行的情況:

5秒發起1000個請求執行緒:

image-20230420205354887

模擬http請求:

image-20230420205456072 image-20230420210317214

全部請求成功,獲取到資料:

image-20230420211650622

在伺服器的控制檯中可以看到:對於資料庫的請求只觸發了一次,證明在高並行的場景下,只有一個執行緒對資料庫發起請求,並對redis對應的快取重新設定。

image-20230420210258377

3.5.2基於邏輯過期方案解決