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