Redis解決緩存擊穿問(wèn)題的兩種方法
引言
緩存擊穿:給某一個(gè)key設(shè)置了過(guò)期時(shí)間,當(dāng)key過(guò)期的時(shí)候,恰好這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)key有大量的并發(fā)請(qǐng)求過(guò)來(lái),這些并發(fā)的請(qǐng)求可能會(huì)瞬間把DB壓垮
解決辦法
互斥鎖(強(qiáng)一致,性能差)

根據(jù)圖片就可以看出,我們的思路就是只能讓一個(gè)線程能夠進(jìn)行訪問(wèn)Redis,要想實(shí)現(xiàn)這個(gè)功能,我們也可以使用Redis自帶的setnx
封裝兩個(gè)方法,一個(gè)寫(xiě)key來(lái)嘗試獲取鎖另一個(gè)刪key來(lái)釋放鎖
/**
* 嘗試獲取鎖
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 釋放鎖
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}在并行情況下每當(dāng)其他線程想要獲取鎖,來(lái)訪問(wèn)緩存都要通過(guò)將自己的key寫(xiě)到tryLock()方法里,setIfAbsent()返回false則說(shuō)明有線程在在更新緩存數(shù)據(jù),鎖未釋放。若返回true則說(shuō)明當(dāng)前線程拿到鎖了可以訪問(wèn)緩存甚至操作緩存。
我們?cè)谙旅嬉粋€(gè)熱門的查詢場(chǎng)景中用代碼用代碼來(lái)實(shí)現(xiàn)互斥鎖解決緩存擊穿,代碼如下:
/**
* 解決緩存擊穿的互斥鎖
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
//1.從Redis查詢緩存
String shopJson = stringRedisTemplate.opsForValue().get(key); //JSON格式
//2.判斷是否存在
if (StrUtil.isNotBlank(shopJson)) { //不為空就返回 此工具類API會(huì)判斷" "為false
//存在則直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//return Result.ok(shop);
return shop;
}
//3.判斷是否為空值 這里過(guò)濾 " "的情況,不用擔(dān)心會(huì)一直觸發(fā)這個(gè)條件因?yàn)樗蠺TL
if (shopJson != null) {
//返回一個(gè)空值
return null;
}
//4.緩存重建 Redis中值為null的情況
//4.1獲得互斥鎖
String lockKey = "lock:shop"+id;
Shop shopById=null;
try {
boolean isLock = tryLock(lockKey);
//4.2判斷是否獲取成功
if (!isLock){
//4.3失敗,則休眠并重試
Thread.sleep(50);
return queryWithMutex(id);
}
//4.4成功,根據(jù)id查詢數(shù)據(jù)庫(kù)
shopById = getById(id);
//5.不存在則返回錯(cuò)誤
if (shopById == null) {
//將空值寫(xiě)入Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
//為什么這里要存一個(gè)" "這是因?yàn)槿绻罄m(xù)DB中有數(shù)據(jù)補(bǔ)充的話還可以去重建緩存
//return Result.fail("暫無(wú)該商鋪信息");
return null;
}
//6.存在,寫(xiě)入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.釋放互斥鎖
unlock(lockKey);
}
return shopById;
}邏輯過(guò)期(高可用,性能優(yōu))
方案:用戶查詢某個(gè)熱門產(chǎn)品信息,如果緩存未命中(即信息為空),則直接返回空,不去查詢數(shù)據(jù)庫(kù)。如果緩存信息命中,則判斷是否邏輯過(guò)期,未過(guò)期返回緩存信息,過(guò)期則重建緩存,嘗試獲得互斥鎖,獲取失敗則直接返回已過(guò)期緩存數(shù)據(jù),獲取成功則開(kāi)啟獨(dú)立線程去重構(gòu)緩存然后直接返回舊的緩存信息,重構(gòu)完成之后就釋放互斥鎖。

封裝一個(gè)方法用來(lái)模擬更新邏輯過(guò)期時(shí)間與緩存的數(shù)據(jù)在測(cè)試類里運(yùn)行起來(lái)達(dá)到數(shù)據(jù)與熱的效果
/**
* 添加邏輯過(guò)期時(shí)間
*
* @param id
* @param expireTime
*/
public void saveShopRedis(Long id, Long expireTime) {
//查詢店鋪信息
Shop shop = getById(id);
//封裝邏輯過(guò)期時(shí)間
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
//將封裝過(guò)期時(shí)間和商鋪數(shù)據(jù)的對(duì)象寫(xiě)入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}查詢接口:
/**
* 邏輯過(guò)期解決緩存擊穿
*
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
String key = CACHE_SHOP_KEY + id;
Thread.sleep(200);
//1.從Redis查詢緩存
String shopJson = stringRedisTemplate.opsForValue().get(key); //JSON格式
//2.判斷是否存在
if (StrUtil.isBlank(shopJson)) {
//不存在則直接返回
return null;
}
//3.判斷是否為空值
if (shopJson != null) {
//返回一個(gè)空值
//return Result.fail("店鋪不存在!");
return null;
}
//4.命中
//4.1將JSON反序列化為對(duì)象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//4.2判斷是否過(guò)期
if (expireTime.isAfter(LocalDateTime.now())) {
//5.未過(guò)期則返回店鋪信息
return shop;
}
//6.過(guò)期則緩存重建
//6.1獲取互斥鎖
String LockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(LockKey);
//6.2判斷是否成功獲得鎖
if (isLock) {
//6.3成功,開(kāi)啟獨(dú)立線程,實(shí)現(xiàn)緩存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建緩存
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//釋放鎖
unlock(LockKey);
}
});
}
//6.4返回商鋪信息
return shop;
}設(shè)計(jì)邏輯過(guò)期時(shí)間
可以用這個(gè)方法設(shè)置邏輯過(guò)期時(shí)間
import org.redisson.Redisson;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonExample {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
String key = "exampleKey";
String value = "exampleValue";
int timeout = 10; // 過(guò)期時(shí)間(秒)
// 獲取RBucket對(duì)象
RBucket<String> bucket = redisson.getBucket(key);
// 設(shè)置值并指定過(guò)期時(shí)間
bucket.set(value, timeout, TimeUnit.SECONDS);
System.out.println("設(shè)置成功");
redisson.shutdown();
}
}大家可以看到,邏輯過(guò)期鎖就是可以實(shí)現(xiàn)并發(fā),所以他的效率更快,性能更好
但是
犧牲了數(shù)據(jù)的實(shí)時(shí)性,以保證高并發(fā)場(chǎng)景下的服務(wù)可用性和數(shù)據(jù)庫(kù)的穩(wěn)定性。
在實(shí)際應(yīng)用中,需要確保獲取互斥鎖的操作是原子的,并且鎖具有合適的超時(shí)時(shí)間,以避免死鎖的發(fā)生。
邏輯過(guò)期策略適用于那些對(duì)數(shù)據(jù)實(shí)時(shí)性要求不高,但要求服務(wù)高可用性的場(chǎng)景。
到此這篇關(guān)于Redis解決緩存擊穿問(wèn)題的兩種方法的文章就介紹到這了,更多相關(guān)Redis解決緩存擊穿內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析Redis底層數(shù)據(jù)結(jié)構(gòu)Dict
Redis是一個(gè)鍵值型的數(shù)據(jù)庫(kù),我們可以根據(jù)鍵實(shí)現(xiàn)快速的增刪改查,而鍵與值的映射關(guān)系正是通過(guò)Dict來(lái)實(shí)現(xiàn)的,當(dāng)然?Dict?也是?Set?Hash?的實(shí)現(xiàn)方式,本文就詳細(xì)帶大家介紹一下Redis底層數(shù)據(jù)結(jié)構(gòu)?Dict,,需要的朋友可以參考下2023-05-05
redis底層數(shù)據(jù)結(jié)構(gòu)之skiplist實(shí)現(xiàn)示例
這篇文章主要為大家介紹了redis底層數(shù)據(jù)結(jié)構(gòu)之skiplist實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
基于redis實(shí)現(xiàn)世界杯排行榜功能項(xiàng)目實(shí)戰(zhàn)
前段時(shí)間,做了一個(gè)世界杯競(jìng)猜積分排行榜。對(duì)世界杯64場(chǎng)球賽勝負(fù)平進(jìn)行猜測(cè),猜對(duì)+1分,錯(cuò)誤+0分,一人一場(chǎng)只能猜一次。下面通過(guò)本文給大家分享基于redis實(shí)現(xiàn)世界杯排行榜功能項(xiàng)目實(shí)戰(zhàn),感興趣的朋友一起看看吧2018-10-10
Redis遠(yuǎn)程字典服務(wù)器?hash類型示例詳解
這篇文章主要介紹了Redis遠(yuǎn)程字典服務(wù)器?hash類型示例詳解,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08
Redis高并發(fā)防止秒殺超賣實(shí)戰(zhàn)源碼解決方案
本文主要介紹了Redis高并發(fā)防止秒殺超賣實(shí)戰(zhàn)源碼解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
使用Docker部署Redis并配置持久化與密碼保護(hù)的詳細(xì)步驟
本文將詳細(xì)介紹如何使用 Docker 部署 Redis,并通過(guò) redis.conf 配置文件實(shí)現(xiàn)數(shù)據(jù)持久化和密碼保護(hù),適合在生產(chǎn)環(huán)境中使用,文章通過(guò)代碼示例講解的非常詳細(xì),需要的朋友可以參考下2025-03-03

