共計 7410 個字符,預計需要花費 19 分鐘才能閱讀完成。
這篇文章主要介紹“Redis 和本地緩存使用的技巧有哪些”,在日常操作中,相信很多人在 Redis 和本地緩存使用的技巧有哪些問題上存在疑惑,丸趣 TV 小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Redis 和本地緩存使用的技巧有哪些”的疑惑有所幫助!接下來,請跟著丸趣 TV 小編一起來學習吧!
三種緩存的使用場景
這部分會介紹 redis,比如 guava 的 LoadingCache 和快手開源的 ReloadableCache 的使用場景和局限,通過這一部分的介紹就能知道在怎樣的業務場景下應該使用哪種緩存,以及為什么。
Redis 的使用場景和局限性
如果寬泛的說 redis 何時使用,那么自然就是用戶訪問量過高的地方使用,從而加速訪問,并且緩解數據庫壓力。如果細分的話,還得分為單節點問題和非單節點問題。
如果一個頁面用戶訪問量比較高,但是訪問的不是同一個資源。比如用戶詳情頁,訪問量比較高,但是每個用戶的數據都是不一樣的,這種情況顯然只能用分布式緩存了,如果使用 redis,key 為用戶唯一鍵,value 則是用戶信息。
redis 導致的緩存擊穿。
但是需要注意一點,一定要設置過期時間,而且不能設置到同一時間點過期。舉個例子,比如用戶又個活動頁,活動頁能看到用戶活動期間獲獎數據,粗心的人可能會設置用戶數據的過期時間點為活動結束,這樣會
單(熱)點問題
單節點問題說的是 redis 的單個節點的并發問題,因為對于相同的 key 會落到 redis 集群的同一個節點上,那么如果對這個 key 的訪問量過高,那么這個 redis 節點就存在并發隱患, 這個 key 就稱為熱 key。
如果所有用戶訪問的都是同一個資源,比如小愛同學 app 首頁對所有用戶展示的內容都一樣(初期),服務端給 h6 返回的是同一個大 json,顯然得使用到緩存。首先我們考慮下用 redis 是否可行,由于 redis 存在單點問題,如果流量過大的話,那么所有用戶的請求到達 redis 的同一個節點,需要評估該節點能否抗住這么大流量。我們的規則是,如果單節點 qps 達到了千級別就要解決單點問題了(即使 redis 號稱能抗住十萬級別的 qps),最常見的做法就是使用本地緩存。顯然小愛 app 首頁流量不過百,使用 redis 是沒問題的。
LoadingCache 的使用場景和局限性
對于這上面說的熱 key 問題,我們最直接的做法就是使用本地緩存,比如你最熟悉的 guava 的 LoadingCache,但是使用本地緩存要求能夠接受一定的臟數據,因為如果你更新了首頁,本地緩存是不會更新的,它只會根據一定的過期策略來重新加載緩存,不過在我們這個場景是完全沒問題的,因為一旦在后臺推送了首頁后就不會再去改變了。即使改變了也沒問題,可以設置寫過期為半小時,超過半小時重新加載緩存,這種短時間內的臟數據我們是可以接受的。
LoadingCache 導致的緩存擊穿
雖然說本地緩存和機器上強相關的,雖然代碼層面寫的是半小時過期,但由于每臺機器的啟動時間不同,導致緩存的加載時間不同,過期時間也就不同,也就不會所有機器上的請求在同一時間緩存失效后都去請求數據庫。但是對于單一一臺機器也是會導致緩存穿透的,假如有 10 臺機器,每臺 1000 的 qps,只要有一臺緩存過期就可能導致這 1000 個請求同時打到了數據庫。這種問題其實比較好解決,但是容易被忽略,也就是在設置 LoadingCache 的時候使用 LoadingCache 的 load-miss 方法,而不是直接判斷 cache.getIfPresent()== null 然后去請求 db;前者會加虛擬機層面的鎖,保證只有一個請求打到數據庫去,從而完美的解決了這個問題。
但是,如果對于實時性要求較高的情況,比如有段時間要經常做活動,我要保證活動頁面能近實時更新,也就是運營在后臺配置好了活動信息后,需要在 C 端近實時展示這次配置的活動信息,此時使用 LoadingCache 肯定就不能滿足了。
ReloadableCache 的使用場景和局限性
對于上面說的 LoadingCache 不能解決的實時問題,可以考慮使用 ReloadableCache,這是快手開源的一個本地緩存框架,最大的特點是支持多機器同時更新緩存,假設我們修改了首頁信息,然后請求打到的是 A 機器,這個時候重新加載 ReloadableCache,然后它會發出通知,監聽了同一 zk 節點的其他機器收到通知后重新更新緩存。使用這個緩存一般的要求是將全量數據加載到本地緩存,所以如果數據量過大肯定會對 gc 造成壓力,這種情況就不能使用了。由于小愛同學首頁這個首頁是帶有狀態的,一般 online 狀態的就那么兩個,所以完全可以使用 ReloadableCache 來只裝載 online 狀態的首頁。
小結
到這里三種緩存基本都介紹完了,做個小結:
對于非熱點的數據訪問,比如用戶維度的數據,直接使用 redis 即可;
對于熱點數據的訪問,如果流量不是很高,無腦使用 redis 即可;
對于熱點數據,如果允許一定時間內的臟數據,使用 LoadingCache 即可;
對于熱點數據,如果一致性要求較高,同時數據量不大的情況,使用 ReloadableCache 即可;
小技巧
不管哪種本地緩存雖然都帶有虛擬機層面的加鎖來解決擊穿問題,但是意外總有可能以你意想不到的方式發生,保險起見你可以使用兩級緩存的方式即本地緩存 +redis+db。
緩存使用的簡單介紹
這里 redis 的使用就不再多說了,相信很多人對 api 的使用比我還熟悉
LoadingCache 的使用
這個是 guava 提供的網上一抓一大把,但是給兩點注意事項
要使用 load-miss 的話, 要么使用 V get(K key, Callable ? extends V loader);要么使用 build 的時候使用的是 build(CacheLoader ? super K1, V1 loader)這個時候可以直接使用 get()了。此外建議使用 load-miss,而不是 getIfPresent==null 的時候再去查數據庫,這可能導致緩存擊穿;
使用 load-miss 是因為這是線程安全的,如果緩存失效的話,多個線程調用 get 的時候只會有一個線程去 db 查詢,其他線程需要等待,也就是說這是線程安全的。
LoadingCache String, String cache = CacheBuilder.newBuilder()
.maximumSize(1000L)
.expireAfterAccess(Duration.ofHours(1L)) // 多久不訪問就過期
.expireAfterWrite(Duration.ofHours(1L)) // 多久這個 key 沒修改就過期
.build(new CacheLoader String, String () {
@Override
public String load(String key) throws Exception {
// 數據裝載方式,一般就是 loadDB
return key + world
}
});
String value = cache.get(hello // 返回 hello world
reloadableCache 的使用
導入三方依賴
dependency
groupId com.github.phantomthief /groupId
artifactId zknotify-cache /artifactId
version 0.1.22 /version
/dependency
需要看文檔,不然無法使用,有興趣自己寫一個也行的。
public interface ReloadableCache T extends Supplier T {
/**
* 獲取緩存數據
*/
@Override
T get();
/**
* 通知全局緩存更新
* 注意:如果本地緩存沒有初始化,本方法并不會初始化本地緩存并重新加載
*
* 如果需要初始化本地緩存,請先調用 {@link ReloadableCache#get()}
*/
void reload();
/**
* 更新本地緩存的本地副本
* 注意:如果本地緩存沒有初始化,本方法并不會初始化并刷新本地的緩存
*
* 如果需要初始化本地緩存,請先調用 {@link ReloadableCache#get()}
*/
void reloadLocal();
}
老生常談的緩存擊穿 / 穿透 / 雪崩問題
這三個真的是亙古不變的問題,如果流量大確實需要考慮。
緩存擊穿
簡單說就是緩存失效,導致大量請求同一時間打到了數據庫。對于緩存擊穿問題上面已經給出了很多解決方案了。
比如使用本地緩存
本地緩存使用 load-miss 方法
使用第三方服務來加載緩存
1.2 和都說過,主要來看 3。假如業務愿意只能使用 redis 而無法使用本地緩存,比如數據量過大,實時性要求比較高。那么當緩存失效的時候就得想辦法保證只有少量的請求打到數據庫。很自然的就想到了使用分布式鎖,理論上說是可行的,但實際上存在隱患。我們的分布式鎖相信很多人都是使用 redis+lua 的方式實現的,并且在 while 中進行了輪訓,這樣請求量大,數據多的話會導致無形中讓 redis 成了隱患,并且占了太多業務線程,其實僅僅是引入了分布式鎖就加大了復雜度,我們的原則就是能不用就不用。
那么我們是不是可以設計一個類似分布式鎖,但是更可靠的 rpc 服務呢?當調用 get 方法的時候這個 rpc 服務保證相同的 key 打到同一個節點,并且使用 synchronized 來進行加鎖,之后完成數據的加載。在快手提供了一個叫 cacheSetter 的框架。下面提供一個簡易版,自己寫也很容易實現。
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
/**
* @Description 分布式加載緩存的 rpc 服務,如果部署了多臺機器那么調用端最好使用 id 做一致性 hash 保證相同 id 的請求打到同一臺機器。
**/
public abstract class AbstractCacheSetterService implements CacheSetterService {
private final ConcurrentMap String, CountDownLatch loadCache = new ConcurrentHashMap ();
private final Object lock = new Object();
@Override
public void load(Collection String needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
CountDownLatch latch;
Collection CountDownLatch loadingLatchList;
synchronized (lock) {
loadingLatchList = excludeLoadingIds(needLoadIds);
needLoadIds = Collections.unmodifiableCollection(needLoadIds);
latch = saveLatch(needLoadIds);
}
System.out.println(needLoadIds: + needLoadIds);
try {
if (CollectionUtils.isNotEmpty(needLoadIds)) {
loadCache(needLoadIds);
}
} finally {
release(needLoadIds, latch);
block(loadingLatchList);
}
}
/**
* 加鎖
* @param loadingLatchList 需要加鎖的 id 對應的 CountDownLatch
*/
protected void block(Collection CountDownLatch loadingLatchList) {
if (CollectionUtils.isEmpty(loadingLatchList)) {
return;
}
System.out.println(block: + loadingLatchList);
loadingLatchList.forEach(l - {
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
/**
* 釋放鎖
* @param needLoadIds 需要釋放鎖的 id 集合
* @param latch 通過該 CountDownLatch 來釋放鎖
*/
private void release(Collection String needLoadIds, CountDownLatch latch) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
synchronized (lock) {
needLoadIds.forEach(id - loadCache.remove(id));
}
if (latch != null) {
latch.countDown();
}
}
/**
* 加載緩存,比如根據 id 從 db 查詢數據,然后設置到 redis 中
* @param needLoadIds 加載緩存的 id 集合
*/
protected abstract void loadCache(Collection String needLoadIds);
/**
* 對需要加載緩存的 id 綁定 CountDownLatch,后續相同的 id 請求來了從 map 中找到 CountDownLatch,并且 await,直到該線程加載完了緩存
* @param needLoadIds 能夠正在去加載緩存的 id 集合
* @return 公用的 CountDownLatch
*/
protected CountDownLatch saveLatch(Collection String needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return null;
}
CountDownLatch latch = new CountDownLatch(1);
needLoadIds.forEach(loadId - loadCache.put(loadId, latch));
System.out.println(loadCache: + loadCache);
return latch;
}
/**
* 哪些 id 正在加載數據,此時持有相同 id 的線程需要等待
* @param ids 需要加載緩存的 id 集合
* @return 正在加載的 id 所對應的 CountDownLatch 集合
*/
private Collection CountDownLatch excludeLoadingIds(Collection String ids) {
List CountDownLatch loadingLatchList = Lists.newArrayList();
Iterator String iterator = ids.iterator();
while (iterator.hasNext()) {
String id = iterator.next();
CountDownLatch latch = loadCache.get(id);
if (latch != null) {
loadingLatchList.add(latch);
iterator.remove();
}
}
System.out.println(loadingLatchList: + loadingLatchList);
return loadingLatchList;
}
}
業務實現
import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
@Override
protected void loadCache(Collection String needLoadIds) {
// 讀取 db 進行處理
// 設置緩存
}
}
緩存穿透
簡單來說就是請求的數據在數據庫不存在,導致無效請求打穿數據庫。
解法也很簡單,從 db 獲取數據的方法 (getByKey(K key)) 一定要給個默認值。
比如我有個獎池,金額上限是 1W,用戶完成任務的時候給他發筆錢,并且使用 redis 記錄下來,并且落表,用戶在任務頁面能實時看到獎池剩余金額,在任務開始的時候顯然獎池金額是不變的,redis 和 db 里面都沒有發放金額的記錄,這就導致每次必然都去查 db,對于這種情況,從 db 沒查出來數據應該緩存個值 0 到緩存。
緩存雪崩
就是大量緩存集中失效打到了 db,當然肯定都是一類的業務緩存,歸根到底是代碼寫的有問題。可以將緩存失效的過期時間打散,別讓其集中失效就可以了。
到此,關于“Redis 和本地緩存使用的技巧有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注丸趣 TV 網站,丸趣 TV 小編會繼續努力為大家帶來更多實用的文章!