黑馬點評day02(緩存)

2、商戶查詢緩存

2.1 什么是緩存?

前言:什么是緩存?

就像自行車,越野車的避震器

img

舉個例子:越野車,山地自行車,都擁有"避震器",防止車體加速后因慣性,在酷似"U"字母的地形上飛躍,硬著陸導致的損害,像個彈簧一樣;

同樣,實際開發中,系統也需要"避震器",*防止過高的數據訪問猛沖系統,導致其操作線程無法及時處理信息而癱瘓;*

這在實際開發中對企業講,對產品口碑,用戶評價都是致命的;所以企業非常重視緩存技術;

緩存(Cache),就是數據交換的緩沖區,俗稱的緩存就是緩沖區內的數據,一般從數據庫中獲取,存儲于本地代碼(例如:

例1:static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并發
?
例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等緩存
?
例3:static final Map<K,V> map = ?new HashMap(); 本地緩存

由于其被Static修飾,所以隨著類的加載而被加載到內存之中,作為本地緩存,由于其又被final修飾,所以其引用(例3:map)和對象(例3:new HashMap())之間的關系是固定的,不能改變,因此不用擔心賦值(=)導致緩存失效;

2.1.1 為什么要使用緩存 一句話:因為速度快,好用

緩存數據存儲于代碼中,而代碼運行在內存中,內存的讀寫性能遠高于磁盤,緩存可以大大降低用戶訪問并發量帶來的服務器讀寫壓力

實際開發過程中,企業的數據量,少則幾十萬,多則幾千萬,這么大數據量,如果沒有緩存來作為"避震器",系統是幾乎撐不住的,所以企業會大量運用到緩存技術;但是緩存也會增加代碼復雜度和運營的成本:

img

2.1.2 如何使用緩存

實際開發中,會構筑多級緩存來使系統運行速度進一步提升,例如:本地緩存與redis中的緩存并發使用

瀏覽器緩存:主要是存在于瀏覽器端的緩存

應用層緩存:可以分為tomcat本地緩存,比如之前提到的map,或者是使用redis作為緩存

數據庫緩存:在數據庫中有一片空間是 buffer pool,增改查數據都會先加載到mysql的緩存中CPU緩存:當代計算機最大的問題是 cpu性能提升了,但內存讀寫速度沒有跟上,所以為了適應當下的情況,增加了cpu的L1,L2,L3級的緩存

img

2.2 添加商戶緩存

在我們查詢商戶信息時,我們是直接操作從數據庫中去進行查詢的,大致邏輯是這樣,直接查詢數據庫那肯定慢咯,所以我們需要增加緩存。

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {//這里是直接查詢數據庫return shopService.queryById(id);
}
2.2.1 、緩存模型和思路

*標準的操作方式就是查詢數據庫之前先查詢緩存,如果緩存數據存在,則直接從緩存中返回,如果緩存數據不存在,再查詢數據庫,然后將數據存入redis。*

img

2.1.2、代碼如下

代碼思路:如果緩存有,則直接返回,如果緩存不存在,則查詢數據庫,然后存入redis。

? 代碼——查詢緩存與數據庫

img

2.3 緩存更新策略 緩存更新是redis為了節約內存而設計出來的一個東西,主要是因為內存數據寶貴,當我們向redis插入太多數據,此時就可能會導致緩存中的數據過多,所以redis會對部分數據進行更新,或者把他叫為淘汰更合適。

內存淘汰:redis自動進行,當redis內存達到咱們設定的max-memery的時候,會自動觸發淘汰機制,淘汰掉一些不重要的數據(可以自己設置策略方式)

超時剔除:當我們給redis設置了過期時間ttl之后,redis會將超時的數據進行刪除,方便咱們繼續使用緩存

主動更新:我們可以手動調用方法把緩存刪掉,通常用于解決緩存和數據庫不一致問題

img

2.3.1 、數據庫緩存不一致解決方案: 由于我們的緩存的數據源來自于數據庫,而數據庫的數據是會發生變化的,因此,如果當數據庫中數據發生變化,而緩存卻沒有同步,此時就會有一致性問題存在,其后果是:

用戶使用緩存中的過時數據,就會產生類似多線程數據安全問題,從而影響業務,產品口碑等;怎么解決呢?有如下幾種方案:

Cache Aside Pattern 人工編碼方式:緩存調用者在更新完數據庫后再去更新緩存,也稱之為雙寫方案

Read/Write Through Pattern : 由系統本身完成,數據庫與緩存的問題交由系統本身去處理

Write Behind Caching Pattern :調用者只操作緩存,其他線程去異步處理數據庫,實現最終一致

img

#####

2.3.2 、數據庫和緩存不一致采用什么方案

綜合考慮使用方案一,但是方案一調用者如何處理呢?這里有幾個問題

操作緩存和數據庫時有三個問題需要考慮:

如果采用第一個方案,那么假設我們每次操作數據庫后,都操作緩存,但是中間如果沒有人查詢,那么這個更新動作實際上只有最后一次生效,中間的更新動作意義并不大,我們可以把緩存刪除,等待再次查詢時,將緩存中的數據加載出來

  • 刪除緩存還是更新緩存?
    • 更新緩存:每次更新數據庫都更新緩存,無效寫操作較多

    • *刪除緩存:更新數據庫時讓緩存失效,查詢時再更新緩存*

  • 如何保證緩存與數據庫的操作的同時成功或失敗?

    • 單體系統,將緩存與數據庫操作放在一個事務

    • 分布式系統,利用TCC等分布式事務方案

應該具體操作緩存還是操作數據庫,我們應當是*先操作數據庫,再刪除緩存*,原因在于,如果你選擇第一種方案,在兩個線程并發來訪問時,假設線程1先來,他先把緩存刪了,此時線程2過來,他查詢緩存數據并不存在,此時他寫入緩存,當他寫入緩存后,線程1再執行更新動作時,實際上寫入的就是舊的數據,新的數據被舊數據覆蓋了。

  • 先操作緩存還是先操作數據庫?
    • 先刪除緩存,再操作數據庫

    • *先操作數據庫,再刪除緩存*

img

#####

2.4 實現商鋪和緩存與數據庫雙寫一致

核心思路如下:

修改ShopController中的業務邏輯,滿足下面的需求:

根據id查詢店鋪時,如果緩存未命中,則查詢數據庫,將數據庫結果寫入緩存,并設置超時時間

根據id修改店鋪時,先修改數據庫,再刪除緩存

修改重點代碼1——查詢添加TTL

修改ShopServiceImpl的queryById方法

設置redis緩存時添加過期時間

img

修改重點代碼2——修改雙寫一致

代碼分析:通過之前的淘汰,我們確定了采用刪除策略,來解決雙寫問題,當我們修改了數據之后,然后把緩存中的數據進行刪除,查詢時發現緩存中沒有數據,則會從mysql中加載最新的數據,從而避免數據庫和緩存不一致的問題

img

#####

2.5緩存穿透問題的解決思路

緩存穿透 :緩存穿透是指客戶端請求的數據在緩存中和數據庫中都不存在,這樣緩存永遠不會生效,這些請求都會打到數據庫。(CT)

  • 常見的解決方案有兩種:

  • 緩存空對象
    • 缺點:

      • 額外的內存消耗

      • 可能造成短期的不一致

    • 優點:實現簡單,維護方便

  • *布隆過濾*
    • 缺點:

      • 實現復雜

      • 存在誤判可能(存在的不一定存在,不存在的一定不存在)

    • 優點:內存占用較少,沒有多余key

  • 緩存空對象思路分析:當我們客戶端訪問不存在的數據時,先請求redis,但是此時redis中沒有數據,此時會訪問到數據庫,但是數據庫中也沒有數據,這個數據穿透了緩存,直擊數據庫,我們都知道數據庫能夠承載的并發不如redis這么高,如果大量的請求同時過來訪問這種不存在的數據,這些請求就都會訪問到數據庫,簡單的解決方案就是哪怕這個數據在數據庫中也不存在,我們也把這個數據存入到redis中去,這樣,下次用戶過來訪問這個不存在的數據,那么在redis中也能找到這個數據就不會進入到緩存了。

  • 布隆過濾:布隆過濾器其實采用的是哈希思想來解決這個問題,通過一個龐大的二進制數組,走哈希思想去判斷當前這個要查詢的這個數據是否存在,如果布隆過濾器判斷存在,則放行,這個請求會去訪問redis,哪怕此時redis中的數據過期了,但是數據庫中一定存在這個數據,在數據庫中查詢出來這個數據后,再將其放入到redis中,

  • 假設布隆過濾器判斷這個數據不存在,則直接返回

  • 這種方式優點在于節約內存空間,存在誤判,誤判原因在于

  • 布隆過濾器走的是哈希思想,只要哈希思想,就可能存在哈希沖突。

  • img

2.6 編碼解決商品查詢的緩存穿透問題: 核心思路如下:

在原來的邏輯中,我們如果發現這個數據在mysql中不存在,直接就返回404了,這樣是會存在緩存穿透問題的。

現在的邏輯中:如果這個數據不存在,我們不會返回404 ,還是會把這個數據寫入到Redis中,并且將value設置為空,當再次發起查詢時,我們如果發現命中之后,判斷這個value是否是null,如果是null,則是之前寫入的數據,證明是緩存穿透數據,如果不是,則直接返回數據。

img

小總結:

緩存穿透產生的原因是什么?

  • 用戶請求的數據在緩存中和數據庫中都不存在,不斷發起這樣的請求,給數據庫帶來巨大壓力

  • 緩存穿透的解決方案有哪些?
    • 緩存null值

    • 布隆過濾

    • 增強id的復雜度,避免被猜測id規律

    • 做好數據的基礎格式校驗

    • 加強用戶權限校驗

    • 做好熱點參數的限流(可用先限流來保底)

2.7 緩存雪崩問題及解決思路

緩存雪崩是指在同一時段大量的緩存key同時失效或者Redis服務宕機,導致大量請求到達數據庫,帶來巨大壓力。

解決方案:

  • 給不同的Key的TTL添加隨機值

  • 利用Redis集群提高服務的可用性

  • 給緩存業務添加降級限流策略

  • 給業務添加多級緩存

img

#####

2.8 緩存擊穿問題及解決思路

緩存擊穿問題也叫熱點Key問題,就是一個被高并發訪問并且緩存重建業務較復雜的key突然失效了,無數的請求訪問會在瞬間給數據庫帶來巨大的沖擊。

常見的解決方案有兩種:

  • 互斥鎖

  • 邏輯過期

邏輯分析:

假設線程1在查詢緩存之后,本來應該去查詢數據庫,然后把這個數據重新加載到緩存的,此時只要線程1走完這個邏輯,其他線程就都能從緩存中加載這些數據了,但是假設在線程1沒有走完的時候,后續的線程2,線程3,線程4同時過來訪問當前這個方法, 那么這些線程都不能從緩存中查詢到數據,那么他們就會同一時刻來訪問查詢緩存,都沒查到,接著同一時間去訪問數據庫,同時的去執行數據庫代碼,對數據庫訪問壓力過大

img

解決方案一、使用鎖來解決:

因為鎖能實現互斥性。假設線程過來,只能一個人一個人的來訪問數據庫,從而避免對于數據庫訪問壓力過大,但這也會影響查詢的性能,因為此時會讓查詢的性能從并行變成了串行,我們可以采用tryLock方法 + double check來解決這樣的問題。

假設現在線程1過來訪問,他查詢緩存沒有命中,但是此時他獲得到了鎖的資源,那么線程1就會一個人去執行邏輯,假設現在線程2過來,線程2在執行過程中,并沒有獲得到鎖,那么線程2就可以進行到休眠,直到線程1把鎖釋放后,線程2獲得到鎖,然后再來執行邏輯,此時就能夠從緩存中拿到數據了。

img

#####

解決方案二、邏輯過期方案

方案分析:我們之所以會出現這個緩存擊穿(熱點key)問題,主要原因是在于我們對key設置了過期時間,假設我們不設置過期時間,其實就不會有緩存擊穿的問題,但是不設置過期時間,這樣數據不就一直占用我們內存了嗎,我們可以采用邏輯過期方案。

我們把過期時間設置在 redis的value中,注意:這個過期時間并不會直接作用于redis,而是我們后續通過邏輯去處理。假設線程1去查詢緩存,然后從value中判斷出來當前的數據已經過期了,此時線程1去獲得互斥鎖,那么其他線程會進行阻塞,獲得了鎖的線程他會開啟一個 線程去進行 以前的重構數據的邏輯,直到新開的線程完成這個邏輯后,才釋放鎖, 而線程1直接進行返回,假設現在線程3過來訪問,由于線程線程2持有著鎖,所以線程3無法獲得鎖,線程3也直接返回數據,只有等到新開的線程2把重建數據構建完后,其他線程才能走返回正確的數據。這種方案優點在于,異步的構建緩存,缺點在于在構建完緩存之前,返回的都是臟數據。

img

兩者方案進行對比

互斥鎖方案:由于保證了互斥性,所以數據一致,且實現簡單,因為僅僅只需要加一把鎖而已,也沒其他的事情需要操心,所以沒有額外的內存消耗,缺點在于有鎖就有死鎖問題的發生,且只能串行執行性能肯定受到影響

邏輯過期方案: 線程讀取過程中不需要等待,性能好,有一個額外的線程持有鎖去進行重構數據,但是在重構數據完成前,其他的線程只能返回之前的數據,且實現起來麻煩

img

2.9 利用互斥鎖解決緩存擊穿問題 核心思路:相較于原來從緩存中查詢不到數據后直接查詢數據庫而言,現在的方案是 進行查詢之后,如果從緩存沒有查詢到數據,則進行互斥鎖的獲取,獲取互斥鎖后,判斷是否獲得到了鎖,如果沒有獲得到,則休眠,過一會再進行嘗試,直到獲取到鎖為止,才能進行查詢

如果獲取到了鎖的線程,再去進行查詢,查詢后將數據寫入redis,再釋放鎖,返回數據,利用互斥鎖就能保證只有一個線程去執行操作數據庫的邏輯,防止緩存擊穿。

img

? 代碼——操作鎖的代碼: 核心思路就是利用redis的setnx方法來表示獲取鎖,該方法含義是redis中如果沒有這個key,則插入成功,返回1,在stringRedisTemplate中返回true, 如果有這個key則插入失敗,則返回0,在stringRedisTemplate返回false,我們可以通過true,或者是false,來表示是否有線程成功插入key,成功插入的key的線程我們認為他就是獲得到鎖的線程。

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);
}
? 代碼——操作代碼:

 ?public Shop queryWithMutex(Long id)  {String key = CACHE_SHOP_KEY + id;// 1、從redis中查詢商鋪緩存String shopJson = stringRedisTemplate.opsForValue().get("key");// 2、判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判斷命中的值是否是空值if (shopJson != null) {//返回一個錯誤信息return null;}// 4.實現緩存重構//4.1 獲取互斥鎖String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 4.2 判斷否獲取成功if(!isLock){//4.3 失敗,則休眠重試Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根據id查詢數據庫shop = getById(id);// 5.不存在,返回錯誤if(shop == null){//將空值寫入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回錯誤信息return null;}//6.寫入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.釋放互斥鎖unlock(lockKey);}return shop;}
3.0 、利用邏輯過期解決緩存擊穿問題

需求:修改根據id查詢商鋪的業務,基于邏輯過期方式來解決緩存擊穿問題

思路分析:當用戶開始查詢redis時,判斷是否命中,如果沒有命中則直接返回空數據,不查詢數據庫,而一旦命中后,將value取出,判斷value中的過期時間是否滿足,如果沒有過期,則直接返回redis中的數據,如果過期,則在開啟獨立線程后直接返回之前的數據,獨立線程去重構數據,重構完成后釋放互斥鎖。

img

如果封裝數據:因為現在redis中存儲的數據的value需要帶上過期時間,此時要么你去修改原來的實體類,要么你

步驟一、

新建一個實體類,我們采用第二個方案,這個方案,對原來代碼沒有侵入性。

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

步驟二、

ShopServiceImpl 新增此方法,利用單元測試進行緩存預熱

img

在測試類中

img

步驟三:正式代碼

ShopServiceImpl

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化為對象RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未過期,直接返回店鋪信息return shop;}// 5.2.已過期,需要緩存重建// 6.緩存重建// 6.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判斷是否獲取鎖成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建緩存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回過期的商鋪信息return shop;
}
3.1、封裝Redis工具類

基于StringRedisTemplate封裝一個緩存工具類,滿足下列需求:

  • 方法1:將任意Java對象序列化為json并存儲在string類型的key中,并且可以設置TTL過期時間

  • 方法2:將任意Java對象序列化為json并存儲在string類型的key中,并且可以設置邏輯過期時間,用于處理緩存存擊穿問題

  • 方法3:根據指定的key查詢緩存,并反序列化為指定類型,利用緩存空值的方式解決緩存穿透問題

  • 方法4:根據指定的key查詢緩存,并反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題

  1. 將邏輯進行封裝

@Slf4j
@Component
public class CacheClient {
?private final StringRedisTemplate stringRedisTemplate;
?private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
?public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}
?public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}
?public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 設置邏輯過期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 寫入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}
?public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判斷命中的是否是空值if (json != null) {// 返回一個錯誤信息return null;}
?// 4.不存在,根據id查詢數據庫R r = dbFallback.apply(id);// 5.不存在,返回錯誤if (r == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return null;}// 6.存在,寫入redisthis.set(key, r, time, unit);return r;}
?public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化為對象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未過期,直接返回店鋪信息return r;}// 5.2.已過期,需要緩存重建// 6.緩存重建// 6.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判斷是否獲取鎖成功if (isLock){// 6.3.成功,開啟獨立線程,實現緩存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查詢數據庫R newR = dbFallback.apply(id);// 重建緩存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 釋放鎖unlock(lockKey);}});}// 6.4.返回過期的商鋪信息return r;}
?public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判斷命中的是否是空值if (shopJson != null) {// 返回一個錯誤信息return null;}
?// 4.實現緩存重建// 4.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判斷是否獲取成功if (!isLock) {// 4.3.獲取鎖失敗,休眠并重試Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.獲取鎖成功,根據id查詢數據庫r = dbFallback.apply(id);// 5.不存在,返回錯誤if (r == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return null;}// 6.存在,寫入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.釋放鎖unlock(lockKey);}// 8.返回return r;}
?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);}
}
在ShopServiceImpl 中@Resource
private CacheClient cacheClient;
?@Overridepublic Result queryById(Long id) {// 解決緩存穿透Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
?// 互斥鎖解決緩存擊穿// Shop shop = cacheClient// ? ? ? ? .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
?// 邏輯過期解決緩存擊穿// Shop shop = cacheClient// ? ? ? ? .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
?if (shop == null) {return Result.fail("店鋪不存在!");}// 7.返回return Result.ok(shop);}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/78655.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/78655.shtml
英文地址,請注明出處:http://en.pswp.cn/web/78655.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

頭歌禁止復制怎么解除(簡單版)

被頭歌數據庫作業禁止復制整神之后&#xff0c;主啵嘗試網上各種解除方法&#xff0c;最后發現一個最簡單且最快速的解除方法。 在瀏覽器中搜索萬能復制插件 下載完成之后就可以隨便復制粘貼啦 超簡單 下載只需幾秒

【無基礎】小白解決Docker pull時報錯:https://registry-1.docker.io/v2/

Docker Compose 啟動失敗問題解決方案 錯誤描述 執行 docker compose up -d 時出現以下錯誤&#xff1a; [] Running 9/9? api Error context canceled …

【數據結構】二叉樹、堆

文章目錄 二叉樹的概念及結構定義特殊的二叉樹核心性質存儲方式 二叉樹的鏈式存儲前序遍歷中序遍歷后序遍歷層序遍歷 二叉樹的順序存儲父子關系的推導堆&#xff08;heap&#xff09;堆的概念向上調整算法和向下調整算法向上調整算法向下調整算法 堆的創建堆的插入堆的刪除 堆的…

Vue3響應式原理那些事

文章目錄 1 響應式基礎:Proxy 與 Reflect1.1 Proxy 代理攔截1.2 Reflect 確保 `this` 指向正確1.2.1 修正 `this` 指向問題1.2.2 統一的操作返回值1.3 與 Vue2 的對比2 依賴收集與觸發機制2.1 全局依賴存儲結構:WeakMap → Map → Set2.2 依賴收集觸發時機2.3 依賴收集核心實…

精選10個好用的WordPress免費主題

10個好用的WordPress免費主題 1. Astra Astra 是全球最受歡迎的WordPress免費主題。它功能豐富&#xff0c;易于使用&#xff0c;SEO友好&#xff0c;是第一個安裝量突破100萬的非默認主題&#xff0c;并獲得了5000多個五星好評。 它完美集成了Elementor、Beaver&#xff0c;…

【SaaS多租架構】數據隔離與性能平衡

SaaS多租戶架構:數據隔離與性能平衡 一、技術背景及發展二、技術特點:數據隔離與性能優化的雙核心三、技術細節:實現路徑與關鍵技術四、實際案例分析五、未來發展趨勢結語一、技術背景及發展 多租戶架構是云計算與SaaS(軟件即服務)模式的核心技術,其核心目標是通過共享基…

部署GM DC Monitor 一體化監控預警平臺

1&#xff09;首先在官網下載鏡像文件 廣目&#xff08;北京&#xff09;軟件有限公司廣目&#xff08;北京&#xff09;軟件有限公司https://www.gm-monitor.com/col.jsp?id1142&#xff09;其次進行部署安裝&#xff0c;教程如下&#xff1a; 1. 基礎環境要求 1) 系統&…

Webug4.0靶場通關筆記15- 第19關文件上傳(畸形文件)

目錄 第19關 文件上傳(畸形文件) 1.打開靶場 2.源碼分析 &#xff08;1&#xff09;客戶端源碼 &#xff08;2&#xff09;服務器源碼 3.滲透實戰 &#xff08;1&#xff09;構造腳本 &#xff08;2&#xff09;雙寫繞過 &#xff08;3&#xff09;訪問腳本 本文通過《…

架構思維:構建高并發讀服務_熱點數據查詢的架構設計與性能調優

文章目錄 一、引言二、熱點查詢定義與場景三、主從復制——垂直擴容四、應用內前置緩存4.1 容量上限與淘汰策略4.2 延遲刷新&#xff1a;定期 vs. 實時4.3 逃逸流量控制4.4 熱點發現&#xff1a;被動 vs. 主動 五、降級與限流兜底六、前端&#xff0f;接入層其他應對七、模擬壓…

寶塔面板運行docker的jenkins

1.在寶塔面板裝docker&#xff0c;以及jenkins 2.ip:端口訪問jenkins 3.獲取密鑰&#xff08;點擊日志&#xff09; 4.配置容器內的jdk和maven環境&#xff08;直接把jdk和maven文件夾放到jenkins容器映射的data文件下&#xff09; 點擊容器-->管理-->數據存儲卷--.把相…

C語言 ——— 函數

目錄 函數是什么 庫函數 學習使用 strcpy 庫函數 自定義函數 寫一個函數能找出兩個整數中的最大值 寫一個函數交換兩個整型變量的內容 牛刀小試 寫一個函數判斷一個整數是否是素數 寫一個函數判斷某一年是否是閏年 寫一個函數&#xff0c;實現一個整型有序數組的二分…

筆記本電腦升級計劃(2017———2025)

ThinkPad T470 (2017) vs ThinkBook 16 (2025) 完整性能對比報告 一、核心硬件性能對比 1. CPU性能對比&#xff08;i5-7200U vs Ultra9-285H&#xff09; 參數i5-7200U (2017)Ultra9-285H (2025)提升百分比核心架構2核4線程 (Skylake)16核16線程 (6P8E2LPE)700%核心數制程工…

具身系列——PPO算法實現CartPole游戲(強化學習)

完整代碼參考&#xff1a; https://gitee.com/chencib/ailib/blob/master/rl/ppo_cartpole.py 執行結果&#xff1a; 部分訓練得分&#xff1a; (sd) D:\Dev\traditional_nn\feiai\test\rl>python ppo_cartpole_v2_succeed.py Ep: 0 | Reward: 23.0 | Running: 2…

Python項目源碼60:電影院選票系統1.0(tkinter)

1.功能特點&#xff1a;通常選票系統應該允許用戶選擇電影、場次、座位&#xff0c;然后顯示總價和生成票據。好的&#xff0c;我得先規劃一下界面布局。 首先&#xff0c;應該有一個電影選擇的列表&#xff0c;可能用下拉菜單Combobox來實現。然后場次時間&#xff0c;可能用…

【全隊項目】智能學術海報生成系統PosterGenius--圖片布局生成模型LayoutPrompt(2)

&#x1f308; 個人主頁&#xff1a;十二月的貓-CSDN博客 &#x1f525; 系列專欄&#xff1a; &#x1f3c0;大模型實戰訓練營_十二月的貓的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻擋不了春天的腳步&#xff0c;十二點的黑夜遮蔽不住黎明的曙光 目錄 1. 前…

Linux的時間同步服務器(附加詳細實驗案例)

一、計時方式的發展 1.古代計時方式? 公元前約 2000 年&#xff1a;古埃及人利用光線留下的影子計時&#xff0c;他們修建高聳的大型方尖碑&#xff0c;通過追蹤方尖碑影子的移動判斷時間&#xff0c;這是早期利用自然現象計時的典型方式 。?商朝時期&#xff1a;人們開發并…

【無需docker】mac本地部署dify

環境安裝準備 #安裝 postgresql13 brew install postgresql13 #使用zsh的在全局添加postgresql命令集 echo export PATH"/usr/local/opt/postgresql13/bin:$PATH" >> ~/.zshrc # 使得zsh的配置修改生效 source ~/.zshrc # 啟動postgresql brew services star…

(5)概述 QT 的元對象系統里的類的調用與聯系,及訪問接口

&#xff08;1&#xff09; QT 的元對象系統&#xff0c;這幾個字大家都知道&#xff0c;那么 QT 的元對象系統里都包含哪些內容呢&#xff0c;其訪問接口是如何呢&#xff1f; 從 QObject 類的實現里&#xff0c;從其數據成員里就可以看出來&#xff1a; QT 里父容器可以釋放其…

打包 Python 項目為 Windows 可執行文件:高效部署指南

Hypackpy 是一款由白月黑羽開發的 Python 項目打包工具&#xff0c;它與 PyInstaller 等傳統工具不同&#xff0c;通過直接打包解釋器環境和項目代碼&#xff0c;并允許開發者修改配置文件以排除不需要的內容&#xff0c;從而創建方便用戶一鍵運行的可執行程序。以下是使用 Hyp…

MySQL JOIN詳解:掌握數據關聯的核心技能

一、為什么需要JOIN&#xff1f; 在關系型數據庫中&#xff0c;數據通常被拆分到不同的表中以提高存儲效率。當我們需要從多個表中組合數據時&#xff0c;JOIN操作就成為了最關鍵的技能。通過本文&#xff0c;您將全面掌握MySQL中7種JOIN操作&#xff0c;并學會如何在實際場景中…