濱州網(wǎng)站建設(shè)phpi百度網(wǎng)盤(pán)帳號(hào)登錄入口
文章目錄
- 一、緩存更新策略
- 1、三種策略
- 2、策略選擇
- 3、主動(dòng)更新的方案
- 二、緩存存在的問(wèn)題
- 1、緩存穿透
- 2、緩存雪崩
- 3、緩存擊穿
- 三、解決緩存問(wèn)題
- 1、自定義分布式鎖
- 2、解決緩存穿透問(wèn)題
- 3、解決緩存擊穿問(wèn)題
一、緩存更新策略
1、三種策略
- 內(nèi)存淘汰:redis自帶的內(nèi)存淘汰機(jī)制
- 過(guò)期淘汰:利用expire命令給數(shù)據(jù)設(shè)置過(guò)期時(shí)間
- 主動(dòng)更新:主動(dòng)完成數(shù)據(jù)庫(kù)和緩存的同時(shí)更新
2、策略選擇
- 低一致性需求:內(nèi)存淘汰或過(guò)期淘汰
- 高一致性需求:主動(dòng)更新為主,過(guò)期淘汰兜底
3、主動(dòng)更新的方案
- Cache Aside:緩存調(diào)用者在更新數(shù)據(jù)庫(kù)的同時(shí)完成對(duì)緩存的更新
- 一致性良好
- 實(shí)現(xiàn)難度一般
- Read/Write Through:緩存與數(shù)據(jù)庫(kù)成為一個(gè)服務(wù),服務(wù)保證兩者的一致性,對(duì)外暴露的API接口。調(diào)用者調(diào)用API,無(wú)需知道自己操作的數(shù)據(jù)庫(kù)還是緩存,不關(guān)心一致性
- 一致性?xún)?yōu)秀
- 實(shí)現(xiàn)復(fù)雜
- 性能一般
- Write Back:緩存調(diào)用者的CRUD都針對(duì)緩存完成。由獨(dú)立線(xiàn)程異步的將緩存寫(xiě)到數(shù)據(jù)庫(kù),實(shí)現(xiàn)最終一致
- 一致性差
- 性能好
- 實(shí)現(xiàn)復(fù)雜
二、緩存存在的問(wèn)題
1、緩存穿透
產(chǎn)生原因:客戶(hù)端請(qǐng)求的數(shù)據(jù)在緩存和數(shù)據(jù)庫(kù)中都不存在。當(dāng)這種情況大量出現(xiàn)或被惡意攻擊時(shí),接口的訪(fǎng)問(wèn)全部透過(guò)Redis訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),而數(shù)據(jù)庫(kù)中也沒(méi)有這些數(shù)據(jù),我們稱(chēng)這種現(xiàn)象為"緩存穿透"。
解決方案:
- 緩存空對(duì)象:對(duì)于不存在的數(shù)據(jù)也在Redis建立緩存,值為空,設(shè)置一個(gè)較短的TTL時(shí)間
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,維護(hù)方便
- 缺點(diǎn):額外消耗內(nèi)存,短期的數(shù)據(jù)不一致
- 布隆過(guò)濾:利用布隆過(guò)濾算法,在請(qǐng)求Redis之前先判斷是否存在,如果不存在則直接拒絕訪(fǎng)問(wèn)
- 優(yōu)點(diǎn):內(nèi)存占用少
- 缺點(diǎn):實(shí)現(xiàn)復(fù)雜,存在誤判的可能性
- 其他方法:
- 做好數(shù)據(jù)的基礎(chǔ)格式校驗(yàn)
- 加強(qiáng)用戶(hù)權(quán)限校驗(yàn)
- 做好熱點(diǎn)數(shù)據(jù)的限流
布隆過(guò)濾器:
一種數(shù)據(jù)結(jié)構(gòu),由一串很長(zhǎng)的二進(jìn)制向量組成,可以將其看成一個(gè)二進(jìn)制數(shù)組。
當(dāng)要向布隆過(guò)濾器中添加一個(gè)元素key時(shí),我們通過(guò)多個(gè)hash函數(shù),算出一個(gè)值,然后將這個(gè)值所在的方格置為1。
因?yàn)槎鄠€(gè)不同的數(shù)據(jù)通過(guò)hash函數(shù)算出來(lái)的結(jié)果是會(huì)有重復(fù)的,所以布隆過(guò)濾器可以判斷某個(gè)數(shù)據(jù)一定不存在,但是無(wú)法判斷一定存在。
優(yōu)點(diǎn):優(yōu)點(diǎn)很明顯,二進(jìn)制組成的數(shù)組,占用內(nèi)存極少,并且插入和查詢(xún)速度都足夠快。
缺點(diǎn):隨著數(shù)據(jù)的增加,誤判率會(huì)增加;還有無(wú)法判斷數(shù)據(jù)一定存在;另外還有一個(gè)重要缺點(diǎn),無(wú)法刪除數(shù)據(jù)。
2、緩存雪崩
產(chǎn)生原因:在同一時(shí)間段大量的緩存key同時(shí)失效或者Redis服務(wù)宕機(jī),導(dǎo)致大量請(qǐng)求到達(dá)數(shù)據(jù)庫(kù),帶來(lái)巨大壓力
解決方案:
- 給不同的Key的TTL設(shè)置隨機(jī)值
- 利用Redis集群提高服務(wù)的可用性
- 誒緩存業(yè)務(wù)添加降級(jí)限流策略
- 給業(yè)務(wù)添加多級(jí)緩存
3、緩存擊穿
產(chǎn)生原因:熱點(diǎn)Key在某一個(gè)時(shí)間段被高并發(fā)訪(fǎng)問(wèn),而此時(shí)Key正好過(guò)期,如果重建緩存時(shí)間耗時(shí)長(zhǎng),在這段時(shí)間內(nèi)大量請(qǐng)求剾數(shù)據(jù)庫(kù),帶來(lái)巨大沖擊
解決方案:
- 設(shè)置value永不過(guò)期:通過(guò)定時(shí)任務(wù)進(jìn)行數(shù)據(jù)庫(kù)查詢(xún)更新緩存,當(dāng)然前提時(shí)不會(huì)給數(shù)據(jù)庫(kù)造成壓力過(guò)大
- 優(yōu)點(diǎn):最可靠,性能好
- 缺點(diǎn):占空間,內(nèi)存消耗大,一致性差
- 互斥鎖:給緩存重建過(guò)程加鎖,確保重建過(guò)程只有一個(gè)線(xiàn)程執(zhí)行,其他線(xiàn)程等待
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,沒(méi)有額外內(nèi)存消耗,一致性好
- 缺點(diǎn):等待導(dǎo)致性能下降,有死鎖風(fēng)險(xiǎn)
- 邏輯過(guò)期:熱點(diǎn)Key緩存永不過(guò)期,認(rèn)識(shí)設(shè)置一個(gè)邏輯過(guò)期時(shí)間,查詢(xún)到數(shù)據(jù)時(shí)通過(guò)對(duì)邏輯時(shí)間判斷,來(lái)決定是否需要進(jìn)行緩存重建。重建過(guò)程也通過(guò)互斥鎖來(lái)保證單線(xiàn)程執(zhí)行。利用獨(dú)立線(xiàn)程異步執(zhí)行,其他線(xiàn)程無(wú)需等待,直接查詢(xún)到舊的數(shù)據(jù)即可。
- 優(yōu)點(diǎn):線(xiàn)程無(wú)需等待,性能較好
- 缺點(diǎn):不保證一致性,有額外內(nèi)存消耗,實(shí)現(xiàn)復(fù)雜
private final RedisTemplate<String, String> redisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(20), r -> new Thread(r, "cache_rebuild"));public CacheClient(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;
}public void setWithLogicalExpire(String key, Object value, Long expireTime, TimeUnit unit) {// 設(shè)置邏輯過(guò)期時(shí)間RedisData redisData = new RedisData();redisData.setValue(value);redisData.setExpireTime(LocalDateTime.now().plusNanos(unit.toNanos(expireTime)));redisTemplate.opsForValue().set(key, JSON.toJSONString(redisData));
}/*** 邏輯過(guò)期,互斥鎖獲取值,用于避免熱點(diǎn)數(shù)據(jù)出現(xiàn)緩存擊穿*/
public <R, V> R getMutex(String keyPrefix, V id, Class<R> clazz, Function<V, R> dbFallback, Long expireTime, TimeUnit unit) {String key = keyPrefix + id;String value = redisTemplate.opsForValue().get(key);if (StringUtils.isBlank(value)) {return null;}RedisData redisData = JSON.parseObject(value, RedisData.class);R result = JSONUtil.toBean((JSONObject) redisData.getValue(), clazz);if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {return result;}// 如果緩存已過(guò)期,則嘗試更新String localKey = RedisConstant.LOCK + id;// 獲取鎖成功if (getLock(localKey)) {// 異步更新緩存CACHE_REBUILD_EXECUTOR.submit(() -> {try {R res = dbFallback.apply(id);this.setWithLogicalExpire(key, res, expireTime, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {unLock(localKey);}});}return result;
}private boolean getLock(String key) {// 直接返回會(huì)進(jìn)行自動(dòng)拆箱,可能會(huì)出現(xiàn)空指針異常return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, "1"));
}private void unLock(String key) {redisTemplate.delete(key);
}
三、解決緩存問(wèn)題
1、自定義分布式鎖
/*** <pre>* 簡(jiǎn)易實(shí)現(xiàn)的Redis分布式鎖* </pre>** @author <a href="https://github.com/Ken-Chy129">Ken-Chy129</a>* @date 2023/2/26 21:18*/
public class SimpleRedisLock {private final RedisTemplate<String, String> redisTemplate;/**鎖的名字,根據(jù)業(yè)務(wù)設(shè)置*/private final String lockName;/*** key前綴*/private static final String KEY_PREFIX = "lock:";/*** value中線(xiàn)程標(biāo)識(shí)的前綴(為每個(gè)節(jié)點(diǎn)提供一個(gè)隨機(jī)的前綴,避免集群部署下線(xiàn)程id出現(xiàn)重復(fù)而導(dǎo)致value出現(xiàn)相同的情況)*/private static final String ID_PREFIX = UUID.fastUUID().toString(true);/*** 釋放鎖邏輯的lua腳本*/private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public SimpleRedisLock(String lockName, RedisTemplate<String, String> redisTemplate) {this.lockName = lockName;this.redisTemplate = redisTemplate;}public boolean tryLock(long timeoutSec) {long threadId = Thread.currentThread().getId();// 返回的是Boolean類(lèi)型,直接return會(huì)進(jìn)行自動(dòng)拆箱,可能會(huì)出現(xiàn)空指針異常// 需要為鎖設(shè)置過(guò)期時(shí)間,防止因服務(wù)宕機(jī)而導(dǎo)致鎖無(wú)法釋放return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + lockName, ID_PREFIX + threadId, timeoutSec, TimeUnit.SECONDS));}public void unlock() {redisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + lockName),ID_PREFIX + Thread.currentThread().getId());}
}
Lua腳本——unlock.lua
--- 比較線(xiàn)程標(biāo)識(shí)與鎖中的標(biāo)識(shí)是否一致 if(redis.call('get', KEYS[1]) == ARGS[1]) then--- 釋放鎖return redis.call('del', KEYS[1]) end return 0
使得釋放鎖的操作具有原子性
Redis是單線(xiàn)程處理,本身不會(huì)存在并發(fā)問(wèn)題,但是由于可能有多個(gè)客戶(hù)端訪(fǎng)問(wèn),每個(gè)客戶(hù)端會(huì)有一個(gè)線(xiàn)程,之間存在競(jìng)爭(zhēng),所以服務(wù)端收到的指令有可能出現(xiàn)多個(gè)客戶(hù)端的指令穿插,而lua腳本可以保證多條指令的原子性從而解決并發(fā)問(wèn)題
2、解決緩存穿透問(wèn)題
/*** 避免緩存穿透的獲取*/
public <R, V> R get(String keyPrefix, V id, Class<R> clazz, Function<V, R> dbFallback, Long expireTime, TimeUnit unit) {String key = keyPrefix + id;// 查詢(xún)緩存String value = redisTemplate.opsForValue().get(key);// 緩存存在則直接返回if (StringUtils.isNotBlank(value)) {return JSON.parseObject(value, clazz);}// 緩存不存在(到此處說(shuō)明value要么是空,要么是null)if (value != null) {// 不為null則說(shuō)明為“”,代表數(shù)據(jù)不存在,直接返回null,不用查詢(xún)數(shù)據(jù)庫(kù)(解決緩存穿透問(wèn)題)return null;}// value為null則查詢(xún)數(shù)據(jù)庫(kù)獲取數(shù)據(jù)進(jìn)行更新R result = dbFallback.apply(id);if (result == null) {// 數(shù)據(jù)庫(kù)查詢(xún)不到結(jié)果,則存入空串避免緩存穿透redisTemplate.opsForValue().set(key, "", RedisConstant.CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 查詢(xún)到結(jié)果,寫(xiě)回緩存this.set(key, result, expireTime, unit);return result;
}
3、解決緩存擊穿問(wèn)題
/*** 邏輯過(guò)期,互斥鎖獲取值,用于避免熱點(diǎn)數(shù)據(jù)出現(xiàn)緩存擊穿*/
public <R, V> R getMutex(String keyPrefix, V id, Class<R> clazz, Function<V, R> dbFallback, Long expireTime, TimeUnit unit) {String key = keyPrefix + id;String value = redisTemplate.opsForValue().get(key);if (StringUtils.isBlank(value)) {return null;}RedisData redisData = JSON.parseObject(value, RedisData.class);R result = JSONUtil.toBean((JSONObject) redisData.getValue(), clazz);if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {return result;}// 如果緩存已過(guò)期,則獲取鎖嘗試更新SimpleRedisLock lock = new SimpleRedisLock(key, redisTemplate);// 獲取鎖成功if (lock.tryLock(5)) {// 異步更新緩存CACHE_REBUILD_EXECUTOR.submit(() -> {try {R res = dbFallback.apply(id);this.setWithLogicalExpire(key, res, expireTime, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {lock.unlock();}});}return result;
}