手游服务端框架之使用Guava构建缓存系统

原创 2017年07月23日 15:03:00

缓存的作用与应用场景

缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存。

缓存跟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




版权声明:本文为博主原创文章,未经博主允许不得转载。

Netty网络聊天室之会话管理

浏览器第一次与服务器建立连接的时候,服务器就会自动为之分配一个Session。在我们的聊天室,也可以使用Session来判断用户是否经过登录验证,保存用户的各种信息,向客户端发送消息。这极大方便了程序...

Dijkstra算法求解最短路径

Dijkstra算法是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有向图中最短路径问题。迪杰斯特拉算法主要特点是以起始点为...

手游服务端框架之配置与玩家数据库设计

一款网络游戏的设计,至少需要策划数据库和用户数据库两种数据库。本文主要介绍这两种数据库的设计及使用,同时,介绍如何通过ORM框架来完成玩家数据的持久化。...

手游服务端框架之模仿SpringMvc处理玩家请求

经典web开发项目通常采用三层架构来组织代码。典型的,第一层为表现层,通常使用MVC模式;第二层为业务逻辑层,该层主要是各种service业务操作类;第三层则为数据访问层。类似的,我们的游戏项目也可以...

手游服务端框架之消息线程模型

玩家的消息请求如果放在mina的io线程池进行处理,当业务处理非常耗时,会严重影响io的吞吐量。所以,我们应该另起用于处理业务逻辑的线程池,采用生产者消费者模型,异步处理玩家请求。...

手游服务端开发基础概念扫盲篇

从事手游服务端开发也快3年了,整理了一份资料,介绍在开发过程中碰到的概念以及自己的理解,希望能够帮到即将从事该职业的朋友。...

TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析

TYPESDK 服务端设计思路与架构之一:应用场景分析              作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够...

TYPESDK手游聚合SDK服务端设计思路与架构之五:流程优化之特殊流程处理

在之前的几篇文字中,我们分析了从零开始搭建一个渠道聚合SDK服务端所需要应对的几个最重要的一般性流程。按照文中的内容,我们大可以自己最擅长的语言和工具开发出一套已经可以正常工作的服务端,这个服务端可以...

高效率完成一次接入80个手游渠道SDK——游戏接入SDK服务端篇

.1 概要     通常,游戏开发商并不会只在一个渠道上线他们的游戏,接入越多的渠道,代表着可能获取越多的用户,但同时也代表着越多的接入SDK工作量、工期和费用。一款游戏要有足够的用户,甚至需要接入3...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:手游服务端框架之使用Guava构建缓存系统
举报原因:
原因补充:

(最多只允许输入30个字)