手遊服務端框架之使用Guava構建緩存系統

緩存的作用與應用場景

緩存,在項目中的應用非常之廣泛。諸如這樣的場景,某些對象計算或者獲取的代碼比較昂貴,並且在程序裏你不止一次要用到這些對象,那麼,你就應該使用緩存。

緩存跟java的CoucurrentMap很類似,但青出於藍勝於藍。CoucurrentMap的特點是,當你往它裏面放元素的時候,你需要自己手動去把它移除。而緩存的最大特點是,你無須手動去移除緩存裏的元素,而是通過某些移除策略,如果超時或者內存空間緊張等等。

本文主要使用Google的guava工具庫來構建我們的緩存系統。

首先說一下我們的緩存系統需要達到的兩個目標。

第一,在獲取某個對象時,如果對象已在緩存裏則直接返回;否則,自動從數據庫讀取並加入到緩存,並返回給用戶接口。

第二,當對象長時間沒有被查詢命中的話,自己將對象從緩存裏移除。


緩存的實現

好,開始我們的編碼......

1.定義抽象緩存容器(CacheContainer.java)

/**
 * 緩存容器
 * @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
        }
    }

}
這裏需要特別說明一下,CacheLoader類表示,當我們從緩存裏拿不到對象時,應該從哪裏獲取。這裏,我們覆寫了load(K key)方法,並讓它去調用緩存容器的loadOnce()抽象方法。怎麼獲取,我們交給子類去完成吧。

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




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章