游戲中為了提高系統運行速度和游戲承載量,使用緩存是一個必要的手段。本文中的緩存是在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( 50769, 10, 10, ""); } 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( 1, new TmpListMultikey( new TmpListMultikeyPK( 1001, 1), 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