訂單緩存實踐
最近在做訂單緩存查詢相關需求,記錄下該過程中緩存查詢考慮的幾個問題以及處理方案。
緩存穿透
實際場景中使用緩存都是先去緩存中查詢,如果緩存沒有命中,在去查詢數據庫並將結果緩存。如果查詢一個在系統中根本就不存在的數據,就會造成每次請求都會穿透緩存去查詢數據庫。如果出現大量的緩存穿透(或者惡意攻擊),就會對數據庫造成比較大的壓力。
處理方案
對於數據庫中不存在的數據,存儲特定的值表示數據不存在。在發生insert之后將緩存中對應數據移除,避免在數據生成之后緩存中查詢還是NULL。
1 public Order findById(Long orderId) { 2 // 從數據庫查詢 3 Order order = getOrderFromDB(orderId); 4 if (order == null) { 5 // 訂單不存在時候填入特定"NULL"表示訂單不存在 6 putOrderToCache(orderId, "NULL"); 7 } else { 8 putOrderToCache(orderId, order); 9 } 10 return order; 11 }
緩存並發
在查詢同一並發量較大情況下,如果該訂單緩存失效,就會造成這一瞬間所有的訂單查詢請求都會訪問到數據庫,造成數據庫壓力增大。
處理方案
並發查詢同一訂單,緩存中不存在,加鎖查詢數據庫,其余請求等待獲取鎖或者獲取超時返回數據或者查詢數據庫。
public Order findById(Long orderId) { // 從緩存中獲取 Result result = getOrderFromCache(orderId); Order order = result.getOrder(); if (result.isEmpty()) { // 從DB獲取數據 order = getOrderFromDB(orderId); // 寫入緩存 putOrderToCache(orderId, order); } return order; }
上面這種寫法,一般是沒什么問題,在並發量大時候會造成所有請求都查詢到數據庫。並發100個請求查詢同一個訂單,當緩存沒數據時100請求都會到DB。為了處理這種情況,解決的方式就是加鎖。
public Order findById(Long orderId) { // 從緩存中獲取 Result result = getOrderFromCache(orderId); Order order = result.getOrder(); if (result.isEmpty()) { try { if (lock.tryLock(1)) { // 從DB獲取數據 order = getOrderFromDB(orderId); // 寫入緩存 putOrderToCache(orderId, order); } else { Thread.sleep(10);//10毫秒根據業務場景自定義 return findById(orderId); } } catch (Exception e) { // } finally { lock.unlock(); } } return order; }
嘗試獲取一次鎖,並發情況下,獲取不到鎖就重新走一遍流程(先查緩存,在查DB),加鎖時間不要設置太長,避免過多的線程在等待。采用這種方式,當有100個請求並發查詢時候,一個線程拿到鎖查詢DB,剩余99個線程在等待10毫秒重試,比如查詢DB線程獲取到數據需要耗時5毫秒,10毫秒之后99個線程發現緩存中有值,直接從緩存中取值,耗時10毫秒。
緩存失效
為了避免緩存中的數據越來越多或者緩存一些很少用到或者根本不會使用到數據,通常都會在訂單緩存中加入過期時間,比如幾分鍾。當某些情況下,會出現多個訂單的過期時間是一樣的,即多個訂單緩存數據同時失效。
處理方案
對於每個訂單的緩存失效時間都不一樣,失效時間都是一個范圍內隨機值,可以在一定程度上減少緩存同時失效