手遊服務端框架之使用Guava構建快取系統
快取的作用與應用場景
快取,在專案中的應用非常之廣泛。諸如這樣的場景,某些物件計算或者獲取的程式碼比較昂貴,並且在程式裡你不止一次要用到這些物件,那麼,你就應該使用快取。
快取跟java的CoucurrentMap很類似,但青出於藍勝於藍。CoucurrentMap的特點是,當你往它裡面放元素的時候,你需要自己手動去把它移除。而快取的最大特點是,你無須手動去移除快取裡的元素,而是通過某些移除策略,如果超時或者記憶體空間緊張等等。
本文主要使用Google的guava工具庫來構建我們的快取系統。
首先說一下我們的快取系統需要達到的兩個目標。
第一,在獲取某個物件時,如果物件已在快取裡則直接返回;否則,自動從資料庫讀取並加入到快取,並返回給使用者介面。
第二,當物件長時間沒有被查詢命中的話,自己將物件從快取裡移除。
快取的實現
好,開始我們的編碼......
1.定義抽象快取容器(CacheContainer.java)這裡需要特別說明一下,CacheLoader類表示,當我們從快取裡拿不到物件時,應該從哪裡獲取。這裡,我們覆寫了load(K key)方法,並讓它去呼叫快取容器的loadOnce()抽象方法。怎麼獲取,我們交給子類去完成吧。/** * 快取容器 * @author kingston */ public abstract class CacheContainer<K, V> { private LoadingCache<K, V> cache; public CacheContainer(CacheOptions p) { cache = CacheBuilder.newBuilder() .initialCapacity(p.initialCapacity) .maximumSize(p.maximumSize) //超時自動刪除 .expireAfterAccess(p.expireAfterAccessSeconds, TimeUnit.SECONDS) .expireAfterWrite(p.expireAfterWriteSeconds, TimeUnit.SECONDS) .removalListener(new MyRemovalListener()) .build(new DataLoader()); } public final V get(K k) { try { return cache.get(k); } catch (ExecutionException e) { LoggerUtils.error("CacheContainer get error", e); throw new UncheckedExecutionException(e); } } public abstract V loadOnce(K k) throws Exception; public final void put(K k, V v) { cache.put(k, v); } public final void remove(K k) { cache.invalidate(k); } public final ConcurrentMap<K, V> asMap() { return cache.asMap(); } class DataLoader extends CacheLoader<K, V> { @Override public V load(K key) throws Exception { return loadOnce(key); } } class MyRemovalListener implements RemovalListener<K, V> { @Override public void onRemoval(RemovalNotification<K, V> notification) { //logger } } }
2. 在我們的系統裡,快取所儲存的物件都是可以進行持久化的,而持久化的物件一般至少要提供兩個介面,一個用於從資料庫裡讀取,一個用於儲存到資料庫。但由於我們的物件持久化,並不打算放在快取裡處理,而是通過單獨的執行緒進行入庫(見上一篇文章)。這裡,我們定義一下快取的物件基本介面(Persistable.java)。
/** * 可持久化的 * @author kingston */ public interface Persistable<K, V> { /** * 能從資料庫獲取bean * @param k 查詢主鍵 * @return 持久化物件 * @throws Exception */ V load(K k) throws Exception; // /** // * 將物件序列號到資料庫 // * @param k // * @param v // * @throws PersistenceException // */ // void save(K k, V v) throws Exception; }
3.抽象快取容器的一個預設實現,拿不到快取的讀取策略採用上面的Persistable方案
/**
* 可持久化的
* @author kingston
*/
public interface Persistable<K, V> {
/**
* 能從資料庫獲取bean
* @param k 查詢主鍵
* @return 持久化物件
* @throws Exception
*/
V load(K k) throws Exception;
// /**
// * 將物件序列號到資料庫
// * @param k
// * @param v
// * @throws PersistenceException
// */
// void save(K k, V v) throws Exception;
}
4. 定義抽象快取服務(CacheService.java)。按理說,快取系統只需要提供一個獲取元素的get(key)方法即可。不過,為了能適應一些奇怪的情形,我們還是可以加入手動新增元素的put()方法,還有手動刪除快取的remove()方法。
/**
* 抽象快取服務
* @author kingston
*/
public abstract class CacheService<K, V> implements Persistable<K, V> {
private final CacheContainer<K, V> container;
public CacheService() {
this(CacheOptions.defaultCacheOptions());
}
public CacheService(CacheOptions p) {
container = new DefaultCacheContainer<>(this, p);
}
/**
* 通過key獲取物件
* @param key
* @return
*/
public V get(K key) {
return container.get(key);
}
/**
* 手動移除快取
* @param key
* @return
*/
public void remove(K key) {
container.remove(key);
}
/**
* 手動加入快取
* @param key
* @return
*/
public void put(K key, V v) {
this.container.put(key, v);
}
}
5.配置類(CacheOptions.java)只是對快取的一些配置的封閉,沒啥好說的,直接上程式碼吧。
/**
* 快取相關配置
* @author kingston
*/
public class CacheOptions {
private final static int DEFAULT_INITIAL_CAPACITY = 1024;
private final static int DEFAULT_MAXIMUM_SIZE = 65536;
private final static int DEFAULT_EXPIRE_AFTER_ACCESS_SECONDS = (int)(5*TimeUtils.ONE_HOUR/TimeUtils.ONE_MILLSECOND);
private final static int DEFAULT_EXPIRE_AFTER_WRITE_SECONDS = (int)(5*TimeUtils.ONE_HOUR/TimeUtils.ONE_MILLSECOND);
public final int initialCapacity;
public final int maximumSize;
public final int expireAfterAccessSeconds;
public final int expireAfterWriteSeconds;
private CacheOptions(int initialCapacity, int maximumSize, int expireAfterAccessSeconds, int expireAfterWriteSeconds) {
this.initialCapacity = initialCapacity;
this.maximumSize = maximumSize;
this.expireAfterAccessSeconds = expireAfterAccessSeconds;
this.expireAfterWriteSeconds = expireAfterWriteSeconds;
}
public static CacheOptions defaultCacheOptions() {
return new Builder().build();
}
static class Builder {
private int initialCapacity;
private int maximumSize;
private int expireAfterAccessSeconds;
private int expireAfterWriteSeconds;
private Builder() {
}
public Builder setInitialCapacity(int initialCapacity) {
this.initialCapacity = initialCapacity;
return this;
}
public Builder setMaximumSize(int maximumSize) {
this.maximumSize = maximumSize;
return this;
}
public Builder setExpireAfterAccessSeconds(int expireAfterAccessSeconds) {
this.expireAfterAccessSeconds = expireAfterAccessSeconds;
return this;
}
public Builder setExpireAfterWriteSeconds(int expireAfterWriteSeconds) {
this.expireAfterWriteSeconds = expireAfterWriteSeconds;
return this;
}
private CacheOptions build() {
if (initialCapacity == 0) {
setInitialCapacity(DEFAULT_INITIAL_CAPACITY);
}
if (maximumSize == 0) {
setMaximumSize(DEFAULT_MAXIMUM_SIZE);
}
if(expireAfterAccessSeconds == 0) {
setExpireAfterAccessSeconds(DEFAULT_EXPIRE_AFTER_ACCESS_SECONDS);
}
if(expireAfterWriteSeconds == 0) {
setExpireAfterWriteSeconds(DEFAULT_EXPIRE_AFTER_WRITE_SECONDS);
}
return new CacheOptions(initialCapacity, maximumSize, expireAfterAccessSeconds, expireAfterWriteSeconds);
}
}
}
業務邏輯使用快取系統
工具框架搭起來了,來點業務程式碼吧玩家管理,最直接的應用場景。我們通過id來查詢玩家的時候,策略肯定是這樣的,如果玩家已經登入了,那麼一定能在記憶體裡找到,否則,就去資料庫撈角色。
所以我們的PlayerManager類就可以繼承抽象快取服務CacheService啦。泛型裡的key就是玩家的主鍵playerId, value就是玩家物件了。
public class PlayerManager extends CacheService<Long, Player> {
/**
* 從使用者表裡讀取玩家資料
*/
@Override
public Player load(Long playerId) throws Exception {
String sql = "SELECT * FROM Player where Id = {0} ";
sql = MessageFormat.format(sql, String.valueOf(playerId));
Player player = DbUtils.queryOne(DbUtils.DB_USER, sql, Player.class);
return player;
}
}
測試快取
寫個簡單的JUnit測試類跑一下吧^_^/**
* 測試玩家快取系統
* @author kingston
*/
public class TestPlayerCache {
@Before
public void init() {
//初始化orm框架
OrmProcessor.INSTANCE.initOrmBridges();
//初始化資料庫連線池
DbUtils.init();
}
@Test
public void testQueryPlayer() {
long playerId = 10000L;
//預先保證使用者資料表playerId = 10000的資料存在
Player player = PlayerManager.getInstance().get(playerId);
//改變記憶體裡的玩家名稱
player.setName("newPlayerName");
//記憶體裡玩家的新名稱
String playerName = player.getName();
//通過同一個id再次獲取玩家資料
Player player2 = PlayerManager.getInstance().get(playerId);
//驗證新的玩家就是記憶體裡的玩家,因為如果又是從資料庫裡讀取,那麼名稱肯定跟記憶體的不同!!
assertTrue(playerName.equals(player2.getName()));
}
}
文章預告:下一篇主要介紹GM命令系統的設計。手遊服務端開源框架系列完整的程式碼請移步github ->>game_server