溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java中怎么設計本地緩存

發布時間:2021-08-13 14:15:53 來源:億速云 閱讀:178 作者:Leah 欄目:編程語言

這篇文章給大家介紹Java中怎么設計本地緩存,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

  1.數據結構

  首要考慮的就是數據該如何存儲,用什么數據結構存儲,最簡單的就直接用Map來存儲數據;或者復雜的如redis一樣提供了多種數據類型哈希,列表,集合,有序集合等,底層使用了雙端鏈表,壓縮列表,集合,跳躍表等數據結構;

  2.對象上限

  因為是本地緩存,內存有上限,所以一般都會指定緩存對象的數量比如1024,當達到某個上限后需要有某種策略去刪除多余的數據;

  3.清除策略

  上面說到當達到對象上限之后需要有清除策略,常見的比如有LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用)等策略;

  4.過期時間

  除了使用清除策略,一般本地緩存也會有一個過期時間設置,比如redis可以給每個key設置一個過期時間,這樣當達到過期時間之后直接刪除,采用清除策略+過期時間雙重保證;

  5.線程安全

  像redis是直接使用單線程處理,所以就不存在線程安全問題;而我們現在提供的本地緩存往往是可以多個線程同時訪問的,所以線程安全是不容忽視的問題;并且線程安全問題是不應該拋給使用者去保證;

  6.簡明的接口

  提供一個傻瓜式的對外接口是很有必要的,對使用者來說使用此緩存不是一種負擔而是一種享受;提供常用的get,put,remove,clear,getSize方法即可;

  7.是否持久化

  這個其實不是必須的,是否需要將緩存數據持久化看需求;本地緩存如ehcache是支持持久化的,而guava是沒有持久化功能的;分布式緩存如redis是有持久化功能的,memcached是沒有持久化功能的;

  8.阻塞機制

  在看Mybatis源碼的時候,二級緩存提供了一個blocking標識,表示當在緩存中找不到元素時,它設置對緩存鍵的鎖定;這樣其他線程將等待此元素被填充,而不是命中數據庫;其實我們使用緩存的目的就是因為被緩存的數據生成比較費時,比如調用對外的接口,查詢數據庫,計算量很大的結果等等;這時候如果多個線程同時調用get方法獲取的結果都為null,每個線程都去執行一遍費時的計算,其實也是對資源的浪費;比較好的辦法是只有一個線程去執行,其他線程等待,計算一次就夠了;但是此功能基本上都交給使用者來處理,很少有本地緩存有這種功能;

  如何實現

  以上大致介紹了實現一個本地緩存我們都有哪些需要考慮的地方,當然可能還有其他沒有考慮到的點;下面繼續看看關于每個點都應該如何去實現,重點介紹一下思路;

  1.數據結構

  本地緩存最常見的是直接使用Map來存儲,比如guava使用ConcurrentHashMap,ehcache也是用了ConcurrentHashMap,Mybatis二級緩存使用HashMap來存儲:

  Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>()

  Mybatis使用HashMap本身是非線程安全的,所以可以看到起內部使用了一個SynchronizedCache用來包裝,保證線程的安全性;

  當然除了使用Map來存儲,可能還使用其他數據結構來存儲,比如redis使用了雙端鏈表,壓縮列表,整數集合,跳躍表和字典;當然這主要是因為redis對外提供的接口很豐富除了哈希還有列表,集合,有序集合等功能;

  2.對象上限

  本地緩存常見的一個屬性,一般緩存都會有一個默認值比如1024,在用戶沒有指定的情況下默認指定;當緩存的數據達到指定最大值時,需要有相關策略從緩存中清除多余的數據這就涉及到下面要介紹的清除策略;

  3.清除策略

  配合對象上限之后使用,場景的清除策略如:LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用);

  LRU:Least Recently Used的縮寫最近最少使用,移除最長時間不被使用的對象;常見的使用LinkedHashMap來實現,也是很多本地緩存默認使用的策略;

  FIFO:先進先出,按對象進入緩存的順序來移除它們;常見使用隊列Queue來實現;

  LFU:Least Frequently Used的縮寫大概也是最近最少使用的意思,和LRU有點像;區別點在LRU的淘汰規則是基于訪問時間,而LFU是基于訪問次數的;可以通過HashMap并且記錄訪問次數來實現;

  SOFT:軟引用基于垃圾回收器狀態和軟引用規則移除對象;常見使用SoftReference來實現;

  WEAK:弱引用更積極地基于垃圾收集器狀態和弱引用規則移除對象;常見使用WeakReference來實現;

  4.過期時間

  設置過期時間,讓緩存數據在指定時間過后自動刪除;常見的過期數據刪除策略有兩種方式:被動刪除和主動刪除;

  被動刪除:每次進行get/put操作的時候都會檢查一下當前key是否已經過期,如果過期則刪除,類似如下代碼:

  if (System.currentTimeMillis() - lastClear > clearInterval) {

  clear();

  }

  主動刪除:專門有一個job在后臺定期去檢查數據是否過期,如果過期則刪除,這其實可以有效的處理冷數據;

  5.線程安全

  盡量用線程安全的類去存儲數據,比如使用ConcurrentHashMap代替HashMap;或者提供相應的同步處理類,比如Mybatis提供了SynchronizedCache:

  public synchronized void putObject(Object key, Object object) {

  ...省略...

  }

  @Override

  public synchronized Object getObject(Object key) {

  ...省略...

  }

  6.簡明的接口

  提供常用的get,put,remove,clear,getSize方法即可,比如Mybatis的Cache接口:

  public interface Cache {

  String getId();

  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();

  ReadWriteLock getReadWriteLock();

  }

  再來看看guava提供的Cache接口,相對來說也是比較簡潔的:

  public interface Cache<K, V> {

  V getIfPresent(@CompatibleWith("K") Object key);

  V get(K key, Callable<? extends V> loader) throws ExecutionException;

  ImmutableMap<K, V> getAllPresent(Iterable<?> keys);

  void put(K key, V value);

  void putAll(Map<? extends K, ? extends V> m);

  void invalidate(@CompatibleWith("K") Object key);

  void invalidateAll(Iterable<?> keys);

  void invalidateAll();

  long size();

  CacheStats stats();

  ConcurrentMap<K, V> asMap();

  void cleanUp();

  }

  7.是否持久化

  持久化的好處是重啟之后可以再次加載文件中的數據,這樣就起到類似熱加載的功效;比如ehcache提供了是否持久化磁盤緩存的功能,將緩存數據存放在一個.data文件中;

  diskPersistent="false" //是否持久化磁盤緩存

  redis更是將持久化功能發揮到極致,慢慢的有點像數據庫了;提供了AOF和RDB兩種持久化方式;當然很多情況下可以配合使用兩種方式;

  8.阻塞機制

  除了在Mybatis中看到了BlockingCache來實現此功能,之前在看<<java并發編程實戰>>的時候其中有實現一個很完美的緩存,大致代碼如下:

  public class Memoizerl<A, V> implements Computable<A, V> {

  private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();

  private final Computable<A, V> c;

  public Memoizerl(Computable<A, V> c) {

  this.c = c;

  }

  @Override

  public V compute(A arg) throws InterruptedException, ExecutionException {

  while (true) {

  Future<V> f = cache.get(arg);

  if (f == null) {

  Callable<V> eval = new Callable<V>() {

  @Override

  public V call() throws Exception {

  return c.compute(arg);

  }

  };

  FutureTask<V> ft = new FutureTask<V>(eval);

  f = cache.putIfAbsent(arg, ft);

  if (f == null) {

  f = ft;

  ft.run();

  }

  try {

  return f.get();

  } catch (CancellationException e) {

  cache.remove(arg, f);

  }

  }

  }

  }

  }

  compute是一個計算很費時的方法,所以這里把計算的結果緩存起來,但是有個問題就是如果兩個線程同時進入此方法中怎么保證只計算一次,這里最核心的地方在于使用了ConcurrentHashMap的putIfAbsent方法,同時只會寫入一個FutureTask;

關于Java中怎么設計本地緩存就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女