我先說一下正常的業務流程:需要查詢店鋪數據,我們會先從redis中查詢,判斷是否能命中,若命中說明redis中有需要的數據就直接返回;沒有命中就需要去mysql數據庫查詢,在數據庫中查到了就返回數據并把該數據存入redis中,若mysql數據庫中也查不到就返回null,并返回錯誤信息:該信息不存在。
代碼是用springboot+mybatis plus +redis+mysql實現的。
想看最初的mapper,service,controller層代碼,就是解決緩存擊穿之前的代碼的話,可以去我的緩存穿透文章中看看,里面有,這里就不在寫一遍了。
?下面讓我來簡單解釋一下什么是緩存擊穿:
? 緩存擊穿問題也叫熱點key問題,就是一個被高并發訪問并且緩存重建業務復雜的存儲在redis中的key突然失效,無數請求就會瞬間打到數據庫造成巨大沖擊。
?解決方法:有倆個
? 一個是互斥鎖:這個互斥鎖只能有一個線程拿到,拿到互斥鎖的線程才能去查詢數據庫,并寫入redis緩存,期間其他查詢該數據的線程會全進入等待。缺點:性能差,且存在死鎖的可能。
?另一個是邏輯過期時間:這個是不給存入的key設置過期時間,而是將過期時間寫入value中,時間過期后,一個線程獲取互斥鎖然后另開一個新線程去查詢數據庫,寫入緩存并釋放鎖。而老線程直接返回查到的舊數據,期間其他獲取互斥鎖失敗的線程查詢也會返回舊數據。缺點:有額外的內存消耗,不保證數據一致性,實現優點復雜。這個另寫一個文章來進行代碼實現。本文章只說用互斥鎖解決。
代碼實現:
?互斥鎖:實現互斥鎖,我們用的是redis的setnx key value命令,該命令只有在key不存在時才會創建成功,若key已存在就會創建失敗。
? 我們先寫一下獲取互斥鎖和釋放鎖的方法
private boolean tryLock(String key) {//參數分別是,key,value,過期時間,過期時間的單位//這里過期時間用的事先寫的靜態變量,10LBoolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag); //如果直接返回flag,當flag為null時,會做拆箱,報錯空指針。}private void UnLock(String key) {stringRedisTemplate.delete(key);}
用互斥鎖解決緩存擊穿:
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate CacheClient cacheClient;public Result queryById(Long id) {//緩存穿透//Shop shop = queryWithPassThrough(id);//用互斥鎖解決緩存擊穿Shop shop = queryWithMutex(id);if (shop==null){return Result.fail("店鋪不存在");}return Result.ok(shop);}public Shop queryWithMutex(Long id) {//1.從redis查詢數據緩存String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);//2.判斷是否存在if (StrUtil.isNotBlank(shopJson)) { //isNOtBlank方法只有有值字符串才會返回true,null和空值都會返回false//3.存在,返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}//shopJson不存在//判斷查到的數據是否為空值(這個空值指的不是null,是空字符串)if (shopJson != null) {//返回錯誤信息return null;}//4實現緩存重建//4.1獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean lock = tryLock(lockKey);//4.2判斷是否獲取成功Shop shop = null;try {if (!lock) {//4.3失敗,休眠并重試Thread.sleep(50);return queryWithMutex(id);}//4.4成功,根據id查詢數據庫shop = getById(id);//模擬數據庫重建的延時Thread.sleep(200);//5.不存在,返回錯誤if (shop == null) {//將空值緩存到redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6.存在,寫入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {//7.釋放互斥鎖UnLock(lockKey);}//8.返回return shop;}
}
下面讓我們來用Jmeter測試一下:
?開啟100個線程去測試,結果都成功了,然后我們去idea控制臺看看查詢了數據庫幾次
?由返回信息可知,只查詢了一次數據庫,所以解決緩存擊穿成功。