1. 穿透問題
緩存穿透問題就是查詢不存在的數據。在緩存穿透中,先查緩存,緩存沒有數據,就會請求到數據庫上,導致數據庫壓力劇增。
解決方法:
- 給不存在的key加上空值,防止每次都會請求到數據庫。
- 布隆過濾器,做一次過濾
1.1 使用緩存空值解決緩存擊穿問題
- 根據id=1來請求
- redis存在數據
2.1. 存儲的是空值{},那么返回null
2.2. 存儲的不是空值,說明存儲的是真實的數據庫數據- redis不存在數據
- 查詢數據庫
4.1. 數據庫存在數據,那么緩存數據到redis,返回真實的數據
4.2. 數據庫不存在數據,那么緩存空對象 {},設置一個過期時間,返回空
@Component
public class RedisCacheClient {private final StringRedisTemplate stringRedisTemplate;public RedisCacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}private void set(String key, Object value, Long time, TimeUnit timeUnit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);}private String get(String key) {return stringRedisTemplate.opsForValue().get(key);}public <ID, R> R queryWithPassThrough(String keyPrefix, ID id, Class<R> clazz,Function<ID, R> dbFallBack, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.從redis查詢數據String json = get(key);// 2.判斷數據是否存在if (RedisConstants.EMPTY_OBJECT_JSON.equals(json)) {return null; //緩存的空對象值}if (StrUtil.isNotEmpty(json)) {return JSONUtil.toBean(json, clazz);}// 3.不存在,根據id查詢數據庫R r = dbFallBack.apply(id);if (r != null) {set(key, r, time, unit);return r;}// 4.存儲空對象set(key, RedisConstants.EMPTY_OBJECT_JSON /*{}*/, RedisConstants.CACHE_NULL_TTL, TimeUnit.SECONDS);return null;}
}
1.2 使用布隆過濾器做初次判斷
對于惡意攻擊,向服務器請求大量不存在的數據造成的緩存穿透,還可以用布隆過濾器先做一次過濾,對于不存在的數據,布隆過濾器一般都能夠過濾掉,不讓請求再往后端發送。當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。
布隆過濾器就是一個大型的位數組和幾個不一樣的無偏 hash 函數。所謂無偏就是能夠把元素的 hash 值算得比較均勻。
向布隆過濾器中添加 key 時,會使用多個 hash 函數對 key 進行 hash 算得一個整數索引值然后對位數組長度 進行取模運算得到一個位置,每個 hash 函數都會算得一個不同的位置。再把位數組的這幾個位置都置為 1 就 完成了 add 操作。向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位數組中這幾個位置是否都為 1,只要有一個位為 0,那么說明布隆過濾器中這個key 不存在。如果都是 1,這并不能說明這個key 就一定存在,只是極有可能存在,因為這些位被置為 1 可能是因為其它的 key 存在所致。如果這個位數組比較稀疏,這個概率就會很大,如果這個位數組比較擁擠,這個概率就會降低。
這種方法適用于數據命中不高、 數據相對固定、 實時性低(通常是數據集較大) 的應用場景, 代碼維護較為復雜, 但是緩存空間占用很少。
1.2.1 導入pom坐標
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>
1.2.2 布隆過濾器代碼示例
class Main {private RedissonClient redissonClient;void test() {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("orderList");// 1.初始化布隆過濾器:預計元素為100000000L,誤判率為3%,根據這兩個參數會計算出底層的bit數組大小bloomFilter.tryInit(100000000L, 0.03);// 2.添加元素到bloomFilterbloomFilter.add("ayuan");// 3.判斷下面的數據是否在布隆過濾器中System.out.println(bloomFilter.contains("asheng"));System.out.println(bloomFilter.contains("longge"));System.out.println(bloomFilter.contains("ayuan"));}
}
使用布隆過濾器需要把所有數據提前放入布隆過濾器,并且在增加數據時也要往布隆過濾器里放,布隆過濾器緩存過濾偽代碼:
1.2.3 布隆過濾器實戰
class Main {@Autowiredprivate RedissonClient redissonClient;private RBloomFilter<String> bloomFilter;@PostConstructvoid init() {// 1.初始化布隆過濾器bloomFilter = redissonClient.getBloomFilter("orderList");// 初始化布隆過濾器:預計元素為100000000L,誤判率為3%,根據這兩個參數會計算出底層的bit數組大小bloomFilter.tryInit(100000000L, 0.03);// 2.加載所有的數據加載到布隆過濾器// for (String key : keys) {// bloomFilter.add(key);// }}@TestString get(String key) {// 3.從布隆過濾器這一級緩存判斷key是否存在boolean isContains = bloomFilter.contains(key);if (!isContains) {return "";}// 4.業務邏輯開發}
}
但是布隆過濾器無法刪除某一個元素,如果要刪除得重新初始化數據
2. 擊穿問題
緩存擊穿中,請求的 key 對應的是熱點數據 ,該數據存在于數據庫中,但不存在于緩存中(通常是因為緩存中的那份數據已經過期) 。這就可能會導致瞬時大量的請求直接打到了數據庫上,對數據庫造成了巨大的壓力,可能直接就被這么多請求弄宕機了。
解決方案:
- 基于互斥鎖(看情況):在緩存過期后,通過設置互斥鎖確保只有一個請求去查詢數據庫并且更新緩存。
- 提前預熱(推薦):針對熱點數據提前預熱,并將其入緩存中并設置合理的過期事件,比如:秒殺場景下的數據在秒殺結束前永不過期。
- 數據永不過期(不推薦):設置熱點數據永不過期或者過期時間比較長。
2.1 基于互斥鎖解決緩存擊穿問題
@Component
public class RedisCacheClient {private final StringRedisTemplate stringRedisTemplate;private final RedissonClient redissonClient;public RedisCacheClient(StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient) {this.stringRedisTemplate = stringRedisTemplate;this.redissonClient = redissonClient;}private void set(String key, Object value, Long time, TimeUnit timeUnit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);}private String get(String key) {return stringRedisTemplate.opsForValue().get(key);}public <ID, R> R query(String keyPrefix, ID id, Class<R> clazz,Function<ID, R> dbFallBack, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.從redis查詢數據String json = get(key);// 2.判斷數據是否存在if (RedisConstants.EMPTY_OBJECT_JSON.equals(json)) {return null; //緩存的空對象值}if (StrUtil.isNotEmpty(json)) {return JSONUtil.toBean(json, clazz);}//加鎖,防止緩存擊穿問題 -> redis的熱點key問題RLock redissonClientLock = redissonClient.getLock(RedisConstants.DISTRIBUTED_LOCK + key);redissonClientLock.lock(); //加鎖try {//dcl判斷鎖是否存在了json = get(key);if (json != null) {return queryWithPassThrough(keyPrefix, id, clazz, dbFallBack, time, unit);}//3. 不存在,根據id查詢數據庫R r = dbFallBack.apply(id);if (r != null) {set(key, r, time, unit);return r;}// 存儲空對象set(key, RedisConstants.EMPTY_OBJECT_JSON, RedisConstants.CACHE_NULL_TTL, TimeUnit.SECONDS);return null;} finally {redissonClientLock.unlock();}}
}
3. 雪崩問題
緩存宕機或者在同一時間大面積的失效,導致大量的請求都直接落到了數據庫上,對數據庫造成了巨大的壓力。
解決方式:
- 設置隨機失效時間(可選):為緩存設置隨機的失效時間,例如在固定過期時間的基礎上加上一個隨機值,這樣可以避免大量緩存同時到期,從而減少緩存雪崩的風險。(例如:批量導入數據到redis的時候,如果設置過期時間一致,那么就會數據就會在同一時刻過期刪除)。
- 多級緩存:設計多級緩存,例如本地緩存+Redis 緩存的二級緩存組合,當 Redis 緩存出現問題時,還可以從本地緩存中獲取到部分數據。
- Redis集群:采用 Redis 集群,避免單機出現問題整個緩存服務都沒辦法使用。比如:Redis Sentinel哨兵集群、Redis Cluster分片集群。
- 限流:如果發現讀請求太多,可以采用限流的策略。