什么是緩存
緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache),是存貯數(shù)據(jù)的臨時(shí)地方,一般讀寫性能較高。
添加 redis 緩存
給店鋪類型查詢業(yè)務(wù)添加緩存
需求:添加ShopTypeController中的queryTypeList方法,添加查詢緩存
緩存更新策略
業(yè)務(wù)場景:
低一致性需求:使用內(nèi)存淘汰機(jī)制。例如店鋪類型的查詢緩存
高一致性需求:主動(dòng)更新,并以超時(shí)剔除作為兜底方案。例如店鋪詳情查詢的緩存
操作緩存和數(shù)據(jù)庫時(shí)有三個(gè)問題需要考慮:
1.刪除緩存還是更新緩存
-
更新緩存:每次更新數(shù)據(jù)庫都更新緩存,無效寫操作較多(×)
-
刪除緩存:更新數(shù)據(jù)庫時(shí)讓緩存失效,查詢時(shí)再更新緩存(?)
2.如何保證緩存與數(shù)據(jù)庫的操作同時(shí)成功或失敗
-
單體系統(tǒng):將緩存與數(shù)據(jù)庫操作放在一個(gè)事務(wù)
-
分布式系統(tǒng):利用TCC等分布式事務(wù)方案
3.先操作緩存還是先操作數(shù)據(jù)庫
-
先刪除緩存,在操作數(shù)據(jù)庫
-
先操作數(shù)據(jù)庫,再刪除緩存
讀操作:
-
緩存命中則直接返回
-
緩存未命中則查詢數(shù)據(jù)庫,并寫入緩存,設(shè)定超時(shí)時(shí)間
寫操作:
-
先寫數(shù)據(jù)庫,然后再刪除緩存
-
要確保數(shù)據(jù)庫與緩存操作的原子性
給查詢商鋪的緩存添加超時(shí)剔除和主動(dòng)更新的策略
修改ShopController的業(yè)務(wù)邏輯,滿足下面的需求
① 根據(jù)id查詢商鋪時(shí),如果緩存未命中,則查詢數(shù)據(jù)庫,將數(shù)據(jù)庫數(shù)據(jù)寫入緩存,并設(shè)置超時(shí)時(shí)間
//6.存在,將商鋪數(shù)據(jù)寫入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
② 根據(jù)id修改店鋪時(shí),先修改數(shù)據(jù)庫,再修改緩存
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id==null) {
return Result.fail("店鋪id不為空");
}
//1更新數(shù)據(jù)庫
updateById(shop);
//2.刪除緩存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
緩存穿透
緩存穿透是指客戶端請(qǐng)求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,這樣緩存永遠(yuǎn)不會(huì)生效,這些請(qǐng)求都會(huì)打到數(shù)據(jù)庫
常見的解決方案有兩種:
1.緩存空對(duì)象
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,維護(hù)方便
缺點(diǎn):額外的內(nèi)存消耗,可能造成短期的數(shù)據(jù)不一致
2.布隆過濾
優(yōu)點(diǎn):內(nèi)存占用少,沒有多余的key
缺點(diǎn):實(shí)現(xiàn)復(fù)雜,存在誤判
其他的解決方案
1.增加id的復(fù)雜度,避免被猜測id規(guī)律
2.做好數(shù)據(jù)的基礎(chǔ)格式校驗(yàn)
3.加強(qiáng)用戶權(quán)限校驗(yàn)
4.做好熱點(diǎn)參數(shù)的限流
緩存雪崩
緩存雪崩是指在同一時(shí)段大量的緩存key同時(shí)失效或者Redis服務(wù)宕機(jī),導(dǎo)致大量請(qǐng)求到達(dá)數(shù)據(jù)庫,帶來巨大壓力
解決方案:
1.給不同的key的TTL添加隨機(jī)值
2.利用redis集群提高服務(wù)的可用性
3.給緩存業(yè)務(wù)添加降級(jí)限流策略
4.給業(yè)務(wù)添加多級(jí)緩存
緩存擊穿
緩存擊穿問題也叫熱點(diǎn)key問題,就是一個(gè)被高并發(fā)訪問并且緩存重建業(yè)務(wù)較復(fù)雜的
key突然失效了,無數(shù)的請(qǐng)求訪問會(huì)在瞬間給數(shù)據(jù)庫帶來巨大的沖擊
常見的解決方案有兩種:
1.互斥鎖
2.邏輯過期
優(yōu)缺點(diǎn):
基于互斥鎖方式解決緩存擊穿問題
需求:修改根據(jù)id查詢商鋪的業(yè)務(wù),基于互斥鎖來解決緩存擊穿問題
基于邏輯過期解決緩存擊穿問題
需求:修改根據(jù)id查詢商鋪的業(yè)務(wù),基于邏輯過期方式來解決緩存擊穿問題
緩存工具封裝
基于StringRedisTemplate封裝一個(gè)緩存工具類,滿足下列需求:
方法:
1.將任意Java對(duì)象序列化為json并存儲(chǔ)在String類型的key中,并且設(shè)置TTL過期時(shí)間
2.將任意Java對(duì)象序列化為json并存儲(chǔ)在String類型的key中,并且設(shè)置邏輯過期時(shí)間,用于處理緩存擊穿問題
3.根據(jù)指定的key查詢緩存,并反序列化為指定類型,利用緩存空值的方式解決緩存穿透問題
4.根據(jù)指定的key查詢緩存,并反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題
CacheClient
@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 setWithLogicExpire(String key, Object value, Long time, TimeUnit unit) {
//設(shè)置邏輯過期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//寫入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
//解決緩存穿透
public <R, ID> R queryWithPassThrough(
String kerPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
//1.查詢商鋪緩存
String key = kerPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
//2.判斷是否存在
if (StrUtil.isNotBlank(json)) {
//3.緩存存在,返回
return JSONUtil.toBean(json, type);
}
//判斷命中的是否是空值
if (json != null) {
//返回一個(gè)錯(cuò)誤的信息
return null;
}
//4.不存在,根據(jù)id查詢數(shù)據(jù)庫
R r = dbFallback.apply(id);
//5.不存在,返回錯(cuò)誤
if (r == null) {
//將空值寫入數(shù)據(jù)庫
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,將商鋪數(shù)據(jù)寫入redis
this.set(key, r, time, unit);
//7.返回
return r;
}
//解決緩存擊穿
public <R, ID> R queryWithLogicExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack, Long time, TimeUnit unit) {
//1.查詢商鋪緩存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
//2.判斷是否存在
if (StrUtil.isBlank(json)) {
//3.緩存存在,返回
return null;
}
//4.命中,需要把json反序列化為對(duì)象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
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成功,開啟獨(dú)立線程,實(shí)現(xiàn)緩存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//查詢數(shù)據(jù)庫
R r1 = dbFallBack.apply(id);
//寫入redis
this.setWithLogicExpire(key, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//釋放鎖
unLock(lockKey);
}
});
}
//6.4返回過期的商鋪信息
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.MINUTES);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}
ShopServiceImpl文章來源:http://www.zghlxwxcb.cn/news/detail-408641.html
@Resource
private CacheClient cacheClient;
@Override
public Result queryById(Long id) {
//緩存穿透
//Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
//Shop shop = queryWithPassThrough(id);
//互斥鎖解決緩存擊穿
//Shop shop = queryWithMutex(id);
//邏輯過期解決緩存擊穿
Shop shop = cacheClient.queryWithLogicExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
//Shop shop = queryWithLogicExpire(id);
if (shop == null) {
return Result.fail("店鋪不存在");
}
//7.返回
return Result.ok(shop);
}
視頻地址文章來源地址http://www.zghlxwxcb.cn/news/detail-408641.html
到了這里,關(guān)于緩存穿透、緩存雪崩、緩存擊穿解決方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!