並發讀寫緩存實現機制(零):緩存操作指南


 

    游戲中為了提高系統運行速度和游戲承載量,使用緩存是一個必要的手段。本文中的緩存是在guava緩存的基礎上增加了數據的持久化狀態和異步同步數據的功能,同時對調用API做了封裝,以達到簡化操作、屏蔽內部實現的目的。

    在介紹緩存的原理之前,為了一些朋友閱讀方便,本文先介紹下緩存的API和使用方法,以幫助大家對本緩存有個大概的理解。這篇文章大家簡單閱讀即可,后面我們會詳細介紹緩存的實現細節。
  
  系列文章目錄:
  並發讀寫緩存實現機制(三): API封裝和簡化
 
  文中緩存最新源碼請參考: https://github.com/cm4j/cm4j-all

緩存的操作指南

1.數據結構簡介

    本文緩存的目的就是 為了減少開發的編碼量、提高編碼的效率,同時為了方便調用,本緩存在對外接口上做了許多封裝,內部也提供了一些常用的緩存類型以供使用。在進一步了解使用方法前,我們先來看下緩存的結構圖:
清單1:緩存簡略結構圖
 
類的功能簡介:
    ConcurrentCache:核心操作類,大部分業務都是由此類完成
    CacheLoader:緩存的加載類
    AbsReference:緩存數據封裝抽象類,緩存中實際存儲的就是此對象,此類提供了一些常用的方法以方便調用者使用,默認提供了增刪改查等方法,文中緩存默認提供了3種常用緩存的實現。為什么需要這個類?主要是為了屏蔽緩存的內部狀態。
    CacheEntry:單個緩存對象或集合緩存中的一個元素,應該與DB的entity一一對應,持久化時需要把它轉化為實體entity然后進行持久化操作
 
    CacheDefiniens:緩存的定義抽象類,主要用於定義緩存如何從db加載
    PrefixMapping:緩存key與前綴的映射類
 
緩存的數據流轉:
  1.使用一個緩存,首先我們需要定義一個緩存,定義緩存是 CacheDefiniens實現的功能,它描述了緩存是如何從DB加載的。
  2.每個緩存就像我們一樣,每個都應該有一個獨一無二的名字,名字和具體的緩存是有映射關系的,這個關系就是通過 PrefixMapping來維護的。
  3.在本系列中,緩存的核心操作都是通過 ConcurrentCache實現的,包括了緩存的讀取、保存、過期以及持久化等等,當然也包含了對緩存的具體數據 AbsReference的操作。
  4.緩存的加載是通過 CacheLoader實現的 ,加載之后,每個數據的存在形態就是 AbsReference,它可以是single、list、map或者其他自定義結構。
  5.AbsReference內部結構允許 有一個或多個元素,如果這些元素需要保存DB,則它們必須是CacheEntry的子類,因為緩存就是通過CacheEntry來進行持久化的。
    因此大部分情況下 緩存的創建, 我們只需要擴展 CacheDefiniens、修改PrefixMapping類就可以了,詳情可參照下面的例子。
 
3種常見的緩存類型
    日常來說,我們最常用到的數據結構就是單個對象、List對象或者Map對象。AbsReference是對緩存數據的一種封裝,緩存中存儲的數據就是它,其繼承結構請看清單2
清單2:默認實現的3種常見的緩存類型

2.緩存的創建

    上面提到系統默認提供了3種常見的數據結構,如果我們要使用這3種結構,那僅僅需要兩步即可完成:一是定義緩存是如何從DB加載,二是定義緩存key和前綴的映射,而這兩步主要是由CacheDefiniens PrefixMapping完成。
 
step1:緩存的定義
 
 清單3:map類型的緩存定義
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
public  class TmpListMultikeyMapCache  extends CacheDefiniens<MapReference<Integer, TmpListMultikey>> {
     public TmpListMultikeyMapCache() {
    }
     public TmpListMultikeyMapCache( int playerId) {
         super(playerId);
    }
    @Override
     public MapReference<Integer, TmpListMultikey> load( String... params) {
        Preconditions.checkArgument(params.length ==  1);
        HibernateDao<TmpListMultikey, Integer> hibernate = ServiceManager.getInstance().getSpringBean( "hibernateDao");
        hibernate.setPersistentClass(TmpListMultikey. class);
         String hql =  "from TmpListMultikey where id.NPlayerId = ?";
        List<TmpListMultikey> all = hibernate.findAll(hql, NumberUtils.toInt(params[ 0]));

        Map<Integer, TmpListMultikey> map =  new HashMap<Integer, TmpListMultikey>();
         for (TmpListMultikey tmpListMultikey : all) {
            map.put(tmpListMultikey.getId().getNType(), tmpListMultikey);
        }
         return  new MapReference<Integer, TmpListMultikey>(map);
    }
}
 
    這段代碼非常簡潔:兩個構造函數外加覆蓋父類的load方法。其中,根據名稱我們知道load()方法就是從DB中加載數據,空參的構造函數是創建描述類使用,非空構造函數則是傳遞參數的需要。
    為了代碼生成的便捷, CacheDefiniens采用了范型來規范代碼結構。內部實現中,有參構造函數將參數拼為字符串,在需要從DB加載時會再把字符串切分為字符串數組,然后作為參數調用load方法,因此load的params參數和有參構造函數中的參數其實是一致的。
    注意19行返回的就是緩存的封裝類,構造函數參數就是從DB中查詢出來的map結果;而 TmpListMultikey則是CacheEntry的一個子類,它是map集合的一個元素,同時提供了 parseEntity()方法將對象轉化Entity 保存到DB中
 
step2:緩存的映射
 
 清單4:緩存定義與前綴的映射
1
2
3
4
5
6
7
 
public enum PrefixMappping {
    $1(TmpFhhdCache. class),
    $2(TmpListMultikeyListCache. class),
    $3(TmpListMultikeyMapCache. class);

     // 部分代碼省略
}
 
    上面這段就更簡單了,一個枚舉類,一個鍵一個緩存描述類,非常簡單。
    至此,我們就完成了緩存的創建,僅僅必須的兩 步操作 我們就擁有了對緩存的增刪改查權限,沒有復雜的設定和配置、 無需關注內部實現和異步寫入DB ,內部實現機制已經屏蔽了所有不相關的代碼和步驟。

3.緩存的讀取

    創建好了緩存的定義、對緩存進行了鍵的映射之后,接下來我們就要看下緩存的使用,大家由清單1可以看到ConcurrentCache是緩存的核心操作類,因此大部分操作最后都是操作在這個類上。在此基礎上,為了調用方便,緩存也擴展了一些其他便捷方法來簡化調用,請看下面對緩存讀取的一些例子:
 
 清單4:緩存的讀取
1
2
3
4
5
6
7
8
9
10
11
12
13
 
@Test
public  void getTest() {
     // Single格式緩存獲取
    SingleReference<TmpFhhd> singleRef = ConcurrentCache.getInstance().get( new TmpFhhdCache( 50769));
    TmpFhhd fhhd = singleRef.get();
    TmpFhhd fhhd2 =  new TmpFhhdCache( 50769).ref().get();
    Assert.assertTrue(fhhd == fhhd2);

     // List格式緩存獲取
    List<TmpListMultikey> list = ConcurrentCache.getInstance().get( new TmpListMultikeyListCache( 50705)).get();
     // Map格式緩存獲取
    Map<Integer, TmpListMultikey> map =  new TmpListMultikeyMapCache( 1001).ref().get();
}
 
    由上面的例子,我們可以看到,不管是那種類型的緩存,我們都有兩種方式獲取:
    1. ConcurrentCache.getInstance().get( new  TmpFhhdCache( 50769 ))
    2. new  TmpFhhdCache( 50769 ).ref()
    上面的 new  TmpFhhdCache( 50769 )就是我們前面的緩存的定義類 ,這兩種方式都能獲取到AbsReference,也就是緩存中實際存儲的數據,后面可以使用這個對象來對緩存進行增刪改查操作。

4.緩存的增刪改查

    對於增刪改查,緩存更多的依賴於AbsReference類。一方面,緩存讀取直接獲取的就是這個封裝類;另一方面,這個類也屏 蔽了ConcurrentCache和緩存狀態控制,減少調用者出錯的概率。
 
 清單5:緩存的增刪改查I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
@Test
public  void updateTest() {
    SingleReference<TmpFhhd> singleRef =  new TmpFhhdCache( 50769).ref();
    TmpFhhd tmpFhhd = singleRef.get();
     if (tmpFhhd == null) {
         // 新增
        tmpFhhd =  new TmpFhhd( 507691010"");
    }  else {
         // 修改
        tmpFhhd.setNCurToken( 10);
    }
     // 新增或修改都可以調用update
    singleRef.update(tmpFhhd);
    Assert.assertTrue( new TmpFhhdCache( 50769).ref().get().getNCurToken() ==  10);

     // 刪除
    singleRef.delete();
    Assert.assertNull( new TmpFhhdCache( 50769).ref().get());

     // 立即保存緩存到DB
    singleRef.persist();
}
 
  對於已經存在於緩存中的對象,我們可以直接調用update()進行修改,也可以直接調用delete()進行刪除
  這樣如果直接從緩存中拿到對象,如果對象存在,可直接修改或刪除,而無需AbsReference的介入
 
  清單6:緩存的增刪改查II
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
@Test
public  void update2Test() {
    MapReference<Integer, TmpListMultikey> mapRef =  new TmpListMultikeyMapCache( 1001).ref();
    TmpListMultikey value = mapRef.get( 1);
     if (value == null) {
        mapRef.put( 1new TmpListMultikey( new TmpListMultikeyPK( 10011),  99));
    }

    TmpListMultikey newValue =  new TmpListMultikeyMapCache( 1001).ref().get( 1);
    newValue.setNValue( 2);
     // 對於已經存在於緩存中的對象
     // 我們可以直接調用update()進行修改
    newValue.update();
    Assert.assertTrue( new TmpListMultikeyMapCache( 1001).ref().get( 1).getNValue() ==  2);

     // 也可以直接調用delete()進行刪除
    newValue.delete();
    Assert.assertNull( new TmpListMultikeyMapCache( 1001).ref().get( 1));
}

5.緩存的擴展

    上面的幾個例子,我們演示了常用的緩存的使用方法,一般來說已基本可以滿足大部分需求,但是需求總是無止境的,在無法滿足的情況下,我們就需要對現有系統進行擴展,本緩基於基本框架提供了部分擴展點。
    首先,我們最常遇到的就是業務需要更復雜的數據類型, 現有緩存提供簡單的single、list或map已經無法滿足業務需求,這時只要繼承AbsReference類,實現其內部業務即可。
    其次,如果需要的緩存類型恰巧是 single、list或map,同時又需要增加些額外功能,那只要繼承對應的類擴展功能就可以了。
    大部分情況下,我們可把DB的entity直接設為CacheEntry的子類,這樣代碼量比較少,而且entity可直接生成。但某些情況,我們需要比Entity更多的屬性,也就是我們需要單獨的POJO來存儲緩存,這時候我們也可以新建POJO來繼承CacheEntry
 
    本文簡單介紹了緩存的結構及幾種常用方法,接下來幾章我會分別從讀取、寫入、數據過期和異步寫入等幾個方面來介紹緩存的內部實現,敬請期待。
 
原創文章,請注明引用來源: CM4J


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM